docs: query comments say what each query selects and why, not how it came to be (#6277)

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>
This commit is contained in:
2026-10-23 12:31:13 -04:00
co-authored by Claude Opus 5.5
parent 7e6228c13f
commit 1d4ef1a23f
100 changed files with 648 additions and 698 deletions
+2 -2
View File
@@ -120,7 +120,7 @@ type ListTracksNeedingAcoustIDLookupRow struct {
DurationMs int32
}
// AcoustID recording-id lookup (M401). Migration 0069 says what each
// AcoustID recording-id lookup. Migration 0069 says what each
// lookup state means.
// The lookup worker's queue: present tracks with no tag MBID and no lookup
// since their file last changed. A looked-up id is included once its row is
@@ -257,7 +257,7 @@ type SetTrackMbidFromAcoustIDParams struct {
ID pgtype.UUID
}
// Never over a tag id (D4): the guard is here, not only in the worker, so a
// Never over a tag id: the guard is here, not only in the worker, so a
// tag the scan read between the worker's list and this write still wins.
// updated_at is left alone: it gates the scan's mtime skip, and moving it
// past a file's mtime would hide a change made just before the lookup.
+1 -1
View File
@@ -147,7 +147,7 @@ type ListAcquisitionServicesRow struct {
UpdatedAt pgtype.Timestamptz
}
// Indexers and download clients (M502 #5518).
// Indexers and download clients.
// Returns no secret, only whether one is set: this feeds the settings page.
func (q *Queries) ListAcquisitionServices(ctx context.Context) ([]ListAcquisitionServicesRow, error) {
rows, err := q.db.Query(ctx, listAcquisitionServices)
+1 -1
View File
@@ -109,7 +109,7 @@ type ListListeningNowRow struct {
AlbumTitle string
}
// The admin Overview's status sections (M504): who is listening, what is
// The admin Overview's status sections: who is listening, what is
// downloading, what the library holds. Each is one cheap read.
// Plays in progress: open play events still inside their track's length,
// with a few minutes' grace for a pause. A client that never says a play
+3 -3
View File
@@ -16,7 +16,7 @@ SELECT
(SELECT count(*) FROM requests WHERE status = 'pending')::bigint AS requests,
(SELECT min(requested_at) FROM requests WHERE status = 'pending')::timestamptz AS requests_since,
-- A broken track is one report however it was found: a listener flagged
-- it, or it failed to play (#5606). Counted by track, as the Reports
-- it, or it failed to play. Counted by track, as the Reports
-- page lists them.
(SELECT count(*) FROM (
SELECT track_id FROM quarantine
@@ -53,11 +53,11 @@ type AdminSummaryRow struct {
MissingSince pgtype.Timestamptz
}
// What the admin rail and the Overview's verdict read (#5605): each queue
// What the admin rail and the Overview's verdict read: each queue
// that waits on a person, with how many and since when. One round trip, so
// the rail costs one cheap read on every admin page. The "since" is the
// oldest item still waiting: how long the queue has been left, which is what
// tells the operator a count of 2 is fine today and wrong after a month.
// tells an admin a count of 2 is fine today and wrong after a month.
func (q *Queries) AdminSummary(ctx context.Context) (AdminSummaryRow, error) {
row := q.db.QueryRow(ctx, adminSummary)
var i AdminSummaryRow
+22 -27
View File
@@ -15,7 +15,7 @@ const countAlbums = `-- name: CountAlbums :one
SELECT COUNT(*) FROM albums
`
// M6a: total album count for the /api/library/albums envelope.
// Total album count for the /api/library/albums envelope.
func (q *Queries) CountAlbums(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, countAlbums)
var count int64
@@ -59,7 +59,7 @@ type DeleteAlbumIfEmptyRow struct {
ArtistID pgtype.UUID
}
// M7 #372: deletes the album row only when it has no remaining tracks.
// Deletes the album row only when it has no remaining tracks.
// Used after a track delete to tidy up orphaned albums. RETURNING gives
// us the artist_id so the service can chain into the artist-empty check.
// Returns no rows when the album still has tracks; the service treats
@@ -91,12 +91,12 @@ type GetAlbumByArtistAndTitleParams struct {
}
// Scanner uses this for the no-mbid dedupe path: resolve-or-create. An album
// of the same name in another release group is a different album (Weezer's
// six self-titled albums shared one row until 2026-10-20), so a row whose
// of the same name in another release group is a different album (one
// artist's self-titled albums), so a row whose
// group is known and differs is not returned. A row of the file's own group
// comes first, then one with no group yet, then the one holding the most
// tracks from the file's folder: a file tagged with no group (a live bonus
// track beside Weezer's 1994 album) belongs with its folder, not the oldest
// track beside its album) belongs with its folder, not the oldest
// row, and a few strays already misfiled do not outvote the folder.
func (q *Queries) GetAlbumByArtistAndTitle(ctx context.Context, arg GetAlbumByArtistAndTitleParams) (Album, error) {
row := q.db.QueryRow(ctx, getAlbumByArtistAndTitle,
@@ -166,7 +166,7 @@ type GetAlbumCoverageRollupRow struct {
PendingNoMbid int64
}
// M7 coverage-gauge: library-wide cover-art coverage rollup for the
// Library-wide cover-art coverage rollup for the
// admin gauge. Single sequential scan with FILTER aggregates —
// sub-millisecond on realistic libraries. pending_no_mbid is a SUBSET
// of pending; the UI surfaces it separately so operators can see how
@@ -176,12 +176,9 @@ type GetAlbumCoverageRollupRow struct {
// Invariant: with_art + pending + settled = total. (pending_no_mbid is
// not part of the sum — it's a subset of pending, surfaced separately.)
//
// The IN list below must stay in sync with the cover_art_source CHECK
// constraint — currently relaxed by migration 0020 to include 'deezer'
// and 'lastfm', and migration 0030 further relaxed it to any non-empty
// string. If a new source value is added without updating this query,
// with_art will silently undercount. Drift #556 caught the deezer +
// lastfm omission introduced by migration 0020.
// The IN list below must name every source the enrichers write (the
// cover_art_source CHECK accepts any non-empty string since migration
// 0030). A source missing here makes with_art silently undercount.
func (q *Queries) GetAlbumCoverageRollup(ctx context.Context) (GetAlbumCoverageRollupRow, error) {
row := q.db.QueryRow(ctx, getAlbumCoverageRollup)
var i GetAlbumCoverageRollupRow
@@ -236,7 +233,7 @@ SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, cre
`
// Batched lookup used by /api/library/sync to hydrate upsert payloads
// (#357). Mirror of GetArtistsByIDs.
// . Mirror of GetArtistsByIDs.
func (q *Queries) GetAlbumsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Album, error) {
rows, err := q.db.Query(ctx, getAlbumsByIDs, dollar_1)
if err != nil {
@@ -277,7 +274,7 @@ SELECT release_group_mbid::text
LIMIT 1
`
// #5241: the release group the library already knows for a release id, so a
// The release group the library already knows for a release id, so a
// request carrying a release id can be repaired without asking MusicBrainz.
func (q *Queries) GetReleaseGroupForReleaseMbid(ctx context.Context, mbid *string) (string, error) {
row := q.db.QueryRow(ctx, getReleaseGroupForReleaseMbid, mbid)
@@ -400,7 +397,7 @@ type ListAlbumsAlphaWithArtistRow struct {
ArtistName string
}
// M6a: alpha-sorted album list joined with artist_name. Used by
// Alpha-sorted album list joined with artist_name. Used by
// /api/library/albums for the wrapping-grid page. Stable id-tiebreak.
func (q *Queries) ListAlbumsAlphaWithArtist(ctx context.Context, arg ListAlbumsAlphaWithArtistParams) ([]ListAlbumsAlphaWithArtistRow, error) {
rows, err := q.db.Query(ctx, listAlbumsAlphaWithArtist, arg.Limit, arg.Offset)
@@ -547,12 +544,10 @@ type ListAlbumsByGenreParams struct {
// Album "belongs to" a genre if any of its tracks carry that genre.
// Serves Subsonic getAlbumList?type=byGenre.
//
// Splits tracks.genre on [;,] as of #367. It previously compared the whole
// column verbatim, so a track tagged "Rock;Pop" was unreachable from EITHER
// "Rock" or "Pop" — a Subsonic client asking for a genre silently missed
// every multi-genre track. This also aligns the endpoint with
// recommendation.sql / discover.sql, which have always split, and with the
// genre browse index that #367 adds.
// Splits tracks.genre on [;,], so a track tagged "Rock;Pop" is reachable from
// both "Rock" and "Pop"; comparing the whole column would hide every
// multi-genre track from a Subsonic client asking for a genre. The same split
// as recommendation.sql, discover.sql and the genre browse index.
//
// EXISTS rather than JOIN + DISTINCT ON: the lateral split emits one row per
// (track, genre-fragment), so a join would multiply rows per album and lean
@@ -620,7 +615,7 @@ type ListAlbumsMissingMbidWithTrackRow struct {
// one of its tracks' file_path so the worker can re-read tags. LIMIT
// supplied by caller for batching/progress purposes. The track read is
// one still on disk; an album with none left is skipped until a scan
// finds its files again (#5139).
// finds its files again.
func (q *Queries) ListAlbumsMissingMbidWithTrack(ctx context.Context, limit int32) ([]ListAlbumsMissingMbidWithTrackRow, error) {
rows, err := q.db.Query(ctx, listAlbumsMissingMbidWithTrack, limit)
if err != nil {
@@ -738,7 +733,7 @@ type ListRecentlyAddedAlbumsWithArtistRow struct {
ArtistName string
}
// M6a: recently-added albums joined with artist_name + artist_id for the
// Recently-added albums joined with artist_name + artist_id for the
// home-page section. created_at is the row-insert timestamp from the
// scanner — the closest proxy to "added to library". Stable secondary
// ordering by id keeps pagination tie-break deterministic.
@@ -838,7 +833,7 @@ type SetAlbumCoverWithVersionParams struct {
CoverArtSourcesVersion int32
}
// M7 cover-sources: records a successful or settled enrichment with the
// Records a successful or settled enrichment with the
// current sources_version stamp. cover_art_path is NULL when source is
// 'none' (no file written). Atomic; called per album per pass.
func (q *Queries) SetAlbumCoverWithVersion(ctx context.Context, arg SetAlbumCoverWithVersionParams) error {
@@ -863,7 +858,7 @@ type SetAlbumMbidIfNullParams struct {
Mbid *string
}
// M7 #379: heal MBID on existing album rows during rescan. Idempotent —
// Heal MBID on existing album rows during rescan. Idempotent —
// only updates when the existing mbid is NULL.
func (q *Queries) SetAlbumMbidIfNull(ctx context.Context, arg SetAlbumMbidIfNullParams) error {
_, err := q.db.Exec(ctx, setAlbumMbidIfNull, arg.ID, arg.Mbid)
@@ -882,7 +877,7 @@ type SetAlbumReleaseGroupMbidIfNullParams struct {
ReleaseGroupMbid *string
}
// #5241: heal the release-group id on an existing album row, from a rescan's
// Heal the release-group id on an existing album row, from a rescan's
// tags or from the re-acquisition sweeper's MusicBrainz lookup. Only fills a
// NULL, so a tag never gets overwritten by a later guess or vice versa.
func (q *Queries) SetAlbumReleaseGroupMbidIfNull(ctx context.Context, arg SetAlbumReleaseGroupMbidIfNullParams) error {
@@ -902,7 +897,7 @@ type SetReleaseGroupForReleaseMbidIfNullParams struct {
ReleaseMbid string
}
// #5241: cache a MusicBrainz-resolved release group onto the album row that
// Cache a MusicBrainz-resolved release group onto the album row that
// holds the release, filling only a NULL.
func (q *Queries) SetReleaseGroupForReleaseMbidIfNull(ctx context.Context, arg SetReleaseGroupForReleaseMbidIfNullParams) error {
_, err := q.db.Exec(ctx, setReleaseGroupForReleaseMbidIfNull, arg.ReleaseGroupMbid, arg.ReleaseMbid)
+6 -6
View File
@@ -19,7 +19,7 @@ UPDATE artists
AND artist_art_source = 'none'
`
// M7 cover-sources: parallel of ClearAlbumCoverNone. Used by the
// Parallel of ClearAlbumCoverNone. Used by the
// eligibility loop to flip stale 'none' rows back to NULL.
func (q *Queries) ClearArtistArtNone(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, clearArtistArtNone, id)
@@ -56,7 +56,7 @@ WHERE a.id = $1
RETURNING a.id
`
// M7 #372: deletes the artist row only when it has no remaining albums
// Deletes the artist row only when it has no remaining albums
// AND no remaining tracks (defensive — tracks can in principle exist
// without an album, though the scanner doesn't write that shape today).
func (q *Queries) DeleteArtistIfEmpty(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) {
@@ -253,7 +253,7 @@ type ListArtistsAlphaWithCoversRow struct {
AlbumCount int64
}
// M6a: alpha-sorted artist list with derived cover_album_id (most-recent
// Alpha-sorted artist list with derived cover_album_id (most-recent
// album whose cover_art_path is set). Replaces ListArtistsAlpha for the
// /api/artists?sort=alpha branch — the new column is appended, so the
// alpha branch is purely additive on the wire. album_count is computed
@@ -305,7 +305,7 @@ type ListArtistsMissingArtParams struct {
Limit int32
}
// M7 cover-sources: returns artist rows the artist-art enricher should
// Returns artist rows the artist-art enricher should
// attempt. Eligible: artist_art_source IS NULL OR (= 'none' AND stale
// artist_art_sources_version). MBID-less artists are returned but the
// enricher skips them at the row.mbid == nil guard inside the chain.
@@ -432,7 +432,7 @@ type SetArtistArtWithVersionParams struct {
ArtistArtSourcesVersion int32
}
// M7 cover-sources: records a successful or settled artist-art
// Records a successful or settled artist-art
// enrichment with the current sources_version stamp. thumb_path /
// fanart_path are NULL on a 'none' result, or independently NULL if
// the provider returned only one of the two image types (e.g.
@@ -460,7 +460,7 @@ type SetArtistMbidIfNullParams struct {
Mbid *string
}
// M7 #379: heal MBID on existing artist rows during rescan. Idempotent —
// Heal MBID on existing artist rows during rescan. Idempotent —
// only updates when the existing mbid is NULL, so concurrent scans don't
// overwrite an existing value with a stale one.
func (q *Queries) SetArtistMbidIfNull(ctx context.Context, arg SetArtistMbidIfNullParams) error {
+1 -1
View File
@@ -84,7 +84,7 @@ type ListAuditLogByActionsParams struct {
}
// One page of the given actions, newest first: the duplicates report's
// "resolved automatically" activity (M498) reads the resolver's merges from
// "resolved automatically" activity reads the resolver's merges from
// here.
func (q *Queries) ListAuditLogByActions(ctx context.Context, arg ListAuditLogByActionsParams) ([]AuditLog, error) {
rows, err := q.db.Query(ctx, listAuditLogByActions,
+16 -23
View File
@@ -75,7 +75,7 @@ type ListAlbumYearsWithCountRow struct {
AlbumCount int64
}
// Year browse index (#367). Only albums with a release_date appear — an
// Year browse index. Only albums with a release_date appear — an
// album with no date isn't "year unknown" as a browsable bucket, it's absent
// from this axis, and the UI says so rather than inventing a 0 row.
// Newest first: recent releases are the likelier browse target.
@@ -244,8 +244,7 @@ ORDER BY trim(g.genre)
// Distinct genres carried by an album's tracks, for the album detail page's
// quick-jump chips. Split and trimmed identically to ListGenresWithCount, so a
// chip always leads to a page that actually contains this album — the two
// diverging is exactly the bug #367 had to fix in ListAlbumsByGenre.
// chip always leads to a page that actually contains this album.
func (q *Queries) ListGenresForAlbum(ctx context.Context, albumID pgtype.UUID) ([]string, error) {
rows, err := q.db.Query(ctx, listGenresForAlbum, albumID)
if err != nil {
@@ -314,30 +313,24 @@ type ListGenresWithCountRow struct {
TrackCount int64
}
// Every query in this file filters `tracks.missing_since IS NULL` (#2523).
// A row whose file has vanished keeps its genre forever — the scanner walks the
// filesystem, so it never revisits a path that no longer exists — which is how
// pre-#2499 welded genres survived a full re-scan and kept showing in the index.
// Browsing is a way of finding something to play, so a track that cannot play
// Every query in this file filters `tracks.missing_since IS NULL`. A row whose
// file has vanished keeps its genre forever — the scanner walks the
// filesystem, so it never revisits a path that no longer exists — and
// browsing is a way of finding something to play, so a track that cannot play
// should not shape it.
//
// The year axis filters too (#2702). It used to join albums only, on the
// reasoning that an album is a real release even when some of its tracks are
// gone — true, but it left the two browse axes disagreeing: the genre queries
// above filter tracks.missing_since, so an album whose every file had vanished
// was already absent from genre while still listed under its year, where
// opening it found nothing playable.
// The year axis filters too, so the two axes agree: an album whose every file
// has vanished is absent from both, rather than absent from its genre and
// still listed under its year, where opening it would find nothing playable.
//
// Hiding is the answer rather than showing-and-marking because browsing is how
// you go looking for something to play, and the operator's rule for that case
// is to take it out of view; the admin missing-files surface is where absence
// is reported. It also means changing the axis that was inconsistent rather
// than the one that was already right.
// Hiding rather than showing-and-marking, because browsing is how you go
// looking for something to play; the admin missing-files surface is where
// absence is reported.
//
// The predicate is "has at least one playable track", so it also excludes an
// album with no tracks at all. That is the same answer for the same reason:
// nothing to play, nothing to browse to.
// Genre browse index (#367).
// Genre browse index.
//
// Genres live inline on tracks.genre as a delimited string, so this splits on
// the same [;,] pattern already used by recommendation.sql and discover.sql —
@@ -348,14 +341,14 @@ type ListGenresWithCountRow struct {
// trim() but deliberately NO lower(): trimming repairs an artifact of OUR
// splitting ("Rock; Pop" yields " Pop", and showing that as a distinct genre
// would be a bug), whereas case is what the tag actually says. Raw ID3 is
// exposed as-is for v1, so "Rock" and "rock" appear as separate rows.
// exposed as-is, so "Rock" and "rock" appear as separate rows.
//
// COUNT(DISTINCT) because a sloppy tag like "Rock;Rock" would otherwise
// inflate its own row.
//
// Ordered by count first: raw ID3 data has a long tail of one-off junk tags,
// so alphabetical would bury the handful of genres an operator actually has a
// library's worth of. Name breaks ties for a stable order.
// so alphabetical would bury the handful of genres a library actually has
// a lot of. Name breaks ties for a stable order.
// Ordered by the expression, not the output alias: `ORDER BY genre` is
// ambiguous between the alias and tracks.genre, and sqlc rejects it.
func (q *Queries) ListGenresWithCount(ctx context.Context) ([]ListGenresWithCountRow, error) {
+2 -2
View File
@@ -110,7 +110,7 @@ SELECT candidate_mbid, tag, weight
WHERE candidate_mbid = ANY($1::text[])
`
// Cached tags for a set of candidates, for slice 6's taste-overlap ranking.
// Cached tags for a set of candidates, for the taste-overlap ranking.
// One row per (candidate, tag).
func (q *Queries) ListCandidateArtistTagsForMbids(ctx context.Context, dollar_1 []string) ([]CandidateArtistTag, error) {
rows, err := q.db.Query(ctx, listCandidateArtistTagsForMbids, dollar_1)
@@ -160,7 +160,7 @@ type ListCandidateArtistsMissingTagsRow struct {
TotalScore float64
}
// Folksonomy tags for out-of-library Discover candidates (#2376). Parallel to
// Folksonomy tags for out-of-library Discover candidates. Parallel to
// track_tags.sql, but keyed by MBID because the artist has no local row. See
// 0050_candidate_artist_tags.up.sql for why the bookkeeping is its own table.
// Candidates eligible for tag enrichment: never processed (no state row) or
+1 -1
View File
@@ -352,7 +352,7 @@ type UpsertMBArtistParams struct {
Country string
}
// The release catalogue read from MusicBrainz (M502 #5519).
// The release catalogue read from MusicBrainz.
func (q *Queries) UpsertMBArtist(ctx context.Context, arg UpsertMBArtistParams) error {
_, err := q.db.Exec(ctx, upsertMBArtist,
arg.Mbid,
+1 -1
View File
@@ -14,7 +14,7 @@ const deleteArtistCoplayEdges = `-- name: DeleteArtistCoplayEdges :exec
DELETE FROM artist_similarity WHERE source = 'user_cooccurrence'
`
// Household co-play similarity (#1533, milestone #160 Opt 5). Minstrel is a
// Household co-play similarity. Minstrel is a
// single shared-library, multi-user server (no per-user library ACL), so the
// "household" is the whole instance's user set. These queries recompute
// artist–artist co-occurrence edges from play_events and store them in
+2 -2
View File
@@ -77,7 +77,7 @@ SELECT provider_id, enabled, api_key, display_order, created_at, updated_at
ORDER BY display_order, provider_id
`
// M7 cover-sources: queries for the cover_art_provider_settings table
// Queries for the cover_art_provider_settings table
// and the cover_art_sources_meta singleton. The SettingsService
// consumes these.
// Returns all rows from cover_art_provider_settings. Used by the
@@ -155,7 +155,7 @@ type UpsertProviderSettingsParams struct {
// INSERT a default row for a newly-registered provider, or update an
// existing row's display_order on boot reconciliation. Does NOT
// overwrite enabled / api_key on conflict — the operator's settings
// overwrite enabled / api_key on conflict — an admin's settings
// persist across restarts.
func (q *Queries) UpsertProviderSettings(ctx context.Context, arg UpsertProviderSettingsParams) error {
_, err := q.db.Exec(ctx, upsertProviderSettings, arg.ProviderID, arg.Enabled, arg.DisplayOrder)
+3 -3
View File
@@ -31,7 +31,7 @@ UPDATE albums
WHERE id = $1 AND cover_art_source = 'none'
`
// M7 #380: when MBID backfill heals an album, clear its 'none' cover_art_source
// When MBID backfill heals an album, clear its 'none' cover_art_source
// back to NULL so the enricher's next batch retries the MBCAA fetch.
// Idempotent — only updates rows currently stamped 'none'.
func (q *Queries) ClearAlbumCoverNone(ctx context.Context, id pgtype.UUID) error {
@@ -128,8 +128,8 @@ type ListAlbumsMissingCoverParams struct {
Limit int32
}
// M7 #353: album cover enrichment queries.
// M7 cover-sources: returns rows the enricher should attempt. Eligible:
// Album cover enrichment queries.
// Returns rows the enricher should attempt. Eligible:
// cover_art_source IS NULL (never tried), OR cover_art_source = 'none'
// AND cover_art_sources_version != current (settled under an older
// source set; recheck against the current chain). LIMIT supplied by
+14 -14
View File
@@ -15,7 +15,7 @@ const listCrossUserLikedTracksForDiscover = `-- name: ListCrossUserLikedTracksFo
SELECT t.id, t.album_id, t.artist_id
FROM general_likes gl
JOIN tracks t ON t.id = gl.track_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND gl.user_id != $1
AND NOT EXISTS (
SELECT 1 FROM play_events pe
@@ -55,8 +55,8 @@ type ListCrossUserLikedTracksForDiscoverRow struct {
//
// GROUP BY (not SELECT DISTINCT) so the md5 ORDER BY expression need
// not appear in the select list — Postgres rejects DISTINCT + ORDER BY
// by-expression at plan time (SQLSTATE 42P10), which previously caused
// the entire Discover build to fail silently.
// by-expression at plan time (SQLSTATE 42P10), which would fail the
// entire Discover build.
func (q *Queries) ListCrossUserLikedTracksForDiscover(ctx context.Context, arg ListCrossUserLikedTracksForDiscoverParams) ([]ListCrossUserLikedTracksForDiscoverRow, error) {
rows, err := q.db.Query(ctx, listCrossUserLikedTracksForDiscover, arg.UserID, arg.Column2)
if err != nil {
@@ -96,7 +96,7 @@ dormant_artists AS (
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
JOIN dormant_artists da ON da.id = t.artist_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM play_events pe
WHERE pe.user_id = $1
@@ -161,7 +161,7 @@ func (q *Queries) ListDormantArtistTracksForDiscover(ctx context.Context, arg Li
const listRandomUnheardTracksForDiscover = `-- name: ListRandomUnheardTracksForDiscover :many
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM play_events pe
WHERE pe.user_id = $1
@@ -223,7 +223,7 @@ WITH scored AS (
FROM tracks t
JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true
JOIN taste_profile_tags nt ON nt.user_id = $3 AND trim(g_split.g) = nt.tag
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND nt.weight > 0
AND trim(g_split.g) <> ''
AND NOT EXISTS (
@@ -274,7 +274,7 @@ type ListTasteUnheardTracksForDiscoverRow struct {
}
// Taste-targeted novelty: unheard tracks whose genres overlap the user's
// taste-profile tags (taste_profile_tags, #796), ranked by summed tag
// taste-profile tags (taste_profile_tags), ranked by summed tag
// weight — "new to you, but your vibe" rather than the crude random arm.
// Genres live inline on tracks.genre as a delimited string, split the
// same way the radio tag_overlap arm does (regexp_split_to_table on
@@ -282,13 +282,13 @@ type ListTasteUnheardTracksForDiscoverRow struct {
// when the user has no taste tags yet (cold start), so the caller
// redistributes its slots to the other buckets. Stamped 'taste_unheard'.
//
// The per-album and per-artist caps apply BEFORE the LIMIT (#5356). Summed
// weight rewards a track for carrying many of the user's tags, so a few
// artists whose every track is tagged with the whole profile take every row
// of a plain LIMIT; the caller's caps then left 6 of 120 on the deploy, and
// the best-performing arm handed its slots to the others. Ranking within
// album, then within artist over what the album cap kept, is the same walk
// capByAlbumAndArtist makes, so the caller's caps keep everything here.
// The per-album and per-artist caps apply BEFORE the LIMIT. Summed weight
// rewards a track for carrying many of the user's tags, so a few artists
// whose every track is tagged with the whole profile would take every row of
// a plain LIMIT, the caller's caps would then leave a handful, and the arm
// would hand its slots to the others. Ranking within album, then within
// artist over what the album cap kept, is the same walk capByAlbumAndArtist
// makes, so the caller's caps keep everything here.
func (q *Queries) ListTasteUnheardTracksForDiscover(ctx context.Context, arg ListTasteUnheardTracksForDiscoverParams) ([]ListTasteUnheardTracksForDiscoverRow, error) {
rows, err := q.db.Query(ctx, listTasteUnheardTracksForDiscover,
arg.MaxPerArtist,
+10 -10
View File
@@ -55,9 +55,9 @@ SELECT count(*)::bigint
END
`
// CountPendingDuplicateGroups narrowed to one view of the report (M498):
// CountPendingDuplicateGroups narrowed to one view of the report:
//
// review what the operator still has to decide: every class but
// review what an admin still has to decide: every class but
// cross_release, and groups not yet classified
// cross_release the same song on different releases, which is kept, not merged
func (q *Queries) CountPendingDuplicateGroupsInView(ctx context.Context, view string) (int64, error) {
@@ -205,7 +205,7 @@ type ListDismissedDuplicateMemberSetsRow struct {
TrackIds []pgtype.UUID
}
// What the operator has already said are not duplicates. A new proposal whose
// What an admin has already said are not duplicates. A new proposal whose
// every member sat together in one of these is not proposed again.
func (q *Queries) ListDismissedDuplicateMemberSets(ctx context.Context) ([]ListDismissedDuplicateMemberSetsRow, error) {
rows, err := q.db.Query(ctx, listDismissedDuplicateMemberSets)
@@ -236,7 +236,7 @@ SELECT t.id, t.duration_ms, f.audio_stream_sha256, f.chromaprint
AND f.chromaprint IS NOT NULL
-- Only chromaprints taken at the current length: prints at two lengths are not
-- comparable, and after a length change the backfill is still re-deriving the
-- rest (#3913).
-- rest.
AND f.chromaprint_length_sec = $2
AND (t.duration_ms, t.id) > ($3::integer, $4::uuid)
ORDER BY t.duration_ms, t.id
@@ -356,19 +356,19 @@ type ListDuplicateGroupsForResolveRow struct {
AcoustidRecording *string
}
// Every pending group's present copies, for the resolver (M498 #5435): what it
// Every pending group's present copies, for the resolver: what it
// needs to classify the group and to choose the copy to keep. One row per copy,
// grouped by group. position_clash is true when another present track on the
// same album claims the same disc and track number: tags that do not fit the
// album, which the survivor rule weighs against a copy. source_verdict says
// whether the copy is a video rip ('suspect') or one the operator kept ('fine').
// whether the copy is a video rip ('suspect') or one an admin kept ('fine').
// prior_class is the class the last pass gave the group, NULL for a group no
// pass has seen. release_group_mbid is the album's MusicBrainz group, from its
// tags or the library listing's match; ” when neither names one.
// acoustid_state and acoustid_recording are what AcoustID said of the file,
// when it was looked up (#6169: which song one recording cut to several slots
// is). group_id narrows it to one group, for the operator settling it (#6184);
// NULL reads them all.
// when it was looked up: which song a recording cut to several slots is.
// group_id narrows it to one group, for an admin settling it; NULL reads them
// all.
func (q *Queries) ListDuplicateGroupsForResolve(ctx context.Context, groupID pgtype.UUID) ([]ListDuplicateGroupsForResolveRow, error) {
rows, err := q.db.Query(ctx, listDuplicateGroupsForResolve, groupID)
if err != nil {
@@ -533,7 +533,7 @@ type ListPendingDuplicateGroupMembersRow struct {
// One page of proposals in one view (see CountPendingDuplicateGroupsInView),
// newest first, flattened to one row per member so the handler folds them
// without a query per group. What each copy carries — likes and plays from
// every user — is here because it is what the operator weighs when deciding
// every user — is here because it is what an admin weighs when deciding
// which copy to keep.
func (q *Queries) ListPendingDuplicateGroupMembers(ctx context.Context, arg ListPendingDuplicateGroupMembersParams) ([]ListPendingDuplicateGroupMembersRow, error) {
rows, err := q.db.Query(ctx, listPendingDuplicateGroupMembers, arg.View, arg.PageOffset, arg.PageLimit)
+2 -2
View File
@@ -131,7 +131,7 @@ type GetSystemPickKindForTrackParams struct {
// Looks a track up in the user's CURRENT snapshot of the given system
// variant and returns its pick_kind. Used at play-ingestion time to
// freeze provenance onto the play_event — snapshots rebuild daily, so
// attribution can't be reconstructed at read time (#1249/#1270). No
// attribution can't be reconstructed at read time. No
// row = track not in today's snapshot (caller stores NULL); a row with
// NULL pick_kind = the variant doesn't stamp (yet). songs_like_artist
// is non-singleton (up to 3 mixes/user); a track in two of them takes
@@ -166,7 +166,7 @@ type InsertPlayEventParams struct {
// pick_kind is non-NULL only for system-playlist plays whose track was
// found (with a stamped kind) in the user's live snapshot for that
// variant at ingestion time (#1249, generalized in #1270).
// variant at ingestion time.
func (q *Queries) InsertPlayEvent(ctx context.Context, arg InsertPlayEventParams) (PlayEvent, error) {
row := q.db.QueryRow(ctx, insertPlayEvent,
arg.UserID,
+3 -3
View File
@@ -81,7 +81,7 @@ SELECT t.id, t.file_path
LEFT JOIN track_fingerprints f ON f.track_id = t.id
WHERE t.missing_since IS NULL
-- A row taken at another length is as stale as one from an older method:
-- chromaprints at two lengths cannot be compared (#3913).
-- chromaprints at two lengths cannot be compared.
AND (f.track_id IS NULL
OR f.fingerprint_version < $1
OR f.chromaprint_length_sec <> $2)
@@ -102,7 +102,7 @@ type ListTracksNeedingFingerprintRow struct {
FilePath string
}
// The backfill's work queue (#3908): tracks with no fingerprint, or one derived
// The backfill's work queue: tracks with no fingerprint, or one derived
// by an older method. Keyset-paged on id so a pass visits each track at most
// once. That cursor is load-bearing: an inconclusive attempt writes no row, so
// without it a file that keeps timing out would be listed again straight away
@@ -157,7 +157,7 @@ type UpsertTrackFingerprintParams struct {
}
// Written whenever a track's fingerprint is derived: by the scan when a file is
// new or its bytes changed, and by the backfill (#3908) for rows derived by an
// new or its bytes changed, and by the backfill for rows derived by an
// older method. Replaces the row wholesale — a fingerprint of the old bytes has
// no standing once the file has changed.
func (q *Queries) UpsertTrackFingerprint(ctx context.Context, arg UpsertTrackFingerprintParams) error {
+9 -11
View File
@@ -20,10 +20,9 @@ UPDATE play_sessions
)
`
// #565: play_sessions.ended_at was added but never populated by any
// writer. Close sessions whose last_event_at is older than 6h —
// treating that as "user moved on" the same way audio_service does
// after grace periods. Sessions with NO events (track_count = 0)
// play_sessions.ended_at has no other writer. Close sessions whose
// last_event_at is older than 6h — treating that as "user moved on" the
// same way audio_service does after grace periods. Sessions with NO events (track_count = 0)
// older than 1h are also closed (stale handshakes from clients that
// never recorded a play). ended_at is set to last_event_at so the
// session's duration reads naturally.
@@ -46,12 +45,11 @@ UPDATE play_events
AND started_at < now() - INTERVAL '24 hours'
`
// Background garbage-collector / lifecycle queries. All five address
// drift findings from the 2026-06-02 audit (Scribe parent #552):
// #565 #566 #567 #574 #575. Sweeper runs on a long tick (default 1h)
// Background garbage-collector / lifecycle queries: rows nothing else closes
// or removes. Sweeper runs on a long tick (default 1h)
// so per-query cost is amortised; each is idempotent (re-running on
// already-closed/-deleted rows is a no-op).
// #566: play_events rows opened more than 24h ago that never got a
// Play_events rows opened more than 24h ago that never got a
// play_ended. Client crashed mid-track, network dropped, etc. We
// synthesize ended_at = started_at + duration_played_ms when present,
// otherwise leave duration_played_ms null and stamp ended_at = now()
@@ -72,7 +70,7 @@ DELETE FROM password_resets
OR (used_at IS NULL AND expires_at < now() - INTERVAL '1 hour')
`
// #575: password_resets accumulates expired + used rows forever. The
// Password_resets accumulates expired + used rows forever. The
// validation path already rejects them; this just keeps the table
// from growing unbounded. Used rows are kept for 7 days for audit;
// unused expired rows go immediately.
@@ -106,7 +104,7 @@ DELETE FROM scrobble_queue
AND enqueued_at < now() - INTERVAL '14 days'
`
// #567: scrobble_queue rows that have been in status='failed' for
// Scrobble_queue rows that have been in status='failed' for
// more than 14 days. The worker stops retrying after maxAttempts;
// failed rows accumulate forever otherwise. CASCADE from play_events
// already drops the row when the underlying event is deleted, so this
@@ -127,7 +125,7 @@ UPDATE system_playlist_runs
AND last_run_at < now() - INTERVAL '10 minutes'
`
// #574: system_playlist_runs.in_flight = true can wedge on a
// System_playlist_runs.in_flight = true can wedge on a
// goroutine panic between SET in_flight=true and SET in_flight=false.
// The duplicate-prevention check refuses to start a fresh regen while
// in_flight, so a stuck row blocks all future regens for that user.
+1 -1
View File
@@ -45,7 +45,7 @@ type ListUserHistoryRow struct {
ArtistName string
}
// M7 #365: track-level listening history. Returns one row per
// Track-level listening history. Returns one row per
// play_events entry, newest first, joined with tracks/albums/artists
// so the SPA can render a TrackRef without follow-up fetches. Filters
// out skipped events and tracks the user has quarantined (per-user
+5 -5
View File
@@ -297,7 +297,7 @@ type ListGrabsRow struct {
// The Wanted view's Activity tab: downloads waiting on a person first, then
// the rest still running, then the ended ones newest first. Each carries
// what is wrong with it now, if anything (#5523).
// what is wrong with it now, if anything.
func (q *Queries) ListGrabs(ctx context.Context, arg ListGrabsParams) ([]ListGrabsRow, error) {
rows, err := q.db.Query(ctx, listGrabs, arg.Skip, arg.MaxRows)
if err != nil {
@@ -366,7 +366,7 @@ type ListImportedGrabsHoldingRow struct {
// The downloads Minstrel imported into the folder holding path: the releases a
// file there came from, to block when the files proved not to be the album
// (#6169). A file imported before Minstrel downloaded anything has none.
// . A file imported before Minstrel downloaded anything has none.
func (q *Queries) ListImportedGrabsHolding(ctx context.Context, path string) ([]ListImportedGrabsHoldingRow, error) {
rows, err := q.db.Query(ctx, listImportedGrabsHolding, path)
if err != nil {
@@ -441,7 +441,7 @@ type ListLiveGrabsRow struct {
WantAlbumID pgtype.UUID
}
// Import with verification (M502 #5522).
// Import with verification.
// Every download still running or waiting on its import, oldest first.
func (q *Queries) ListLiveGrabs(ctx context.Context) ([]ListLiveGrabsRow, error) {
rows, err := q.db.Query(ctx, listLiveGrabs)
@@ -574,7 +574,7 @@ type ListUpgradeTracksRow struct {
SourceVerdict *string
}
// The tracks an upgrade may replace (#5524): the present tracks of the album
// The tracks an upgrade may replace: the present tracks of the album
// the want holds.
func (q *Queries) ListUpgradeTracks(ctx context.Context, albumID pgtype.UUID) ([]ListUpgradeTracksRow, error) {
rows, err := q.db.Query(ctx, listUpgradeTracks, albumID)
@@ -694,7 +694,7 @@ UPDATE grabs
import_forced = false,
imported_at = CASE WHEN $1::text = 'imported' THEN now() ELSE imported_at END,
-- The files were copied, so an imported download goes on sharing
-- until the lifecycle removes it (#5523).
-- until the lifecycle removes it.
seeding = $1::text = 'imported',
imported_files = $4,
destination = $5,
+4 -5
View File
@@ -116,11 +116,10 @@ type InsertLibraryChangesParams struct {
Op string
}
// Batch form, for a mutation that touches many rows at once (#2704: the scan's
// reconcile pass can mark up to a quarter of a library missing in one go).
// Every caller before this one changed a single entity, so per-row inserts
// were the right shape; a loop here would be thousands of round-trips inside
// a scan that is already the slow path.
// Batch form, for a mutation that touches many rows at once (the scan's
// reconcile pass can mark up to a quarter of a library missing in one go). A
// loop of single inserts would be thousands of round-trips inside a scan that
// is already the slow path.
func (q *Queries) InsertLibraryChanges(ctx context.Context, arg InsertLibraryChangesParams) error {
_, err := q.db.Exec(ctx, insertLibraryChanges, arg.EntityType, arg.EntityIds, arg.Op)
return err
+1 -1
View File
@@ -101,7 +101,7 @@ type RerootTrackPathsParams struct {
OldRoot string
}
// A library folder that moved (#6129): every path under old_root is rewritten
// A library folder that moved: every path under old_root is rewritten
// to the same place under new_root, so the tracks keep their ids and with them
// their plays, likes and playlists. updated_at is left alone on purpose: the
// scan compares it with each file's mtime, and the files have not changed.
+2 -2
View File
@@ -22,7 +22,7 @@ type CountGoneGrabsParams struct {
}
// How many of a release's grabs ended gone since a time: a release that keeps
// leaving its client is held back rather than taken on every pass (#6176).
// leaving its client is held back rather than taken on every pass.
func (q *Queries) CountGoneGrabs(ctx context.Context, arg CountGoneGrabsParams) (int64, error) {
row := q.db.QueryRow(ctx, countGoneGrabs, arg.InfoHash, arg.Since)
var count int64
@@ -289,7 +289,7 @@ type RaiseInterventionParams struct {
Detail string
}
// The download lifecycle (M502 #5523): the intervention ledger, giving a
// The download lifecycle: the intervention ledger, giving a
// download up, and seeding after import.
// A condition seen, or seen again: a second pass lands on the open row, so
// a download stuck for a week is one row that says how long. The name only
+1 -1
View File
@@ -93,7 +93,7 @@ type LikeTrackParams struct {
TrackID pgtype.UUID
}
// A like is on the song (M498 #5438): every copy of it sharing the track's
// A like is on the song: every copy of it sharing the track's
// song_key is liked. Returns the tracks newly liked, for the sync log; empty
// when the song was already liked.
func (q *Queries) LikeTrack(ctx context.Context, arg LikeTrackParams) ([]pgtype.UUID, error) {
+3 -3
View File
@@ -164,7 +164,7 @@ type GetReplayGainByTrackIDsRow struct {
}
// Track and album loudness for a set of tracks, for every surface that hands
// gains to a client (#4997). A measurement from an older analysis version is
// gains to a client. A measurement from an older analysis version is
// still delivered: it is a better gain than none until the backfill redoes it.
func (q *Queries) GetReplayGainByTrackIDs(ctx context.Context, ids []pgtype.UUID) ([]GetReplayGainByTrackIDsRow, error) {
rows, err := q.db.Query(ctx, getReplayGainByTrackIDs, ids)
@@ -280,7 +280,7 @@ type ListAlbumsNeedingLoudnessRow struct {
Digest string
}
// The album pass's work queue (#4996): albums whose present tracks or their
// The album pass's work queue: albums whose present tracks or their
// measurements have changed since album loudness was last computed, or that
// never had it. The digest is over every present track's id and the
// measurement it holds at the current version ('-' for none), so a track
@@ -484,7 +484,7 @@ type UpsertTrackLoudnessParams struct {
AnalysisVersion int16
}
// Written when the backfill measures a track (#4995). Replaces the row
// Written when the backfill measures a track. Replaces the row
// wholesale: a measurement of the old bytes has no standing once the file has
// changed.
func (q *Queries) UpsertTrackLoudness(ctx context.Context, arg UpsertTrackLoudnessParams) error {
+1 -1
View File
@@ -69,7 +69,7 @@ type LockDuplicateGroupForMergeRow struct {
Status string
}
// Duplicate merge (Scribe #3911). Every statement here runs inside the one
// Duplicate merge. Every statement here runs inside the one
// transaction library.MergeDuplicateGroup opens, after the removed copy's file
// is already gone. The loser's own track row is deleted last with DeleteTrack;
// what these do is move everything it carries onto the survivor first, so that
+8 -8
View File
@@ -63,27 +63,27 @@ type ListMissingTrackPairsRow struct {
PresentCandidates int32
}
// Missing-pair pass (M485). A track marked missing whose replacement is already
// Missing-pair pass. A track marked missing whose replacement is already
// on disk under another row — most often a quality upgrade, mp3 → flac at a new
// path — is folded into that replacement, so its history moves across and
// re-acquisition stops chasing music the library already has.
//
// Move adoption (#2528) cannot catch these: it links a new file to a missing row
// Move adoption cannot catch these: it links a new file to a missing row
// at the moment the file is first inserted, and the replacement arrives as a
// re-encode with no recording MBID, so it has nothing to match on. AcoustID may
// give it an MBID later; by then both rows exist and only a fold can join them.
// Every candidate (missing, present) pair, with how many candidates each side
// has. Two rules, measured on the operator's library on 2026-10-07 (#5274):
// has. Two rules:
//
// mbid the same recording MBID, within the album group (the album row, or
// albums sharing a release group). 142 of 359 pairs.
// title the same album row and the same title, ignoring case. 213 more. The
// albums sharing a release group).
// title the same album row and the same title, ignoring case. The
// replacements are often not the original file — another edition,
// re-encoded, untagged — so the metadata is what is left to go on.
//
// Deliberately NOT gated on duration or track position. On the 142 pairs known
// to be one recording, 18% differed by over 2s (old mp3 durations are poor
// estimates), and the poorly tagged set is exactly where numbering is broken.
// Deliberately NOT gated on duration or track position. Pairs known to be one
// recording often differ by over 2s (old mp3 durations are poor estimates),
// and the poorly tagged set is exactly where numbering is broken.
//
// A pair whose MBIDs are both set and differ is not a title match: the tags say
// they are different recordings. The caller folds a pair only when each side has
+7 -7
View File
@@ -54,7 +54,7 @@ type InsertNotificationParams struct {
EmailWanted bool
}
// M489 notifications inbox (#726). Rows are written only through
// Notifications inbox. Rows are written only through
// internal/notifications.Notifier, which decides recipients and honours each
// recipient's inbox preference before it reaches these.
// email_wanted is the recipient's email channel for this kind as of now. A
@@ -122,7 +122,7 @@ type ListEmailPendingNotificationsRow struct {
ReadAsOf pgtype.Timestamptz
}
// Email digest (#5346) ------------------------------------------------------
// Email digest ------------------------------------------------------
// Every unread, un-emailed row of a user who has an address, oldest first.
// Read rows are never selected: the user has seen them, so they are not news.
// read_as_of is the database's clock at the read, handed back to
@@ -348,7 +348,7 @@ type MarkAllNotificationsReadParams struct {
// all read" queued offline and replayed later must not mark notices that
// arrived in between, which the user never saw. A coalesced row that grew
// since then is marked read with it: it arrived before they asked, and its
// growth is a quiet update to a notice they had seen (M542).
// growth is a quiet update to a notice they had seen.
func (q *Queries) MarkAllNotificationsRead(ctx context.Context, arg MarkAllNotificationsReadParams) (int64, error) {
result, err := q.db.Exec(ctx, markAllNotificationsRead, arg.UserID, arg.UpTo)
if err != nil {
@@ -484,10 +484,10 @@ type UpsertCoalescedNotificationParams struct {
// ten. Every coalesced kind's event is "N more happened", never a restated
// total, so the row always says what is new since it was last read.
//
// created_at and emailed_at are left alone (M542): an update changes what the
// notice says, not when it arrived. Moving created_at made the phone announce
// it again and clearing emailed_at sent it again, so a notice the hourly
// duplicate pass kept growing alerted every hour.
// created_at and emailed_at are left alone: an update changes what the
// notice says, not when it arrived. Moving created_at would make the phone
// announce it again and clearing emailed_at would send it again, so a notice
// that keeps growing would alert on every update.
func (q *Queries) UpsertCoalescedNotification(ctx context.Context, arg UpsertCoalescedNotificationParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, upsertCoalescedNotification,
arg.UserID,
+3 -3
View File
@@ -38,7 +38,7 @@ type AppendPlaylistTrackParams struct {
// Inserts at the next available position. Snapshot fields are copied
// from the tracks/albums/artists join at insert time. tracks.duration_ms
// is converted to seconds for the snapshot. pick_kind is NULL for manual
// playlists; the For You builder stamps 'taste'/'fresh' (#1249).
// playlists; the For You builder stamps 'taste'/'fresh'.
func (q *Queries) AppendPlaylistTrack(ctx context.Context, arg AppendPlaylistTrackParams) (PlaylistTrack, error) {
row := q.db.QueryRow(ctx, appendPlaylistTrack, arg.PlaylistID, arg.PickKind, arg.TrackID)
var i PlaylistTrack
@@ -182,7 +182,7 @@ SELECT id, user_id, name, description, is_public, cover_path, track_count, durat
`
// Batched lookup used by /api/library/sync to hydrate upsert payloads
// (#357). Mirror of GetArtistsByIDs.
// . Mirror of GetArtistsByIDs.
func (q *Queries) GetPlaylistsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Playlist, error) {
rows, err := q.db.Query(ctx, getPlaylistsByIDs, dollar_1)
if err != nil {
@@ -295,7 +295,7 @@ type ListPlaylistTracksRow struct {
// (track was removed from the library). The denormalized snapshot fields
// on playlist_tracks remain authoritative for title/artist/album text.
//
// Deliberately NOT filtered on missing_since (#2527), unlike every browse /
// Deliberately NOT filtered on missing_since, unlike every browse /
// discover / mix query. A playlist entry is something the user put here on
// purpose, so a missing file stays in the list and renders as a dead row
// rather than silently vanishing; missing_since rides along so the service
+8 -8
View File
@@ -35,9 +35,9 @@ const getReacquisitionSettings = `-- name: GetReacquisitionSettings :one
SELECT id, enabled, grace_hours, max_per_pass FROM reacquisition_settings WHERE id = true
`
// Re-acquisition of missing files (milestone #290, reworked for M502 #5520).
// Re-acquisition of missing files.
// The unit is the album: an album whose files went missing becomes a want for
// its release group, and the search schedule (#5521) looks for it.
// its release group, and the search schedule looks for it.
func (q *Queries) GetReacquisitionSettings(ctx context.Context) (ReacquisitionSetting, error) {
row := q.db.QueryRow(ctx, getReacquisitionSettings)
var i ReacquisitionSetting
@@ -140,17 +140,17 @@ type ListAlbumsDueReacquisitionRow struct {
//
// - it still has at least one track whose file has been missing longer than
// the grace window. Measured on missing_since, which reconcile never
// re-stamps (#2523), so it is a genuine "gone since" clock;
// re-stamps, so it is a genuine "gone since" clock;
// - MusicBrainz can name it: the artist has an MBID, and the album a
// release MBID or a release group. The release MBID is only ever used to
// find the group, and 238 albums matched to a group carried no release
// id, so their losses were never wanted back (#6167). Albums failing this
// are counted separately (CountAlbumsMissingWithoutMbid);
// - it is on the wanted list (#5567): an album taken off the list, or
// find the group, and an album matched to a group may carry no release
// id. Albums failing this are counted separately
// (CountAlbumsMissingWithoutMbid);
// - it is on the wanted list: an album taken off the list, or
// dropped into the library by hand, is not fetched back;
// - no want already covers it: none open, and none held since its files
// went missing. A want held after the loss means the library has a full
// copy despite the missing rows (#5245), so asking again would loop.
// copy despite the missing rows, so asking again would loop.
func (q *Queries) ListAlbumsDueReacquisition(ctx context.Context, arg ListAlbumsDueReacquisitionParams) ([]ListAlbumsDueReacquisitionRow, error) {
rows, err := q.db.Query(ctx, listAlbumsDueReacquisition, arg.GraceHours, arg.PageLimit)
if err != nil {
+42 -49
View File
@@ -19,7 +19,7 @@ ORDER BY started_at DESC
LIMIT 1
`
// The device_class of the user's most recent play (#1551), used as the
// The device_class of the user's most recent play, used as the
// "current device" for radio context conditioning. NULL when the latest play
// predates device capture or came from a client that didn't send one.
func (q *Queries) GetLatestPlayDeviceClassForUser(ctx context.Context, userID pgtype.UUID) (*string, error) {
@@ -87,10 +87,10 @@ type ListArtistContextPlayCountsForUserRow struct {
}
// Per-artist completed-play counts split by whether each play falls in the
// CURRENT context cell, in the user's local timezone (#1531). The cell is
// daypart × weekday-type, optionally narrowed by device class (#1551): when
// $2 (the current device) is ”, the device dimension is ignored (identical to
// the time-only #1531 behaviour, used by the daily mixes which have no device);
// CURRENT context cell, in the user's local timezone. The cell is
// daypart × weekday-type, optionally narrowed by device class: when
// $2 (the current device) is ”, the device dimension is ignored (time only,
// for the daily mixes, which have no device);
// when $2 is set (radio), a play only counts in the cell if its device_class
// matches. Feeds the context-affinity scoring term. Skips excluded; a 365-day
// window bounds cost. Daypart buckets: night [22,5) morning [5,12)
@@ -154,7 +154,7 @@ type ListLastPlayedArtistsForUserRow struct {
LastPlayedAt pgtype.Timestamptz
}
// M6a: artists ranked by max(play_events.started_at) for the user, with
// Artists ranked by max(play_events.started_at) for the user, with
// a derived cover_album_id via a representative-album lateral join (most
// recent album that has cover_art_path set). album_count joined for the
// ArtistRef wire shape.
@@ -216,7 +216,7 @@ JOIN tracks t ON t.id = p.track_id
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = $1
AND t.missing_since IS NULL -- #2523: never offer a file that is gone
AND t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $2 AND q.track_id = t.id
@@ -303,7 +303,7 @@ FROM plays p
JOIN tracks t ON t.id = p.track_id
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
@@ -323,7 +323,7 @@ type ListMostPlayedTracksForUserRow struct {
ArtistName string
}
// M6a: top-N tracks by completed-play count for the user. was_skipped
// Top-N tracks by completed-play count for the user. was_skipped
// excludes skips so a user spamming next doesn't fabricate a top track.
// Quarantined tracks (per-user soft-hide from M5b) are filtered out.
//
@@ -723,7 +723,7 @@ LEFT JOIN LATERAL (
WHERE user_id = $1 AND track_id = t.id
) pe ON true
WHERE t.id <> $2
AND t.missing_since IS NULL -- #2523: never offer a file that is gone
AND t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM play_events
WHERE user_id = $1 AND track_id = t.id
@@ -843,11 +843,10 @@ lb_similar AS (
LIMIT $5
),
similar_artists AS (
-- Round-robin across the similar artists (#5297): each artist's first
-- Round-robin across the similar artists: each artist's first
-- track, best-scoring artist first, then each one's second, and so on.
-- Ordering by artist score alone let the closest similar artist's
-- catalogue fill the whole LIMIT; on the operator's library this arm held
-- exactly one artist for all 17 seeds measured (#3879).
-- Ordering by artist score alone would let the closest similar artist's
-- catalogue fill the whole LIMIT.
SELECT track_id, sim_score
FROM (
SELECT t.id AS track_id, asim.score * 0.5 AS sim_score, asim.score AS artist_score,
@@ -873,11 +872,10 @@ tag_overlap AS (
AND t.id <> $2
GROUP BY t.id
HAVING count(DISTINCT trim(g_split.g)) > 0
-- md5 breaks the ties (#3880). A seed with one tag gives every track of
-- that genre the same score, 7,804 tracks on the operator's library, and
-- with no tie-break the LIMIT took whichever rows Postgres reached first:
-- the same slice for every seed of the genre, and not one the order seed
-- controls.
-- md5 breaks the ties. A seed with one tag gives every track of that
-- genre the same score, and with no tie-break the LIMIT would take
-- whichever rows Postgres reached first: the same slice for every seed of
-- the genre, and not one the order seed controls.
ORDER BY sim_score DESC, md5(t.id::text || $12::text)
LIMIT $7
),
@@ -896,12 +894,10 @@ likes_overlap AS (
LIMIT $8
),
taste_overlap AS (
-- Round-robin across the taste artists (#3880), as similar_artists is
-- (#5297): each artist's first track, strongest artist first, then each
-- one's second. Ordered by weight then track id, the strongest artist's
-- catalogue filled the whole arm with the same lowest-id tracks every
-- time; on the operator's library that was 20 tracks of one artist of
-- 174, whose catalogue is 197 tracks.
-- Round-robin across the taste artists, as similar_artists is: each
-- artist's first track, strongest artist first, then each one's second.
-- Ordered by weight then track id, the strongest artist's catalogue would
-- fill the whole arm with the same lowest-id tracks every time.
SELECT track_id, sim_score
FROM (
SELECT t.id AS track_id, 0.0::float8 AS sim_score, tpa.weight,
@@ -918,7 +914,7 @@ taste_overlap AS (
LIMIT $10
),
coplay_artists AS (
-- Household co-play (#1533): tracks by artists co-played across the
-- Household co-play: tracks by artists co-played across the
-- instance with the seed's artist (source='user_cooccurrence', built by
-- the coplay worker). Mirrors similar_artists but from local co-occurrence
-- instead of ListenBrainz; empty on single-user servers. Same 0.5 damp as
@@ -972,7 +968,7 @@ FROM (
UNION ALL SELECT track_id, sim_score FROM coplay_artists
UNION ALL SELECT track_id, sim_score FROM random_fill
) u
JOIN tracks t ON t.id = u.track_id AND t.missing_since IS NULL -- #2523: never offer a file that is gone
JOIN tracks t ON t.id = u.track_id AND t.missing_since IS NULL -- never offer a file that is gone
JOIN albums al ON al.id = t.album_id
LEFT JOIN general_likes l ON l.user_id = $1 AND l.track_id = t.id
LEFT JOIN LATERAL (
@@ -1014,26 +1010,23 @@ type LoadRadioCandidatesV2Row struct {
SimilarityScore interface{}
}
// M4c: similarity-driven candidate pool. 6-way UNION:
// Similarity-driven candidate pool. 6-way UNION:
//
// $1 user_id, $2 seed_track_id, $3 recently_played_hours,
// $4 exclude (uuid[]), $5 lb_similar K, $6 similar_artists K,
// $7 tag_overlap K, $8 likes_overlap K, $9 random_fill K,
// $10 taste_overlap K (#796 phase 2b — tracks by the user's top
// $10 taste_overlap K (tracks by the user's top
// positively-weighted taste-profile artists, so taste-relevant tracks
// enter the pool even when the similarity/random arms miss them; scored
// in Go via TasteMatch, so sim_score here is 0 pool-inclusion),
// $11 coplay_artists K (#1533 — tracks by artists co-played across the
// $11 coplay_artists K (tracks by artists co-played across the
// instance with the seed's artist; source='user_cooccurrence'),
// $12 order_seed (text) — see below.
//
// $12 REPLACES `ORDER BY random()` IN FOUR ARMS (#3889). Those arms returned
// a stable set only while their LIMIT exceeded the rows eligible for them: at
// that point they returned all of them and the order stopped mattering,
// because the caller sorts by track id before scoring. Below that threshold
// they returned a random SUBSET, and two builds on the same day drew
// different ones — so "daily determinism" held by accident, and only for
// libraries smaller than the limits.
// $12 orders four arms by md5(id || seed), not random(). An arm whose LIMIT is
// below the rows eligible for it returns a SUBSET, and with random() two
// builds on the same day would draw different ones; the caller sorts by track
// id before scoring, so only the subset matters.
//
// md5(id || seed) keeps the intent — an arbitrary spread that changes when
// the seed does — while making it reproducible for a given seed. The CALLER
@@ -1042,7 +1035,7 @@ type LoadRadioCandidatesV2Row struct {
// keeps varying, which is what a radio should do.
// Returns same shape as LoadRadioCandidates plus similarity_score column.
//
// sim_score BANDS keep the tiers in order (rule 131, #6177). Similarity edges
// sim_score BANDS keep the eligibility tiers in order. Similarity edges
// are stored relative to the seed's best match, 0 to 1, so the arms are placed
// in bands rather than compared raw:
//
@@ -1052,9 +1045,9 @@ type LoadRadioCandidatesV2Row struct {
// likes_overlap 0.3, flat
// tier 3 taste_overlap, random_fill 0
//
// Without the bands, the seed's weaker LB edges fell below a shared genre tag
// and the flat likes score, and the one arm that measures the seed lost to
// the ones that guess.
// Without the bands, the seed's weaker LB edges would fall below a shared
// genre tag and the flat likes score, and the one arm that measures the seed
// would lose to the ones that guess.
func (q *Queries) LoadRadioCandidatesV2(ctx context.Context, arg LoadRadioCandidatesV2Params) ([]LoadRadioCandidatesV2Row, error) {
rows, err := q.db.Query(ctx, loadRadioCandidatesV2,
arg.UserID,
@@ -1122,9 +1115,9 @@ func (q *Queries) LoadRadioCandidatesV2(ctx context.Context, arg LoadRadioCandid
const suggestArtistsForUser = `-- name: SuggestArtistsForUser :many
WITH artist_plays AS (
-- Completed plays only. The previous seed query counted every play_event,
-- so skipping an artist repeatedly INCREASED its signal and pushed more of
-- its neighbours at the user (issue #2367 mechanism 3).
-- Completed plays only: counting every play_event would make skipping an
-- artist repeatedly INCREASE its signal and push more of its neighbours
-- at the user.
SELECT t.artist_id, count(*)::bigint AS play_count
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
@@ -1181,7 +1174,7 @@ contributions AS (
AND r.artist_mbid = u.candidate_mbid
AND r.status NOT IN ('rejected', 'failed')
)
-- Snoozed by this user and not yet expired (#2374). Time-boxed and
-- Snoozed by this user and not yet expired. Time-boxed and
-- per-user: the candidate returns on its own once snoozed_until
-- passes, and stays visible to everyone else meanwhile. Deliberately
-- filtered at the candidate stage, NOT folded into the score — a
@@ -1226,7 +1219,7 @@ type SuggestArtistsForUserRow struct {
// Per-user artist suggestions ranked by taste signal x similarity, projected
// through artist_similarity_unmatched (out-of-library candidates only).
//
// Seeds are TIERED (rule #131) so the surface never empties:
// Seeds are TIERED so the surface never empties:
//
// tier 1 - taste_profile_artists.weight: engagement-graded, time-decayed and
// SIGNED by internal/taste, so an artist the user has drifted away
@@ -1234,9 +1227,9 @@ type SuggestArtistsForUserRow struct {
// tier 2 - likes + completed plays, used ONLY when the profile has no rows
// (new account, or before the first daily recompute).
//
// The signal is log-damped: contribution is signal x similarity, and the old
// undamped sum let one heavily-played artist's neighbours take every slot --
// entrenching harder the MORE the user listened (issue #2367 mechanism 2).
// The signal is log-damped: contribution is signal x similarity, and an
// undamped sum would let one heavily-played artist's neighbours take every
// slot -- entrenching harder the MORE the user listened.
//
// Candidates already in the library, already requested and not terminal, or
// snoozed by this user, are excluded. $1=user_id, $2=half_life_days (tier 2
@@ -44,12 +44,12 @@ type RecommendationSourceMetricsForUserRow struct {
// $1 user_id, $2 window_days. plays/skips are counts; avg_completion is the
// mean completion ratio over the completion_n plays that recorded one.
// pick_kind splits For You plays into taste/fresh/unattributed (#1249);
// pick_kind splits For You plays into taste/fresh/unattributed;
// it is NULL for every other source, so those still group to one row.
//
// completion_sqsum carries the sum of SQUARED completion ratios so the Go
// handler can compute a variance — needed for the margin of error on a
// completion delta (#2495). It is the sum rather than `stddev_samp` on purpose:
// completion delta. It is the sum rather than `stddev_samp` on purpose:
// raw source rows get merged into surface families in Go, and sums of squares
// add across groups exactly, whereas standard deviations cannot be combined
// without them. Variance = (sqsum - sum²/n) / (n-1), with sum = avg × n.
@@ -112,17 +112,17 @@ type RecommendationWeeklyTrendsRow struct {
TasteHits int64
}
// Recommendation observability (#796 phase 4). Per-source play outcomes so the
// Recommendation observability. Per-source play outcomes so the
// operator can see whether each recommendation surface is landing and tune the
// taste weights. Source is stamped on play_events when a play is launched from
// a recommendation surface; NULL means the user picked the track manually —
// those rows are INCLUDED here as the baseline control group the surfaces are
// judged against (milestone #127: delta-vs-baseline is what makes the numbers
// actionable). Raw source strings are bucketed into stable surface families in
// the Go handler; completion_n is carried so family merges can weight
// judged against (delta-vs-baseline is what makes the numbers actionable).
// Raw source strings are bucketed into stable surface families in the Go
// handler; completion_n is carried so family merges can weight
// avg_completion correctly.
// Weekly per-source outcome series for the tuning lab's trend view
// (#1251). Aggregated across ALL users: the tuning knobs are global,
// . Aggregated across ALL users: the tuning knobs are global,
// so judging a knob turn needs global outcomes — rows carry rates
// only, no track or user identity. NULL-source (manual) rows are
// included as the baseline family.
+5 -5
View File
@@ -70,7 +70,7 @@ SELECT id, changed_at, scope, action, changes
LIMIT $1
`
// Newest first; consumed by the metrics trend view (#1251) to annotate
// Newest first; consumed by the metrics trend view to annotate
// knob turns on the timeline.
func (q *Queries) ListTuningAudit(ctx context.Context, limit int32) ([]RecommendationTuningAudit, error) {
rows, err := q.db.Query(ctx, listTuningAudit, limit)
@@ -279,8 +279,8 @@ type UpsertDiscoverTuningDefaultsParams struct {
SnoozeDays float64
}
// Boot reconcile for the Discover scope (#2377). Never overwrites
// operator-tuned values, same contract as the other two.
// Boot reconcile for the Discover scope. Never overwrites
// admin-tuned values, same contract as the other two.
func (q *Queries) UpsertDiscoverTuningDefaults(ctx context.Context, arg UpsertDiscoverTuningDefaultsParams) error {
_, err := q.db.Exec(ctx, upsertDiscoverTuningDefaults, arg.TagOverlapWeight, arg.SnoozeDays)
return err
@@ -341,10 +341,10 @@ type UpsertWeightProfileDefaultsParams struct {
ContextTimeWeight float64
}
// Recommendation tuning lab queries (#1250). Seeding happens via the
// Recommendation tuning lab queries. Seeding happens via the
// recsettings boot reconcile; shipped defaults live in Go only.
// Boot reconcile: insert the shipped defaults for a profile if the row
// doesn't exist yet. Never overwrites operator-tuned values.
// doesn't exist yet. Never overwrites admin-tuned values.
func (q *Queries) UpsertWeightProfileDefaults(ctx context.Context, arg UpsertWeightProfileDefaultsParams) error {
_, err := q.db.Exec(ctx, upsertWeightProfileDefaults,
arg.Profile,
+1 -1
View File
@@ -520,7 +520,7 @@ type RepointRequestsAlbumMbidParams struct {
OldMbid *string
}
// A want moved from a release id to its group (#5241) takes the requests
// A want moved from a release id to its group takes the requests
// that name it along, so the reconciler finds it.
func (q *Queries) RepointRequestsAlbumMbid(ctx context.Context, arg RepointRequestsAlbumMbidParams) error {
_, err := q.db.Exec(ctx, repointRequestsAlbumMbid, arg.NewMbid, arg.OldMbid)
+2 -2
View File
@@ -98,7 +98,7 @@ type InsertScanRunRow struct {
StartedAt pgtype.Timestamptz
}
// M7 #381: scan-run lifecycle queries.
// Scan-run lifecycle queries.
// Creates a new scan_runs row with started_at=now(). Stages are filled in
// as each completes via UpdateScanRunStage; finished_at is set by FinishScanRun.
func (q *Queries) InsertScanRun(ctx context.Context) (InsertScanRunRow, error) {
@@ -119,7 +119,7 @@ type UpdateScanRunArtistArtEnrichParams struct {
ArtistArtEnrich []byte
}
// M7 cover-sources: persists the artist-art enrich stage tally JSON.
// Persists the artist-art enrich stage tally JSON.
func (q *Queries) UpdateScanRunArtistArtEnrich(ctx context.Context, arg UpdateScanRunArtistArtEnrichParams) error {
_, err := q.db.Exec(ctx, updateScanRunArtistArtEnrich, arg.ID, arg.ArtistArtEnrich)
return err
+3 -3
View File
@@ -99,7 +99,7 @@ const getAcquisitionSettings = `-- name: GetAcquisitionSettings :one
SELECT id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours, library_listing_started_at, library_listed_at, library_unlisted, upgrade_until FROM acquisition_settings WHERE id = true
`
// Searching for wants and grabbing releases (M502 #5521).
// Searching for wants and grabbing releases.
func (q *Queries) GetAcquisitionSettings(ctx context.Context) (AcquisitionSetting, error) {
row := q.db.QueryRow(ctx, getAcquisitionSettings)
var i AcquisitionSetting
@@ -385,7 +385,7 @@ SELECT info_hash, name, reason, detail, want_id, blocked_by, blocked_at, expires
`
// Every release set aside and still held back: a stalled one lifts by itself
// at expires_at (#5523).
// at expires_at.
func (q *Queries) ListReleaseBlocks(ctx context.Context) ([]ReleaseBlock, error) {
rows, err := q.db.Query(ctx, listReleaseBlocks)
if err != nil {
@@ -500,7 +500,7 @@ type ListWantsDueForSearchRow struct {
// Wants with no download running that were never searched or not since
// @hours ago: open wants first (never-searched, then requests, then missing
// files, then followed artists' releases), then held albums wanted for an
// upgrade (#5524), which are searched at most weekly.
// upgrade, which are searched at most weekly.
func (q *Queries) ListWantsDueForSearch(ctx context.Context, arg ListWantsDueForSearchParams) ([]ListWantsDueForSearchRow, error) {
rows, err := q.db.Query(ctx, listWantsDueForSearch, arg.Hours, arg.MaxRows)
if err != nil {
+2 -1
View File
@@ -57,7 +57,8 @@ type DeleteSessionForUserParams struct {
UserID pgtype.UUID
}
// Scoped by user_id, not just id (rule #47). Keyed on the id alone, any
// Scoped by user_id, not just id: household data is per-user. Keyed on the
// id alone, any
// household member could revoke another member's session by guessing a uuid.
// execrows lets the handler answer 404 rather than a false 204 when the row
// isn't theirs.
+5 -5
View File
@@ -134,8 +134,8 @@ type ListPlayedTracksNeedingSimilarityRow struct {
// 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.
// 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 {
@@ -310,9 +310,9 @@ WHERE ts.source = 'listenbrainz'
//
// 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
// 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.
+2 -2
View File
@@ -33,7 +33,7 @@ type LinkTracksAsSongRow struct {
Moved int64
}
// Song links (M498 #5438): copies of one song on different releases share a
// Song links: copies of one song on different releases share a
// song_key (migration 0076).
// Links the tracks into one song, joining any songs they already belong to.
// The song keeps the lowest key among them, so linking the same set again
@@ -57,7 +57,7 @@ type ListTrackSongKeysRow struct {
}
// What the mix writer needs to place a track: its song, and whether it is a
// video rip held back from the mixes (#5439).
// video rip held back from the mixes.
func (q *Queries) ListTrackSongKeys(ctx context.Context, ids []pgtype.UUID) ([]ListTrackSongKeysRow, error) {
rows, err := q.db.Query(ctx, listTrackSongKeys, ids)
if err != nil {
+1 -1
View File
@@ -85,7 +85,7 @@ type SnoozeSuggestionParams struct {
Column4 float64
}
// Time-boxed "not right now" on a Discover artist suggestion (#2374).
// Time-boxed "not right now" on a Discover artist suggestion.
//
// See 0049_suggestion_snoozes.up.sql for why this is a snooze and not a
// dismissal: it records no verdict on the music, expires on its own, and
+37 -40
View File
@@ -39,7 +39,7 @@ SELECT t.id, t.album_id, t.artist_id
JOIN affinity_artists aa ON aa.artist_id = t.artist_id
LEFT JOIN play_counts pc ON pc.track_id = t.id
LEFT JOIN skip_counts sc ON sc.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND COALESCE(pc.c, 0) <= 2
AND COALESCE(sc.c, 0) < 2
AND NOT EXISTS (
@@ -62,18 +62,18 @@ type ListDeepCutsTracksRow struct {
}
// Candidate queries for the discovery system-playlist mixes
// (#419-423). Each returns (id, album_id, artist_id) rows; the Go
// . Each returns (id, album_id, artist_id) rows; the Go
// producer caps + truncates and converts to rankedCandidate. Daily-
// deterministic md5(id||dateStr) ordering (where used) keeps a mix
// stable within a day while rotating across days; shuffle-on-play
// gives per-play variety on top.
// #419 Deep Cuts: low-play tracks (<=2 plays) from artists the user
// Deep Cuts: low-play tracks (<=2 plays) from artists the user
// has liked OR played heavily (>=5 non-skip plays across the artist).
// Tracks the user has skipped twice or more don't qualify (#1257):
// eligibility used to count only unskipped plays, so the most actively
// rejected tracks read as "barely heard" and kept being re-offered.
// Threshold 2 so a single accidental skip doesn't banish a track;
// passive-signal only (no dislike UI, rule #101).
// Tracks the user has skipped twice or more don't qualify: counting only
// unskipped plays would make the most actively rejected tracks read as
// "barely heard" and keep re-offering them. Threshold 2 so a single
// accidental skip doesn't banish a track; passive signal only (there is no
// dislike button).
// $1 user_id, $2 date string.
func (q *Queries) ListDeepCutsTracks(ctx context.Context, arg ListDeepCutsTracksParams) ([]ListDeepCutsTracksRow, error) {
rows, err := q.db.Query(ctx, listDeepCutsTracks, arg.UserID, arg.Column2)
@@ -125,7 +125,7 @@ SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
FROM tracks t
JOIN albums al ON al.id = t.album_id
JOIN albums_tiered alt ON alt.album_id = al.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND alt.tier IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM attempted a WHERE a.track_id = t.id)
AND NOT EXISTS (
@@ -153,11 +153,10 @@ type ListFirstListensTracksRow struct {
Tier int32
}
// #423 First Listens: songs the user has never heard or even
// attempted. "Attempted" is track-level with a >=30s listen threshold
// (#1268, operator decision 2026-07-03) — the old version disqualified
// a whole album on ANY play_event, so a 2-second accidental brush
// banished it permanently.
// First Listens: songs the user has never heard or even
// attempted. "Attempted" is track-level with a >=30s listen threshold:
// disqualifying a whole album on ANY play_event would let a 2-second
// accidental brush banish it permanently.
//
// tier 1 the exact desire: albums with ZERO attempted tracks
// tier 2 step back: barely-attempted albums (<=25% of tracks
@@ -227,7 +226,7 @@ albums_tiered AS (
SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
FROM tracks t
JOIN albums_tiered alt ON alt.album_id = t.album_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
@@ -244,14 +243,13 @@ type ListNewForYouTracksRow struct {
Tier int32
}
// #421 New for you, tiered per project rule #131 (#1267). The old
// single hard rule (added <30d AND direct affinity) had two failure
// modes with one root: no notion of consumption — the album you
// devoured in week one crowded the mix for three more weeks, and
// after a quiet month nothing qualified at all.
// New for you, tiered so the mix degrades rather than vanishes. A
// single hard rule (added <30d AND direct affinity) has no notion of
// consumption: the album you devoured in week one would crowd the mix
// for three more weeks, and after a quiet month nothing would qualify.
//
// "Consumed" = any track attempted for >=30s (the First Listens
// threshold, #1268): once you've meaningfully engaged, the album is
// threshold): once you've meaningfully engaged, the album is
// no longer news. Applies to every tier — played albums leave the
// mix the next build.
//
@@ -306,7 +304,7 @@ WITH windowed AS (
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
JOIN windowed w ON w.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
@@ -326,18 +324,18 @@ type ListOnThisDayTracksRow struct {
ArtistID pgtype.UUID
}
// #422 On This Day: tracks the user played around this calendar date
// On This Day: tracks the user played around this calendar date
// (±10 day-of-year) in the past, excluding the very recent (>30 days
// ago) so it's nostalgic, not just "last week". Weighted by how much
// they were played in those windows. Floor relaxed from 60→30 days
// and window ±7→±10 so it surfaces on a months-old library instead
// of needing a full year of history; still skips cleanly (no rows →
// no playlist) when there's no qualifying history yet.
// Day-of-year distance is circular (#1256): plain ABS made Dec 28 vs
// Jan 3 read as 359 days apart, silently gutting the window for ~3
// weeks around every New Year — precisely when holiday nostalgia is
// the point. LEAST(d, 365-d) wraps the boundary; leap-year drift of
// ±1 is absorbed by the ±10 window. The build date ($2, already here
// they were played in those windows. The 30-day floor and ±10 window
// let it surface on a months-old library rather than needing a full
// year of history; it still skips cleanly (no rows → no playlist)
// when there's no qualifying history yet.
// Day-of-year distance is circular: plain ABS would read Dec 28 vs
// Jan 3 as 359 days apart, gutting the window for ~3 weeks around
// every New Year — precisely when holiday nostalgia is the point.
// LEAST(d, 365-d) wraps the boundary; leap-year drift of ±1 is absorbed
// by the ±10 window. The build date ($2, already here
// for the md5 rotation) anchors "today" instead of now() so the
// window is testable and consistent with the mix's daily determinism.
// $1 user_id, $2 date string.
@@ -371,18 +369,18 @@ WITH stats AS (
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
JOIN stats s ON s.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND s.c >= 3
AND s.last_at <= now() - interval '30 days'
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
-- Declined-resurfacing cooldown (#1258): a skip on a
-- Declined-resurfacing cooldown: a skip on a
-- rediscover-sourced play is the user explicitly saying "I've moved
-- on" to the exact invitation this mix extends — such tracks sit
-- out ~90 days instead of re-qualifying the next day. Passive
-- signal only (rule #101).
-- signal only.
AND NOT EXISTS (
SELECT 1 FROM play_events rp
WHERE rp.user_id = $1
@@ -403,11 +401,10 @@ type ListRediscoverTracksRow struct {
ArtistID pgtype.UUID
}
// #420 Rediscover: tracks the user played a lot but has drifted away
// from. One blended pool (issue #1246: the old two-tier UNION was
// all-or-nothing — a single ">=6 months cold" row suppressed the whole
// ">=30 days" tier, which is how a one-song playlist shipped; the 6mo
// tier was also a strict subset of the 30d tier). Eligibility is >=3
// Rediscover: tracks the user played a lot but has drifted away
// from. One blended pool, not a two-tier UNION: the ">=6 months cold" tier is
// a strict subset of the ">=30 days" one, and a UNION that let one row of it
// suppress the wider tier would ship a one-song playlist. Eligibility is >=3
// non-skip plays and >=30 days cold; the bar sits at 3 rather than 5
// so a weeks-old library still fields a pool — on young histories the
// >=5-play tracks are precisely the ones still in rotation. Ordering
@@ -34,7 +34,7 @@ type AppendRotationPlayedParams struct {
TrackID pgtype.UUID
}
// Rotation state for system-playlist sample-history dedup (#415).
// Rotation state for system-playlist sample-history dedup.
// One row per (user, playlist_kind). played_track_ids accumulates
// tracks heard from that playlist in the current rotation; shuffle
// prefers ids NOT in this set, then resets when the snapshot is
+10 -10
View File
@@ -198,14 +198,14 @@ SELECT u.id FROM users u
)
`
// Track picks here join `tracks ... AND t.missing_since IS NULL` (#2523): a
// Track picks here join `tracks ... AND t.missing_since IS NULL`: a
// seed or For-You candidate has to be something that can actually play. Note
// this only affects newly GENERATED playlists — already-stored system
// playlists keep their rows until the next daily rebuild, which is why the
// shared ListPlaylistTracks read path is deliberately left unfiltered (it
// also serves user-curated playlists, where hiding a track the user added
// themselves would be wrong).
// M7 #352 slice 2: system-generated playlist queries.
// System-generated playlist queries.
// Active = had a play in the last 7 days. The cron iterates this list.
func (q *Queries) ListActiveUsersForSystemPlaylists(ctx context.Context) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listActiveUsersForSystemPlaylists)
@@ -368,10 +368,10 @@ type PickSeedArtistsRow struct {
}
// Top-12 most-engaged distinct artist candidates, tiered so the
// "Songs like X" mixes never silently vanish (#1255): the old hard
// 7-day window emptied the seed pool after a quiet week, and the
// daily atomic-replace build then deleted every existing mix until
// the user played something again. Same fallback shape as
// "Songs like X" mixes never silently vanish: a hard 7-day window
// would empty the seed pool after a quiet week, and the daily
// atomic-replace build would then delete every existing mix until the
// user played something again. Same fallback shape as
// PickTopPlayedTracksForUser (For You's seeds):
//
// tier 0 engagement in the last 7 days
@@ -380,12 +380,12 @@ type PickSeedArtistsRow struct {
// tier 3 (only if 0-2 empty) liked artists with no play history
//
// All returned rows share one tier; produceSeedMixes maps it onto the
// rule-#131 pick-kind ladder and stamps the built tracks, so metrics
// tiered pick-kind ladder and stamps the built tracks, so metrics
// can compare mixes seeded from fresh vs stale engagement.
// Score = unskipped-play count + 5 if user has liked the artist. The
// Go-side picker (pickSeedArtistsForDay) shuffles the 12
// daily-deterministically and takes songsLikeSeedCount (6) so the mix
// set both rotates day-to-day and fills the dedicated Home row (#1491).
// set both rotates day-to-day and fills the dedicated Home row.
func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) {
rows, err := q.db.Query(ctx, pickSeedArtists, userID)
if err != nil {
@@ -452,7 +452,7 @@ SELECT COALESCE(
FROM tracks t
JOIN albums a ON a.id = t.album_id
WHERE t.artist_id = $2
AND t.missing_since IS NULL -- #2701: the play branch filters; this one must too
AND t.missing_since IS NULL -- the play branch filters; this one must too
ORDER BY a.release_date DESC NULLS LAST,
t.disc_number NULLS LAST,
t.track_number NULLS LAST,
@@ -530,7 +530,7 @@ SELECT id
// library with sparse history.
// A likes-only read still joins tracks: a like outlives its file, and a
// seed whose file is gone points For You at something the user can't hear
// (#2701). The other two tiers get the same filter from their play_events
// . The other two tiers get the same filter from their play_events
// join.
func (q *Queries) PickTopPlayedTracksForUser(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, pickTopPlayedTracksForUser, userID)
+2 -2
View File
@@ -74,7 +74,7 @@ SELECT provider_id, enabled, api_key, display_order, created_at, updated_at
ORDER BY display_order, provider_id
`
// Tag-enrichment provider settings queries (milestone #160, #1490 Step 2).
// Tag-enrichment provider settings queries.
// The tags.SettingsService consumes these — parallel to the cover-art
// SettingsService (coverart_settings.sql). Keeping the two independent
// means a new tag source is added without touching the art settings.
@@ -150,7 +150,7 @@ type UpsertTagProviderSettingsParams struct {
// INSERT a default row for a newly-registered provider, or refresh an
// existing row's display_order on boot. Does NOT overwrite enabled /
// api_key on conflict — the operator's settings persist across restarts.
// api_key on conflict — an admin's settings persist across restarts.
func (q *Queries) UpsertTagProviderSettings(ctx context.Context, arg UpsertTagProviderSettingsParams) error {
_, err := q.db.Exec(ctx, upsertTagProviderSettings, arg.ProviderID, arg.Enabled, arg.DisplayOrder)
return err
+7 -7
View File
@@ -153,7 +153,7 @@ type ListLikedTrackTasteInputsForUserRow struct {
// (track_id, artist_id, genre, release_date) for each track the user has
// explicitly liked. Feeds the track-like bonus into the liked track's artist,
// tags, and era (#1530); track_id keys the enriched track_tags lookup (#1490).
// tags, and era; track_id keys the enriched track_tags lookup.
func (q *Queries) ListLikedTrackTasteInputsForUser(ctx context.Context, userID pgtype.UUID) ([]ListLikedTrackTasteInputsForUserRow, error) {
rows, err := q.db.Query(ctx, listLikedTrackTasteInputsForUser, userID)
if err != nil {
@@ -216,14 +216,14 @@ type ListPlayEngagementInputsForUserRow struct {
AgeDays float64
}
// Taste profile (#796 phase 1). The daily build computes signed artist/tag
// Taste profile. The daily build computes signed artist/tag
// weights in Go (graded engagement + decay + like bonus) and atomic-replaces
// them here. Read queries serve "top taste" lookups (used in phase 2).
// them here. Read queries serve the scorer's "top taste" lookups.
// One row per play in the decay-relevant window. completion is the play's
// completion ratio (precomputed column when present, else duration_played /
// track duration, clamped to [0,1]); age_days drives the time-decay. Genre
// is split into tags in Go; release_date derives the decade for the era
// facet (#1530). Quarantined tracks are excluded.
// facet. Quarantined tracks are excluded.
func (q *Queries) ListPlayEngagementInputsForUser(ctx context.Context, arg ListPlayEngagementInputsForUserParams) ([]ListPlayEngagementInputsForUserRow, error) {
rows, err := q.db.Query(ctx, listPlayEngagementInputsForUser, arg.UserID, arg.Column2)
if err != nil {
@@ -269,7 +269,7 @@ type ListTasteProfileArtistsForUserRow struct {
Weight float64
}
// Top-weighted taste artists (phase-2 consumption + tests).
// Top-weighted taste artists, for the scorer and tests.
func (q *Queries) ListTasteProfileArtistsForUser(ctx context.Context, arg ListTasteProfileArtistsForUserParams) ([]ListTasteProfileArtistsForUserRow, error) {
rows, err := q.db.Query(ctx, listTasteProfileArtistsForUser, arg.UserID, arg.Limit)
if err != nil {
@@ -308,7 +308,7 @@ type ListTasteProfileErasForUserRow struct {
Weight float64
}
// Top-weighted taste eras (#1530); consumed by the scorer's era term.
// Top-weighted taste eras; consumed by the scorer's era term.
func (q *Queries) ListTasteProfileErasForUser(ctx context.Context, arg ListTasteProfileErasForUserParams) ([]ListTasteProfileErasForUserRow, error) {
rows, err := q.db.Query(ctx, listTasteProfileErasForUser, arg.UserID, arg.Limit)
if err != nil {
@@ -347,7 +347,7 @@ type ListTasteProfileMoodsForUserRow struct {
Weight float64
}
// Top-weighted taste moods (#1534); consumed by the scorer's mood term.
// Top-weighted taste moods; consumed by the scorer's mood term.
func (q *Queries) ListTasteProfileMoodsForUser(ctx context.Context, arg ListTasteProfileMoodsForUserParams) ([]ListTasteProfileMoodsForUserRow, error) {
rows, err := q.db.Query(ctx, listTasteProfileMoodsForUser, arg.UserID, arg.Limit)
if err != nil {
+3 -3
View File
@@ -121,7 +121,7 @@ type ListTrackTagsForTracksRow struct {
Tag string
}
// Enriched tags for a set of candidate tracks (#1534), so the scorer can
// Enriched tags for a set of candidate tracks, so the scorer can
// derive each candidate's mood buckets at scoring time. One row per (track,
// tag); the caller filters to mood words via the internal/mood vocabulary.
func (q *Queries) ListTrackTagsForTracks(ctx context.Context, dollar_1 []pgtype.UUID) ([]ListTrackTagsForTracksRow, error) {
@@ -168,7 +168,7 @@ type ListTracksMissingTagsRow struct {
ArtistMbid *string
}
// Track-tag enrichment queries (milestone #160, #1490). The tag enricher
// Track-tag enrichment queries. The tag enricher
// drains ListTracksMissingTags, fetches from the provider chain, and
// writes the merged top-K via DeleteTrackTags + InsertTrackTag, then
// stamps SetTrackTagSource. Mirrors the coverart artist-enricher shape.
@@ -176,7 +176,7 @@ type ListTracksMissingTagsRow struct {
// or previously settled 'none' under an older provider version. Returns
// the fields the provider chain needs — recording MBID (nullable) for
// keyed lookups, title + artist name for name-based fallback, and the
// artist MBID (nullable) for MusicBrainz's artist-tag fallback (#1519).
// artist MBID (nullable) for MusicBrainz's artist-tag fallback.
// $1 = current tag_sources_version, $2 = limit.
func (q *Queries) ListTracksMissingTags(ctx context.Context, arg ListTracksMissingTagsParams) ([]ListTracksMissingTagsRow, error) {
rows, err := q.db.Query(ctx, listTracksMissingTags, arg.TagSourcesVersion, arg.Limit)
+28 -29
View File
@@ -62,10 +62,10 @@ UPDATE tracks
WHERE id = ANY($1::uuid[])
`
// Forgets the recording MBID of rows whose file was another song's (M498's
// wrong-slot removal). The MBID came from that file, so it names the song kept
// elsewhere on the album, and missing-pair folding would fold the emptied slot
// into it: twelve slots went that way on 2026-10-20 and were never wanted back.
// Forgets the recording MBID of rows whose file was another song's (the
// duplicate resolver's wrong-slot removal). The MBID came from that file, so it
// names the song kept elsewhere on the album, and missing-pair folding would
// fold the emptied slot into it, so it would never be wanted back.
// mbid_source goes with it (tracks_mbid_source_pair).
func (q *Queries) ClearTrackMbids(ctx context.Context, ids []pgtype.UUID) error {
_, err := q.db.Exec(ctx, clearTrackMbids, ids)
@@ -177,7 +177,7 @@ type DeleteTrackRow struct {
Mbid *string
}
// M7 #372: hard delete with FK cascade. The CASCADE on track_id from
// Hard delete with FK cascade. The CASCADE on track_id from
// play_events / general_likes_tracks / quarantine /
// quarantine_actions handles their cleanup. RETURNING gives us
// album_id + artist_id for the album-empty / artist-empty cascade
@@ -209,11 +209,11 @@ type FindMissingTrackByAudioHashRow struct {
FilePath string
}
// Move detection fallback for files with no MBID (#2528, #3914). The audio stream
// Move detection fallback for files with no MBID. The audio stream
// hash identifies the encoded audio itself, so it survives a rename, a move and a
// retag — anything short of a re-encode. It replaced (file_size, duration_ms),
// which could pair two unrelated files that happened to share a byte count and a
// duration, and missed a file retagged in place, whose size changes.
// retag — anything short of a re-encode. (file_size, duration_ms) would pair two
// unrelated files that share a byte count and a duration, and miss a file
// retagged in place, whose size changes.
//
// Same missing-only constraint and same LIMIT 2 rationale as the MBID variant.
func (q *Queries) FindMissingTrackByAudioHash(ctx context.Context, audioStreamSha256 []byte) ([]FindMissingTrackByAudioHashRow, error) {
@@ -249,7 +249,7 @@ type FindMissingTrackByMbidRow struct {
FilePath string
}
// Move detection, strongest signal (#2528). A file that turned up at a new path
// Move detection, strongest signal. A file that turned up at a new path
// carrying a recording MBID we already have on a MISSING row is that recording,
// moved — not a new track.
//
@@ -287,8 +287,8 @@ UPDATE tracks
AND regexp_replace(file_path, '^.*/', '') ~* $1::text
`
// Holds back present tracks whose basename carries a video-rip marker (M498
// #5439), the same pattern the report lists. A track the operator called fine
// Holds back present tracks whose basename carries a video-rip marker, the
// same pattern the report lists. A track an admin called fine
// keeps that verdict.
func (q *Queries) FlagSuspectSourceTracks(ctx context.Context, pattern string) (int64, error) {
result, err := q.db.Exec(ctx, flagSuspectSourceTracks, pattern)
@@ -377,7 +377,7 @@ SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, f
`
// Batched lookup used by /api/library/sync to hydrate upsert payloads
// (#357). Mirror of GetArtistsByIDs.
// . Mirror of GetArtistsByIDs.
func (q *Queries) GetTracksByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Track, error) {
rows, err := q.db.Query(ctx, getTracksByIDs, dollar_1)
if err != nil {
@@ -451,7 +451,7 @@ type ListArtistTracksForUserRow struct {
ArtistName string
}
// M6a: every track for the artist across their albums, with album_title
// Every track for the artist across their albums, with album_title
// and artist_name joined. Honors per-user quarantine. Used by
// /api/artists/{id}/tracks for the artist-card play affordance, which
// shuffles client-side. Ordering matches album/track natural order so
@@ -545,19 +545,18 @@ type ListMissingTracksRow struct {
LastPlayedAt pgtype.Timestamptz
}
// The admin review surface for files the scan could not find (#2527).
// The admin review surface for files the scan could not find.
//
// Ordered by directory, then by the file's own position within its album,
// because the unit an operator actually reasons about is a FOLDER: the case
// this was built for was three whole albums that had been reorganised, and a
// flat list ordered by timestamp presents that as forty unrelated decisions.
// because the unit an admin actually reasons about is a FOLDER: a reorganised
// album, listed flat by timestamp, reads as dozens of unrelated decisions.
// Grouping happens in the handler; the ordering here is what makes a group
// contiguous, so a page boundary splits a directory at worst.
//
// last_played_at is a correlated MAX rather than a join so a track with no
// plays stays in the result with NULL. It is here because "gone six months,
// never played" and "gone yesterday, played 200 times" deserve opposite
// reactions, and the operator can't tell them apart from a path.
// reactions, and an admin can't tell them apart from a path.
func (q *Queries) ListMissingTracks(ctx context.Context, arg ListMissingTracksParams) ([]ListMissingTracksRow, error) {
rows, err := q.db.Query(ctx, listMissingTracks, arg.PageOffset, arg.PageLimit)
if err != nil {
@@ -616,7 +615,7 @@ type ListRandomTracksForUserRow struct {
ArtistName string
}
// #427 S4: backing query for GET /api/library/shuffle — the online
// Backing query for GET /api/library/shuffle — the online
// source for "Shuffle all". N random tracks across the whole
// library, per-user-quarantine filtered. $1 user_id, $2 limit.
func (q *Queries) ListRandomTracksForUser(ctx context.Context, arg ListRandomTracksForUserParams) ([]ListRandomTracksForUserRow, error) {
@@ -682,7 +681,7 @@ type ListStillMissingTrackLabelsRow struct {
}
// Which of the tracks a scan marked missing still are once it has adopted
// moves and folded replacements (M542): the ones worth telling an admin
// moves and folded replacements: the ones worth telling an admin
// about, by name. A quality upgrade folds into its replacement and drops out.
func (q *Queries) ListStillMissingTrackLabels(ctx context.Context, ids []pgtype.UUID) ([]ListStillMissingTrackLabelsRow, error) {
rows, err := q.db.Query(ctx, listStillMissingTrackLabels, ids)
@@ -719,7 +718,7 @@ SELECT t.id,
t.source_verdict,
t.source_verdict_at,
-- The album's place on the wanted list: wanted for a clean copy, and
-- when that was last searched for (#5524).
-- when that was last searched for.
COALESCE(w.upgrade, '')::text AS want_upgrade,
w.searched_at AS searched_at
FROM tracks t
@@ -760,7 +759,7 @@ type ListSuspectSourceTracksRow struct {
}
// The admin report of present tracks whose filename looks like a video rip
// (#5410): "(Official Video)", "[Audio]", "Visualizer" and the like.
// : "(Official Video)", "[Audio]", "Visualizer" and the like.
//
// pattern is built in Go (internal/api/admin_suspect_sources.go) from the
// same marker list that labels each row, so the filter and the labels cannot
@@ -816,7 +815,7 @@ type ListTrackPathsForReconcileRow struct {
}
// Every row's path + current missing mark, for the scanner's reconcile pass
// (#2523). Deliberately unfiltered and unpaged: reconcile has to compare the
// . Deliberately unfiltered and unpaged: reconcile has to compare the
// WHOLE table against what the walk saw, and a filtered subset would let rows
// outside it drift forever. Three narrow columns keep it cheap even on a
// library of a few hundred thousand tracks.
@@ -921,7 +920,7 @@ type ListTracksMissingMbidWithPathRow struct {
// Track recording-MBID backfill: tracks with NULL mbid that still have
// a file to re-read. $1 caps the batch (mirrors the album backfill).
// Missing tracks are skipped: there is no file, and opening it anyway
// logged a warning per track on every scan (#5139).
// would log a warning per track on every scan.
func (q *Queries) ListTracksMissingMbidWithPath(ctx context.Context, limit int32) ([]ListTracksMissingMbidWithPathRow, error) {
rows, err := q.db.Query(ctx, listTracksMissingMbidWithPath, limit)
if err != nil {
@@ -1081,7 +1080,7 @@ type SetTrackSourceVerdictParams struct {
ID pgtype.UUID
}
// The operator's call on a flagged track: 'fine' lets it back into radio and
// An admin's call on a flagged track: 'fine' lets it back into radio and
// the mixes for good; 'suspect' holds it back again.
func (q *Queries) SetTrackSourceVerdict(ctx context.Context, arg SetTrackSourceVerdictParams) (int64, error) {
result, err := q.db.Exec(ctx, setTrackSourceVerdict, arg.Verdict, arg.ID)
@@ -1115,7 +1114,7 @@ ON CONFLICT (file_path) DO UPDATE SET
WHEN tracks.mbid_source = 'acoustid' THEN 'acoustid' END,
genre = EXCLUDED.genre,
-- Stamped on update too, so a tag-repair pass marks rows as done and the
-- next scan can short-circuit them again (#2499).
-- next scan can short-circuit them again.
tag_read_version = EXCLUDED.tag_read_version,
updated_at = now()
RETURNING id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since, mbid_source, song_id, song_key, source_verdict, source_verdict_at, emptied_at
@@ -1138,8 +1137,8 @@ type UpsertTrackParams struct {
}
// file_path is the canonical identity for library scan; mbid is secondary.
// mbid is the file's tag. A looked-up id (mbid_source 'acoustid', M401)
// survives a re-read that finds no tag id, and a tag id replaces it (D4).
// mbid is the file's tag. A looked-up id (mbid_source 'acoustid')
// survives a re-read that finds no tag id, and a tag id replaces it.
func (q *Queries) UpsertTrack(ctx context.Context, arg UpsertTrackParams) (Track, error) {
row := q.db.QueryRow(ctx, upsertTrack,
arg.Title,
+1 -1
View File
@@ -584,7 +584,7 @@ type SetSubsonicPasswordParams struct {
// Stores (or clears with NULL) the per-user Subsonic legacy credential used
// for t/s and p auth on /rest/*. Must be plaintext; see migration 0003. Only
// ever a server-generated value, never the login password (#5026).
// ever a server-generated value, never the login password.
func (q *Queries) SetSubsonicPassword(ctx context.Context, arg SetSubsonicPasswordParams) error {
_, err := q.db.Exec(ctx, setSubsonicPassword, arg.ID, arg.SubsonicPassword)
return err
+21 -21
View File
@@ -68,15 +68,15 @@ const countWants = `-- name: CountWants :one
SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::bigint AS wanted,
COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason = 'missing')::bigint AS missing,
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
-- Held albums wanted for a better copy (#5524).
-- Held albums wanted for a better copy.
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL AND upgrade <> '')::bigint AS upgrades,
COUNT(DISTINCT artist_mbid)::bigint AS artists,
-- Library albums waiting to be matched by hand (#5579).
-- Library albums waiting to be matched by hand.
(SELECT COUNT(*) FROM library_album_matches m
WHERE m.outcome = 'unmatched'
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = m.album_id))::bigint AS unmatched,
(SELECT COUNT(*) FROM follows)::bigint AS following,
-- The Wanted view's other tabs (#5522): downloads running, those
-- The Wanted view's other tabs: downloads running, those
-- waiting on a person, and releases set aside.
(SELECT COUNT(*) FROM grabs WHERE ended_at IS NULL)::bigint AS activity,
(SELECT COUNT(*) FROM grabs
@@ -324,7 +324,7 @@ type GetLibraryListingRow struct {
LibraryUnlisted int32
}
// The one-time listing of the library (#5567). ---------------------------
// The one-time listing of the library. ---------------------------
func (q *Queries) GetLibraryListing(ctx context.Context) (GetLibraryListingRow, error) {
row := q.db.QueryRow(ctx, getLibraryListing)
var i GetLibraryListingRow
@@ -361,7 +361,7 @@ type GetWantAlbumRow struct {
AlbumID pgtype.UUID
}
// What the Wanted view's track list needs of a want (#5572).
// What the Wanted view's track list needs of a want.
func (q *Queries) GetWantAlbum(ctx context.Context, id pgtype.UUID) (GetWantAlbumRow, error) {
row := q.db.QueryRow(ctx, getWantAlbum, id)
var i GetWantAlbumRow
@@ -547,9 +547,9 @@ type ListAlbumsToMatchRow struct {
// Library albums the listing has not decided, or that an older matcher
// refused, or whose tracks changed since they were refused: each is matched
// to MusicBrainz (#5569). A library album is one from before the cutoff, or
// to MusicBrainz. A library album is one from before the cutoff, or
// one whose tracks were: a re-tag files a track under a new album row but
// keeps the track (#5579). An album row with no tracks is not an album in
// keeps the track. An album row with no tracks is not an album in
// the library; one a person left off stays off.
func (q *Queries) ListAlbumsToMatch(ctx context.Context, arg ListAlbumsToMatchParams) ([]ListAlbumsToMatchRow, error) {
rows, err := q.db.Query(ctx, listAlbumsToMatch, arg.Cutoff, arg.Matcher, arg.MaxRows)
@@ -752,7 +752,7 @@ type ListHeldWantFormatsRow struct {
Rips int32
}
// Upgrades (#5524). -------------------------------------------------------
// Upgrades. -------------------------------------------------------
// What each held album's copy is made of, for judging whether it is worth
// upgrading: its present tracks grouped by format, with their bytes and
// length summed, and how many are held back as video rips. A held want whose
@@ -936,7 +936,7 @@ type ListReleaseTypeProfilesRow struct {
FollowCount int64
}
// The wanted list (M502 #5520): release-type profiles, follows and wants.
// The wanted list: release-type profiles, follows and wants.
func (q *Queries) ListReleaseTypeProfiles(ctx context.Context) ([]ListReleaseTypeProfilesRow, error) {
rows, err := q.db.Query(ctx, listReleaseTypeProfiles)
if err != nil {
@@ -988,7 +988,7 @@ type ListUnmatchedLibraryAlbumsRow struct {
Tracks int32
}
// The Collection's Unmatched section (#5579): the albums the listing could
// The Collection's Unmatched section: the albums the listing could
// not match, and below them the ones a person left off. An album whose
// tracks have all moved to another row is gone, not unmatched.
func (q *Queries) ListUnmatchedLibraryAlbums(ctx context.Context, maxRows int32) ([]ListUnmatchedLibraryAlbumsRow, error) {
@@ -1070,7 +1070,7 @@ type ListWantArtistsRow struct {
Total int64
}
// The Wanted view's main page (#5572): every artist on the list, A–Z like
// The Wanted view's main page: every artist on the list, A–Z like
// the library, with how many of their albums are held of how many are on
// the list. q matches the artist's name or any of their albums' titles;
// artist_mbid narrows to one artist, for their page's header.
@@ -1114,7 +1114,7 @@ func (q *Queries) ListWantArtists(ctx context.Context, arg ListWantArtistsParams
const listWants = `-- name: ListWants :many
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason,
w.album_id, w.wanted_at, w.fulfilled_at,
-- Why a held album is wanted again, and the copy it has (#5524).
-- Why a held album is wanted again, and the copy it has.
w.upgrade, w.held_quality,
-- A request names an album before the catalogue has read it: the
-- catalogue's type and date fill in once it has.
@@ -1124,10 +1124,10 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
u.username AS requested_by,
la.id AS library_artist_id,
COUNT(*) OVER () AS total,
-- The search schedule's last look (#5521), and the download running.
-- The search schedule's last look, and the download running.
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
-- Its latest download, running or ended, and what the import made of
-- it (#5522).
-- it.
gr.id AS grab_id, gr.name AS grab_name, gr.grabbed_at, gr.origin AS grab_origin,
gr.quality AS grab_quality, gr.ended_at AS grab_ended_at, gr.end_reason AS grab_end_reason,
gr.progress AS grab_progress, gr.import_state AS grab_import_state,
@@ -1147,7 +1147,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
END
AND ($2::text = '' OR w.artist_mbid = $2::text)
-- want_ids narrows to given wants: the Missing page's folders (M504).
-- want_ids narrows to given wants: the Missing page's folders.
AND (COALESCE(cardinality($3::uuid[]), 0) = 0 OR w.id = ANY($3::uuid[]))
AND ($4::text = ''
OR strpos(lower(w.artist_name), lower($4::text)) > 0
@@ -1208,7 +1208,7 @@ type ListWantsRow struct {
// The Wanted page: open wants (all but missing ones, or only missing ones),
// the held ones, or all of them; artist_mbid narrows to one artist's page,
// want_ids to given wants. Ordered like the library (#5572): artists A–Z by the
// want_ids to given wants. Ordered like the library: artists A–Z by the
// library's sort name when it holds the artist, each artist's albums by
// release date, so the page groups them as they arrive. q narrows to an
// artist or album title; total counts every match, past the page.
@@ -1349,7 +1349,7 @@ ON CONFLICT (album_id) DO NOTHING
`
// The albums ListLibraryAlbums takes are decided as listed by their tags,
// so the matching that follows leaves them alone (#5569).
// so the matching that follows leaves them alone.
func (q *Queries) RecordListedAlbums(ctx context.Context, cutoff pgtype.Timestamptz) error {
_, err := q.db.Exec(ctx, recordListedAlbums, cutoff)
return err
@@ -1379,7 +1379,7 @@ type RepointWantParams struct {
ReleaseGroupMbid string
}
// A want recorded under a release id before #5241 is moved to its group.
// A want recorded under a release id is moved to its group.
func (q *Queries) RepointWant(ctx context.Context, arg RepointWantParams) error {
_, err := q.db.Exec(ctx, repointWant, arg.ID, arg.ReleaseGroupMbid)
return err
@@ -1571,10 +1571,10 @@ SELECT a.id AS album_id,
COALESCE(bool_or(t.missing_since IS NULL AND t.added_at > $1::timestamptz), false)::bool AS arrived_since,
COALESCE(bool_or(t.missing_since IS NOT NULL AND t.missing_since < $1::timestamptz), false)::bool AS missing_before,
-- Missing tracks no track on disk stands in for, by position or
-- title. A missing row whose song is back at a new path (#5245) is
-- title. A missing row whose song is back at a new path is
-- covered; a lost bonus track is not. A track with no disc number is
-- on disc 1: Air's Love 2 stayed wanted, and was downloaded again for
-- nothing, over a missing track 6 with no disc beside a present 1/6.
-- on disc 1, so a missing track 6 with no disc beside a present 1/6
-- does not keep the album wanted.
COUNT(*) FILTER (WHERE t.missing_since IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM tracks p
WHERE p.album_id = a.id AND p.missing_since IS NULL
+1 -1
View File
@@ -26,7 +26,7 @@ type CountListeningSignalForUserRow struct {
DistinctArtists int64
}
// "You might like" Home rows (#790). The daily build computes ranked
// "You might like" Home rows. The daily build computes ranked
// album/artist IDs in Go (similarity roll-up + cold-start gate) and
// atomic-replaces them here; /api/home reads them back hydrated.
// Cold-start gate. Returns the breadth of the user's real listening:
+2 -2
View File
@@ -1,4 +1,4 @@
-- AcoustID recording-id lookup (M401). Migration 0069 says what each
-- AcoustID recording-id lookup. Migration 0069 says what each
-- lookup state means.
-- name: ListTracksNeedingAcoustIDLookup :many
@@ -29,7 +29,7 @@ ON CONFLICT (track_id) DO UPDATE SET
looked_up_at = now();
-- name: SetTrackMbidFromAcoustID :execrows
-- Never over a tag id (D4): the guard is here, not only in the worker, so a
-- Never over a tag id: the guard is here, not only in the worker, so a
-- tag the scan read between the worker's list and this write still wins.
-- updated_at is left alone: it gates the scan's mtime skip, and moving it
-- past a file's mtime would hide a change made just before the lookup.
+1 -1
View File
@@ -1,4 +1,4 @@
-- Indexers and download clients (M502 #5518).
-- Indexers and download clients.
-- name: ListAcquisitionServices :many
-- Returns no secret, only whether one is set: this feeds the settings page.
+1 -1
View File
@@ -1,4 +1,4 @@
-- The admin Overview's status sections (M504): who is listening, what is
-- The admin Overview's status sections: who is listening, what is
-- downloading, what the library holds. Each is one cheap read.
-- name: ListListeningNow :many
+3 -3
View File
@@ -1,14 +1,14 @@
-- name: AdminSummary :one
-- What the admin rail and the Overview's verdict read (#5605): each queue
-- What the admin rail and the Overview's verdict read: each queue
-- that waits on a person, with how many and since when. One round trip, so
-- the rail costs one cheap read on every admin page. The "since" is the
-- oldest item still waiting: how long the queue has been left, which is what
-- tells the operator a count of 2 is fine today and wrong after a month.
-- tells an admin a count of 2 is fine today and wrong after a month.
SELECT
(SELECT count(*) FROM requests WHERE status = 'pending')::bigint AS requests,
(SELECT min(requested_at) FROM requests WHERE status = 'pending')::timestamptz AS requests_since,
-- A broken track is one report however it was found: a listener flagged
-- it, or it failed to play (#5606). Counted by track, as the Reports
-- it, or it failed to play. Counted by track, as the Reports
-- page lists them.
(SELECT count(*) FROM (
SELECT track_id FROM quarantine
+22 -27
View File
@@ -37,12 +37,12 @@ ORDER BY release_date NULLS LAST, sort_title;
-- name: GetAlbumByArtistAndTitle :one
-- Scanner uses this for the no-mbid dedupe path: resolve-or-create. An album
-- of the same name in another release group is a different album (Weezer's
-- six self-titled albums shared one row until 2026-10-20), so a row whose
-- of the same name in another release group is a different album (one
-- artist's self-titled albums), so a row whose
-- group is known and differs is not returned. A row of the file's own group
-- comes first, then one with no group yet, then the one holding the most
-- tracks from the file's folder: a file tagged with no group (a live bonus
-- track beside Weezer's 1994 album) belongs with its folder, not the oldest
-- track beside its album) belongs with its folder, not the oldest
-- row, and a few strays already misfiled do not outvote the folder.
SELECT albums.* FROM albums
WHERE albums.artist_id = sqlc.arg(artist_id) AND albums.title = sqlc.arg(title)
@@ -79,12 +79,10 @@ SELECT * FROM albums ORDER BY random() LIMIT $1;
-- Album "belongs to" a genre if any of its tracks carry that genre.
-- Serves Subsonic getAlbumList?type=byGenre.
--
-- Splits tracks.genre on [;,] as of #367. It previously compared the whole
-- column verbatim, so a track tagged "Rock;Pop" was unreachable from EITHER
-- "Rock" or "Pop" — a Subsonic client asking for a genre silently missed
-- every multi-genre track. This also aligns the endpoint with
-- recommendation.sql / discover.sql, which have always split, and with the
-- genre browse index that #367 adds.
-- Splits tracks.genre on [;,], so a track tagged "Rock;Pop" is reachable from
-- both "Rock" and "Pop"; comparing the whole column would hide every
-- multi-genre track from a Subsonic client asking for a genre. The same split
-- as recommendation.sql, discover.sql and the genre browse index.
--
-- EXISTS rather than JOIN + DISTINCT ON: the lateral split emits one row per
-- (track, genre-fragment), so a join would multiply rows per album and lean
@@ -111,7 +109,7 @@ LIMIT $2 OFFSET $3;
SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%';
-- name: ListRecentlyAddedAlbumsWithArtist :many
-- M6a: recently-added albums joined with artist_name + artist_id for the
-- Recently-added albums joined with artist_name + artist_id for the
-- home-page section. created_at is the row-insert timestamp from the
-- scanner — the closest proxy to "added to library". Stable secondary
-- ordering by id keeps pagination tie-break deterministic.
@@ -122,7 +120,7 @@ ORDER BY albums.created_at DESC, albums.id
LIMIT $1;
-- name: ListAlbumsAlphaWithArtist :many
-- M6a: alpha-sorted album list joined with artist_name. Used by
-- Alpha-sorted album list joined with artist_name. Used by
-- /api/library/albums for the wrapping-grid page. Stable id-tiebreak.
SELECT sqlc.embed(albums), artists.name AS artist_name
FROM albums
@@ -131,7 +129,7 @@ ORDER BY albums.sort_title, albums.id
LIMIT $1 OFFSET $2;
-- name: CountAlbums :one
-- M6a: total album count for the /api/library/albums envelope.
-- Total album count for the /api/library/albums envelope.
SELECT COUNT(*) FROM albums;
-- name: CountAlbumsByArtist :one
@@ -140,7 +138,7 @@ SELECT COUNT(*) FROM albums;
SELECT COUNT(*) FROM albums WHERE artist_id = $1;
-- name: DeleteAlbumIfEmpty :one
-- M7 #372: deletes the album row only when it has no remaining tracks.
-- Deletes the album row only when it has no remaining tracks.
-- Used after a track delete to tidy up orphaned albums. RETURNING gives
-- us the artist_id so the service can chain into the artist-empty check.
-- Returns no rows when the album still has tracks; the service treats
@@ -151,7 +149,7 @@ WHERE a.id = $1
RETURNING a.id, a.artist_id;
-- name: SetAlbumMbidIfNull :exec
-- M7 #379: heal MBID on existing album rows during rescan. Idempotent —
-- Heal MBID on existing album rows during rescan. Idempotent —
-- only updates when the existing mbid is NULL.
UPDATE albums
SET mbid = $2,
@@ -159,7 +157,7 @@ UPDATE albums
WHERE id = $1 AND mbid IS NULL;
-- name: SetAlbumReleaseGroupMbidIfNull :exec
-- #5241: heal the release-group id on an existing album row, from a rescan's
-- Heal the release-group id on an existing album row, from a rescan's
-- tags or from the re-acquisition sweeper's MusicBrainz lookup. Only fills a
-- NULL, so a tag never gets overwritten by a later guess or vice versa.
UPDATE albums
@@ -168,7 +166,7 @@ UPDATE albums
WHERE id = $1 AND release_group_mbid IS NULL;
-- name: GetReleaseGroupForReleaseMbid :one
-- #5241: the release group the library already knows for a release id, so a
-- The release group the library already knows for a release id, so a
-- request carrying a release id can be repaired without asking MusicBrainz.
SELECT release_group_mbid::text
FROM albums
@@ -176,7 +174,7 @@ SELECT release_group_mbid::text
LIMIT 1;
-- name: SetReleaseGroupForReleaseMbidIfNull :exec
-- #5241: cache a MusicBrainz-resolved release group onto the album row that
-- Cache a MusicBrainz-resolved release group onto the album row that
-- holds the release, filling only a NULL.
UPDATE albums
SET release_group_mbid = sqlc.arg(release_group_mbid)::text,
@@ -188,7 +186,7 @@ UPDATE albums
-- one of its tracks' file_path so the worker can re-read tags. LIMIT
-- supplied by caller for batching/progress purposes. The track read is
-- one still on disk; an album with none left is skipped until a scan
-- finds its files again (#5139).
-- finds its files again.
SELECT a.id AS album_id,
a.artist_id AS artist_id,
a.title AS title,
@@ -207,7 +205,7 @@ SELECT a.id AS album_id,
LIMIT $1;
-- name: GetAlbumCoverageRollup :one
-- M7 coverage-gauge: library-wide cover-art coverage rollup for the
-- Library-wide cover-art coverage rollup for the
-- admin gauge. Single sequential scan with FILTER aggregates —
-- sub-millisecond on realistic libraries. pending_no_mbid is a SUBSET
-- of pending; the UI surfaces it separately so operators can see how
@@ -217,12 +215,9 @@ SELECT a.id AS album_id,
-- Invariant: with_art + pending + settled = total. (pending_no_mbid is
-- not part of the sum — it's a subset of pending, surfaced separately.)
--
-- The IN list below must stay in sync with the cover_art_source CHECK
-- constraint — currently relaxed by migration 0020 to include 'deezer'
-- and 'lastfm', and migration 0030 further relaxed it to any non-empty
-- string. If a new source value is added without updating this query,
-- with_art will silently undercount. Drift #556 caught the deezer +
-- lastfm omission introduced by migration 0020.
-- The IN list below must name every source the enrichers write (the
-- cover_art_source CHECK accepts any non-empty string since migration
-- 0030). A source missing here makes with_art silently undercount.
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE cover_art_source IN
@@ -233,7 +228,7 @@ SELECT
FROM albums;
-- name: SetAlbumCoverWithVersion :exec
-- M7 cover-sources: records a successful or settled enrichment with the
-- Records a successful or settled enrichment with the
-- current sources_version stamp. cover_art_path is NULL when source is
-- 'none' (no file written). Atomic; called per album per pass.
UPDATE albums
@@ -246,5 +241,5 @@ UPDATE albums
-- name: GetAlbumsByIDs :many
-- Batched lookup used by /api/library/sync to hydrate upsert payloads
-- (#357). Mirror of GetArtistsByIDs.
--. Mirror of GetArtistsByIDs.
SELECT * FROM albums WHERE id = ANY($1::uuid[]);
+6 -6
View File
@@ -46,7 +46,7 @@ SELECT COUNT(*) FROM artists WHERE name ILIKE '%' || $1::text || '%';
SELECT * FROM artists WHERE id = ANY($1::uuid[]);
-- name: ListArtistsAlphaWithCovers :many
-- M6a: alpha-sorted artist list with derived cover_album_id (most-recent
-- Alpha-sorted artist list with derived cover_album_id (most-recent
-- album whose cover_art_path is set). Replaces ListArtistsAlpha for the
-- /api/artists?sort=alpha branch — the new column is appended, so the
-- alpha branch is purely additive on the wire. album_count is computed
@@ -68,7 +68,7 @@ ORDER BY artists.sort_name, artists.name, artists.id
LIMIT $1 OFFSET $2;
-- name: DeleteArtistIfEmpty :one
-- M7 #372: deletes the artist row only when it has no remaining albums
-- Deletes the artist row only when it has no remaining albums
-- AND no remaining tracks (defensive — tracks can in principle exist
-- without an album, though the scanner doesn't write that shape today).
DELETE FROM artists a
@@ -78,7 +78,7 @@ WHERE a.id = $1
RETURNING a.id;
-- name: SetArtistMbidIfNull :exec
-- M7 #379: heal MBID on existing artist rows during rescan. Idempotent —
-- Heal MBID on existing artist rows during rescan. Idempotent —
-- only updates when the existing mbid is NULL, so concurrent scans don't
-- overwrite an existing value with a stale one.
UPDATE artists
@@ -87,7 +87,7 @@ UPDATE artists
WHERE id = $1 AND mbid IS NULL;
-- name: ListArtistsMissingArt :many
-- M7 cover-sources: returns artist rows the artist-art enricher should
-- Returns artist rows the artist-art enricher should
-- attempt. Eligible: artist_art_source IS NULL OR (= 'none' AND stale
-- artist_art_sources_version). MBID-less artists are returned but the
-- enricher skips them at the row.mbid == nil guard inside the chain.
@@ -99,7 +99,7 @@ SELECT a.id
LIMIT $2;
-- name: SetArtistArtWithVersion :exec
-- M7 cover-sources: records a successful or settled artist-art
-- Records a successful or settled artist-art
-- enrichment with the current sources_version stamp. thumb_path /
-- fanart_path are NULL on a 'none' result, or independently NULL if
-- the provider returned only one of the two image types (e.g.
@@ -113,7 +113,7 @@ UPDATE artists
WHERE id = $1;
-- name: ClearArtistArtNone :exec
-- M7 cover-sources: parallel of ClearAlbumCoverNone. Used by the
-- Parallel of ClearAlbumCoverNone. Used by the
-- eligibility loop to flip stale 'none' rows back to NULL.
UPDATE artists
SET artist_art_source = NULL,
+1 -1
View File
@@ -15,7 +15,7 @@ SELECT count(*)::bigint FROM audit_log
-- name: ListAuditLogByActions :many
-- One page of the given actions, newest first: the duplicates report's
-- "resolved automatically" activity (M498) reads the resolver's merges from
-- "resolved automatically" activity reads the resolver's merges from
-- here.
SELECT * FROM audit_log
WHERE action = ANY(sqlc.arg(actions)::text[])
+16 -23
View File
@@ -1,29 +1,23 @@
-- Every query in this file filters `tracks.missing_since IS NULL` (#2523).
-- A row whose file has vanished keeps its genre forever — the scanner walks the
-- filesystem, so it never revisits a path that no longer exists — which is how
-- pre-#2499 welded genres survived a full re-scan and kept showing in the index.
-- Browsing is a way of finding something to play, so a track that cannot play
-- Every query in this file filters `tracks.missing_since IS NULL`. A row whose
-- file has vanished keeps its genre forever — the scanner walks the
-- filesystem, so it never revisits a path that no longer exists — and
-- browsing is a way of finding something to play, so a track that cannot play
-- should not shape it.
--
-- The year axis filters too (#2702). It used to join albums only, on the
-- reasoning that an album is a real release even when some of its tracks are
-- gone — true, but it left the two browse axes disagreeing: the genre queries
-- above filter tracks.missing_since, so an album whose every file had vanished
-- was already absent from genre while still listed under its year, where
-- opening it found nothing playable.
-- The year axis filters too, so the two axes agree: an album whose every file
-- has vanished is absent from both, rather than absent from its genre and
-- still listed under its year, where opening it would find nothing playable.
--
-- Hiding is the answer rather than showing-and-marking because browsing is how
-- you go looking for something to play, and the operator's rule for that case
-- is to take it out of view; the admin missing-files surface is where absence
-- is reported. It also means changing the axis that was inconsistent rather
-- than the one that was already right.
-- Hiding rather than showing-and-marking, because browsing is how you go
-- looking for something to play; the admin missing-files surface is where
-- absence is reported.
--
-- The predicate is "has at least one playable track", so it also excludes an
-- album with no tracks at all. That is the same answer for the same reason:
-- nothing to play, nothing to browse to.
-- name: ListGenresWithCount :many
-- Genre browse index (#367).
-- Genre browse index.
--
-- Genres live inline on tracks.genre as a delimited string, so this splits on
-- the same [;,] pattern already used by recommendation.sql and discover.sql —
@@ -34,14 +28,14 @@
-- trim() but deliberately NO lower(): trimming repairs an artifact of OUR
-- splitting ("Rock; Pop" yields " Pop", and showing that as a distinct genre
-- would be a bug), whereas case is what the tag actually says. Raw ID3 is
-- exposed as-is for v1, so "Rock" and "rock" appear as separate rows.
-- exposed as-is, so "Rock" and "rock" appear as separate rows.
--
-- COUNT(DISTINCT) because a sloppy tag like "Rock;Rock" would otherwise
-- inflate its own row.
--
-- Ordered by count first: raw ID3 data has a long tail of one-off junk tags,
-- so alphabetical would bury the handful of genres an operator actually has a
-- library's worth of. Name breaks ties for a stable order.
-- so alphabetical would bury the handful of genres a library actually has
-- a lot of. Name breaks ties for a stable order.
SELECT trim(g.genre) AS genre, COUNT(DISTINCT tracks.id)::bigint AS track_count
FROM tracks
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
@@ -87,7 +81,7 @@ WHERE EXISTS (
);
-- name: ListAlbumYearsWithCount :many
-- Year browse index (#367). Only albums with a release_date appear — an
-- Year browse index. Only albums with a release_date appear — an
-- album with no date isn't "year unknown" as a browsable bucket, it's absent
-- from this axis, and the UI says so rather than inventing a 0 row.
-- Newest first: recent releases are the likelier browse target.
@@ -132,8 +126,7 @@ WHERE release_date IS NOT NULL
-- name: ListGenresForAlbum :many
-- Distinct genres carried by an album's tracks, for the album detail page's
-- quick-jump chips. Split and trimmed identically to ListGenresWithCount, so a
-- chip always leads to a page that actually contains this album — the two
-- diverging is exactly the bug #367 had to fix in ListAlbumsByGenre.
-- chip always leads to a page that actually contains this album.
SELECT DISTINCT trim(g.genre) AS genre
FROM tracks
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
@@ -1,4 +1,4 @@
-- Folksonomy tags for out-of-library Discover candidates (#2376). Parallel to
-- Folksonomy tags for out-of-library Discover candidates. Parallel to
-- track_tags.sql, but keyed by MBID because the artist has no local row. See
-- 0050_candidate_artist_tags.up.sql for why the bookkeeping is its own table.
@@ -63,7 +63,7 @@ ON CONFLICT (candidate_mbid) DO UPDATE
updated_at = now();
-- name: ListCandidateArtistTagsForMbids :many
-- Cached tags for a set of candidates, for slice 6's taste-overlap ranking.
-- Cached tags for a set of candidates, for the taste-overlap ranking.
-- One row per (candidate, tag).
SELECT candidate_mbid, tag, weight
FROM candidate_artist_tags
+1 -1
View File
@@ -1,4 +1,4 @@
-- The release catalogue read from MusicBrainz (M502 #5519).
-- The release catalogue read from MusicBrainz.
-- name: UpsertMBArtist :exec
INSERT INTO mb_artists (mbid, name, sort_name, disambiguation, artist_type, country)
+1 -1
View File
@@ -1,4 +1,4 @@
-- Household co-play similarity (#1533, milestone #160 Opt 5). Minstrel is a
-- Household co-play similarity. Minstrel is a
-- single shared-library, multi-user server (no per-user library ACL), so the
-- "household" is the whole instance's user set. These queries recompute
-- artist–artist co-occurrence edges from play_events and store them in
+2 -2
View File
@@ -1,4 +1,4 @@
-- M7 cover-sources: queries for the cover_art_provider_settings table
-- Queries for the cover_art_provider_settings table
-- and the cover_art_sources_meta singleton. The SettingsService
-- consumes these.
@@ -13,7 +13,7 @@ SELECT provider_id, enabled, api_key, display_order, created_at, updated_at
-- name: UpsertProviderSettings :exec
-- INSERT a default row for a newly-registered provider, or update an
-- existing row's display_order on boot reconciliation. Does NOT
-- overwrite enabled / api_key on conflict — the operator's settings
-- overwrite enabled / api_key on conflict — an admin's settings
-- persist across restarts.
INSERT INTO cover_art_provider_settings (provider_id, enabled, display_order)
VALUES ($1, $2, $3)
+3 -3
View File
@@ -1,7 +1,7 @@
-- M7 #353: album cover enrichment queries.
-- Album cover enrichment queries.
-- name: ListAlbumsMissingCover :many
-- M7 cover-sources: returns rows the enricher should attempt. Eligible:
-- Returns rows the enricher should attempt. Eligible:
-- cover_art_source IS NULL (never tried), OR cover_art_source = 'none'
-- AND cover_art_sources_version != current (settled under an older
-- source set; recheck against the current chain). LIMIT supplied by
@@ -53,7 +53,7 @@ UPDATE albums
WHERE id = $1;
-- name: ClearAlbumCoverNone :exec
-- M7 #380: when MBID backfill heals an album, clear its 'none' cover_art_source
-- When MBID backfill heals an album, clear its 'none' cover_art_source
-- back to NULL so the enricher's next batch retries the MBCAA fetch.
-- Idempotent — only updates rows currently stamped 'none'.
UPDATE albums
+14 -14
View File
@@ -29,7 +29,7 @@ dormant_artists AS (
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
JOIN dormant_artists da ON da.id = t.artist_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM play_events pe
WHERE pe.user_id = $1
@@ -56,12 +56,12 @@ SELECT t.id, t.album_id, t.artist_id
--
-- GROUP BY (not SELECT DISTINCT) so the md5 ORDER BY expression need
-- not appear in the select list — Postgres rejects DISTINCT + ORDER BY
-- by-expression at plan time (SQLSTATE 42P10), which previously caused
-- the entire Discover build to fail silently.
-- by-expression at plan time (SQLSTATE 42P10), which would fail the
-- entire Discover build.
SELECT t.id, t.album_id, t.artist_id
FROM general_likes gl
JOIN tracks t ON t.id = gl.track_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND gl.user_id != $1
AND NOT EXISTS (
SELECT 1 FROM play_events pe
@@ -88,7 +88,7 @@ SELECT t.id, t.album_id, t.artist_id
-- $1 = user_id, $2 = date string for md5 ordering.
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM play_events pe
WHERE pe.user_id = $1
@@ -108,7 +108,7 @@ SELECT t.id, t.album_id, t.artist_id
-- name: ListTasteUnheardTracksForDiscover :many
-- Taste-targeted novelty: unheard tracks whose genres overlap the user's
-- taste-profile tags (taste_profile_tags, #796), ranked by summed tag
-- taste-profile tags (taste_profile_tags), ranked by summed tag
-- weight — "new to you, but your vibe" rather than the crude random arm.
-- Genres live inline on tracks.genre as a delimited string, split the
-- same way the radio tag_overlap arm does (regexp_split_to_table on
@@ -116,13 +116,13 @@ SELECT t.id, t.album_id, t.artist_id
-- when the user has no taste tags yet (cold start), so the caller
-- redistributes its slots to the other buckets. Stamped 'taste_unheard'.
--
-- The per-album and per-artist caps apply BEFORE the LIMIT (#5356). Summed
-- weight rewards a track for carrying many of the user's tags, so a few
-- artists whose every track is tagged with the whole profile take every row
-- of a plain LIMIT; the caller's caps then left 6 of 120 on the deploy, and
-- the best-performing arm handed its slots to the others. Ranking within
-- album, then within artist over what the album cap kept, is the same walk
-- capByAlbumAndArtist makes, so the caller's caps keep everything here.
-- The per-album and per-artist caps apply BEFORE the LIMIT. Summed weight
-- rewards a track for carrying many of the user's tags, so a few artists
-- whose every track is tagged with the whole profile would take every row of
-- a plain LIMIT, the caller's caps would then leave a handful, and the arm
-- would hand its slots to the others. Ranking within album, then within
-- artist over what the album cap kept, is the same walk capByAlbumAndArtist
-- makes, so the caller's caps keep everything here.
WITH scored AS (
SELECT t.id, t.album_id, t.artist_id,
SUM(nt.weight) AS weight,
@@ -130,7 +130,7 @@ WITH scored AS (
FROM tracks t
JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true
JOIN taste_profile_tags nt ON nt.user_id = sqlc.arg(user_id) AND trim(g_split.g) = nt.tag
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND nt.weight > 0
AND trim(g_split.g) <> ''
AND NOT EXISTS (
+10 -10
View File
@@ -55,14 +55,14 @@ SELECT t.id, t.duration_ms, f.audio_stream_sha256, f.chromaprint
AND f.chromaprint IS NOT NULL
-- Only chromaprints taken at the current length: prints at two lengths are not
-- comparable, and after a length change the backfill is still re-deriving the
-- rest (#3913).
-- rest.
AND f.chromaprint_length_sec = sqlc.arg(chromaprint_length_sec)
AND (t.duration_ms, t.id) > (sqlc.arg(after_duration_ms)::integer, sqlc.arg(after_id)::uuid)
ORDER BY t.duration_ms, t.id
LIMIT sqlc.arg(page_limit);
-- name: ListDismissedDuplicateMemberSets :many
-- What the operator has already said are not duplicates. A new proposal whose
-- What an admin has already said are not duplicates. A new proposal whose
-- every member sat together in one of these is not proposed again.
SELECT g.id, array_agg(m.track_id ORDER BY m.track_id)::uuid[] AS track_ids
FROM duplicate_groups g
@@ -113,8 +113,8 @@ SELECT count(*)::bigint
AND (SELECT count(*) FROM duplicate_group_members m WHERE m.group_id = g.id) >= 2;
-- name: CountPendingDuplicateGroupsInView :one
-- CountPendingDuplicateGroups narrowed to one view of the report (M498):
-- review what the operator still has to decide: every class but
-- CountPendingDuplicateGroups narrowed to one view of the report:
-- review what an admin still has to decide: every class but
-- cross_release, and groups not yet classified
-- cross_release the same song on different releases, which is kept, not merged
SELECT count(*)::bigint
@@ -130,7 +130,7 @@ SELECT count(*)::bigint
-- One page of proposals in one view (see CountPendingDuplicateGroupsInView),
-- newest first, flattened to one row per member so the handler folds them
-- without a query per group. What each copy carries — likes and plays from
-- every user — is here because it is what the operator weighs when deciding
-- every user — is here because it is what an admin weighs when deciding
-- which copy to keep.
WITH page AS (
SELECT g.id, g.tier, g.worst_bit_error_rate, g.detected_at, g.class, g.resolve_note
@@ -186,19 +186,19 @@ UPDATE duplicate_groups
WHERE id = sqlc.arg(id) AND status = 'pending';
-- name: ListDuplicateGroupsForResolve :many
-- Every pending group's present copies, for the resolver (M498 #5435): what it
-- Every pending group's present copies, for the resolver: what it
-- needs to classify the group and to choose the copy to keep. One row per copy,
-- grouped by group. position_clash is true when another present track on the
-- same album claims the same disc and track number: tags that do not fit the
-- album, which the survivor rule weighs against a copy. source_verdict says
-- whether the copy is a video rip ('suspect') or one the operator kept ('fine').
-- whether the copy is a video rip ('suspect') or one an admin kept ('fine').
-- prior_class is the class the last pass gave the group, NULL for a group no
-- pass has seen. release_group_mbid is the album's MusicBrainz group, from its
-- tags or the library listing's match; '' when neither names one.
-- acoustid_state and acoustid_recording are what AcoustID said of the file,
-- when it was looked up (#6169: which song one recording cut to several slots
-- is). group_id narrows it to one group, for the operator settling it (#6184);
-- NULL reads them all.
-- when it was looked up: which song a recording cut to several slots is.
-- group_id narrows it to one group, for an admin settling it; NULL reads them
-- all.
SELECT g.id AS group_id,
g.tier,
g.worst_bit_error_rate,
+2 -2
View File
@@ -26,7 +26,7 @@ LIMIT 1;
-- name: InsertPlayEvent :one
-- pick_kind is non-NULL only for system-playlist plays whose track was
-- found (with a stamped kind) in the user's live snapshot for that
-- variant at ingestion time (#1249, generalized in #1270).
-- variant at ingestion time.
INSERT INTO play_events (
user_id, track_id, session_id, started_at, client_id, source, pick_kind,
device_class
@@ -38,7 +38,7 @@ RETURNING *;
-- Looks a track up in the user's CURRENT snapshot of the given system
-- variant and returns its pick_kind. Used at play-ingestion time to
-- freeze provenance onto the play_event — snapshots rebuild daily, so
-- attribution can't be reconstructed at read time (#1249/#1270). No
-- attribution can't be reconstructed at read time. No
-- row = track not in today's snapshot (caller stores NULL); a row with
-- NULL pick_kind = the variant doesn't stamp (yet). songs_like_artist
-- is non-singleton (up to 3 mixes/user); a track in two of them takes
+3 -3
View File
@@ -1,6 +1,6 @@
-- name: UpsertTrackFingerprint :exec
-- Written whenever a track's fingerprint is derived: by the scan when a file is
-- new or its bytes changed, and by the backfill (#3908) for rows derived by an
-- new or its bytes changed, and by the backfill for rows derived by an
-- older method. Replaces the row wholesale — a fingerprint of the old bytes has
-- no standing once the file has changed.
INSERT INTO track_fingerprints (
@@ -23,7 +23,7 @@ ON CONFLICT (track_id) DO UPDATE SET
DELETE FROM track_fingerprints WHERE track_id = $1;
-- name: ListTracksNeedingFingerprint :many
-- The backfill's work queue (#3908): tracks with no fingerprint, or one derived
-- The backfill's work queue: tracks with no fingerprint, or one derived
-- by an older method. Keyset-paged on id so a pass visits each track at most
-- once. That cursor is load-bearing: an inconclusive attempt writes no row, so
-- without it a file that keeps timing out would be listed again straight away
@@ -34,7 +34,7 @@ SELECT t.id, t.file_path
LEFT JOIN track_fingerprints f ON f.track_id = t.id
WHERE t.missing_since IS NULL
-- A row taken at another length is as stale as one from an older method:
-- chromaprints at two lengths cannot be compared (#3913).
-- chromaprints at two lengths cannot be compared.
AND (f.track_id IS NULL
OR f.fingerprint_version < sqlc.arg(current_version)
OR f.chromaprint_length_sec <> sqlc.arg(chromaprint_length_sec))
+9 -11
View File
@@ -1,11 +1,10 @@
-- Background garbage-collector / lifecycle queries. All five address
-- drift findings from the 2026-06-02 audit (Scribe parent #552):
-- #565 #566 #567 #574 #575. Sweeper runs on a long tick (default 1h)
-- Background garbage-collector / lifecycle queries: rows nothing else closes
-- or removes. Sweeper runs on a long tick (default 1h)
-- so per-query cost is amortised; each is idempotent (re-running on
-- already-closed/-deleted rows is a no-op).
-- name: GcCloseStalePlayEvents :execrows
-- #566: play_events rows opened more than 24h ago that never got a
-- Play_events rows opened more than 24h ago that never got a
-- play_ended. Client crashed mid-track, network dropped, etc. We
-- synthesize ended_at = started_at + duration_played_ms when present,
-- otherwise leave duration_played_ms null and stamp ended_at = now()
@@ -21,10 +20,9 @@ UPDATE play_events
AND started_at < now() - INTERVAL '24 hours';
-- name: GcClosePlaySessionsWithNoRecentEvents :execrows
-- #565: play_sessions.ended_at was added but never populated by any
-- writer. Close sessions whose last_event_at is older than 6h —
-- treating that as "user moved on" the same way audio_service does
-- after grace periods. Sessions with NO events (track_count = 0)
-- play_sessions.ended_at has no other writer. Close sessions whose
-- last_event_at is older than 6h — treating that as "user moved on" the
-- same way audio_service does after grace periods. Sessions with NO events (track_count = 0)
-- older than 1h are also closed (stale handshakes from clients that
-- never recorded a play). ended_at is set to last_event_at so the
-- session's duration reads naturally.
@@ -38,7 +36,7 @@ UPDATE play_sessions
);
-- name: GcExpireScrobbleQueueFailedRows :execrows
-- #567: scrobble_queue rows that have been in status='failed' for
-- Scrobble_queue rows that have been in status='failed' for
-- more than 14 days. The worker stops retrying after maxAttempts;
-- failed rows accumulate forever otherwise. CASCADE from play_events
-- already drops the row when the underlying event is deleted, so this
@@ -48,7 +46,7 @@ DELETE FROM scrobble_queue
AND enqueued_at < now() - INTERVAL '14 days';
-- name: GcResetStuckSystemPlaylistRuns :execrows
-- #574: system_playlist_runs.in_flight = true can wedge on a
-- System_playlist_runs.in_flight = true can wedge on a
-- goroutine panic between SET in_flight=true and SET in_flight=false.
-- The duplicate-prevention check refuses to start a fresh regen while
-- in_flight, so a stuck row blocks all future regens for that user.
@@ -61,7 +59,7 @@ UPDATE system_playlist_runs
AND last_run_at < now() - INTERVAL '10 minutes';
-- name: GcDeleteExpiredPasswordResets :execrows
-- #575: password_resets accumulates expired + used rows forever. The
-- Password_resets accumulates expired + used rows forever. The
-- validation path already rejects them; this just keeps the table
-- from growing unbounded. Used rows are kept for 7 days for audit;
-- unused expired rows go immediately.
+1 -1
View File
@@ -1,5 +1,5 @@
-- name: ListUserHistory :many
-- M7 #365: track-level listening history. Returns one row per
-- Track-level listening history. Returns one row per
-- play_events entry, newest first, joined with tracks/albums/artists
-- so the SPA can render a TrackRef without follow-up fetches. Filters
-- out skipped events and tracks the user has quarantined (per-user
+5 -5
View File
@@ -1,4 +1,4 @@
-- Import with verification (M502 #5522).
-- Import with verification.
-- name: ListLiveGrabs :many
-- Every download still running or waiting on its import, oldest first.
@@ -52,7 +52,7 @@ UPDATE grabs
import_forced = false,
imported_at = CASE WHEN sqlc.arg(import_state)::text = 'imported' THEN now() ELSE imported_at END,
-- The files were copied, so an imported download goes on sharing
-- until the lifecycle removes it (#5523).
-- until the lifecycle removes it.
seeding = sqlc.arg(import_state)::text = 'imported',
imported_files = sqlc.arg(imported_files),
destination = sqlc.arg(destination),
@@ -114,7 +114,7 @@ SELECT DISTINCT COALESCE(t.disc_number, 1)::int AS disc, t.track_number::int AS
-- name: ListGrabs :many
-- The Wanted view's Activity tab: downloads waiting on a person first, then
-- the rest still running, then the ended ones newest first. Each carries
-- what is wrong with it now, if anything (#5523).
-- what is wrong with it now, if anything.
SELECT g.id, g.want_id, g.name, g.indexer, g.client, g.quality, g.origin, g.forced,
g.grabbed_at, g.ended_at, g.end_reason, g.end_detail, g.progress, g.import_state,
g.import_reason, g.import_detail, g.import_updated_at, g.imported_at, g.imported_files,
@@ -142,7 +142,7 @@ SELECT b.info_hash, b.name, b.reason, b.detail, b.blocked_at, b.expires_at, w.ti
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
-- name: ListUpgradeTracks :many
-- The tracks an upgrade may replace (#5524): the present tracks of the album
-- The tracks an upgrade may replace: the present tracks of the album
-- the want holds.
SELECT id, file_path, mbid, COALESCE(disc_number, 1)::int AS disc, track_number,
file_format, file_size, duration_ms, source_verdict
@@ -167,7 +167,7 @@ UPDATE tracks SET source_verdict = NULL, source_verdict_at = NULL
-- name: ListImportedGrabsHolding :many
-- The downloads Minstrel imported into the folder holding path: the releases a
-- file there came from, to block when the files proved not to be the album
-- (#6169). A file imported before Minstrel downloaded anything has none.
--. A file imported before Minstrel downloaded anything has none.
SELECT id, info_hash, name, want_id
FROM grabs
WHERE import_state = 'imported'
+4 -5
View File
@@ -3,11 +3,10 @@ INSERT INTO library_changes (entity_type, entity_id, op)
VALUES ($1, $2, $3);
-- name: InsertLibraryChanges :exec
-- Batch form, for a mutation that touches many rows at once (#2704: the scan's
-- reconcile pass can mark up to a quarter of a library missing in one go).
-- Every caller before this one changed a single entity, so per-row inserts
-- were the right shape; a loop here would be thousands of round-trips inside
-- a scan that is already the slow path.
-- Batch form, for a mutation that touches many rows at once (the scan's
-- reconcile pass can mark up to a quarter of a library missing in one go). A
-- loop of single inserts would be thousands of round-trips inside a scan that
-- is already the slow path.
INSERT INTO library_changes (entity_type, entity_id, op)
SELECT sqlc.arg(entity_type)::text,
unnest(sqlc.arg(entity_ids)::text[]),
+1 -1
View File
@@ -8,7 +8,7 @@ UPDATE library_settings
WHERE id = true
RETURNING *;
-- A library folder that moved (#6129): every path under old_root is rewritten
-- A library folder that moved: every path under old_root is rewritten
-- to the same place under new_root, so the tracks keep their ids and with them
-- their plays, likes and playlists. updated_at is left alone on purpose: the
-- scan compares it with each file's mtime, and the files have not changed.
+2 -2
View File
@@ -1,4 +1,4 @@
-- The download lifecycle (M502 #5523): the intervention ledger, giving a
-- The download lifecycle: the intervention ledger, giving a
-- download up, and seeding after import.
-- name: RaiseIntervention :one
@@ -79,7 +79,7 @@ RETURNING *;
-- name: CountGoneGrabs :one
-- How many of a release's grabs ended gone since a time: a release that keeps
-- leaving its client is held back rather than taken on every pass (#6176).
-- leaving its client is held back rather than taken on every pass.
SELECT count(*) FROM grabs
WHERE info_hash = sqlc.arg(info_hash) AND end_reason = 'gone' AND ended_at > sqlc.arg(since);
+1 -1
View File
@@ -1,5 +1,5 @@
-- name: LikeTrack :many
-- A like is on the song (M498 #5438): every copy of it sharing the track's
-- A like is on the song: every copy of it sharing the track's
-- song_key is liked. Returns the tracks newly liked, for the sync log; empty
-- when the song was already liked.
INSERT INTO general_likes (user_id, track_id)
+3 -3
View File
@@ -1,5 +1,5 @@
-- name: UpsertTrackLoudness :exec
-- Written when the backfill measures a track (#4995). Replaces the row
-- Written when the backfill measures a track. Replaces the row
-- wholesale: a measurement of the old bytes has no standing once the file has
-- changed.
INSERT INTO track_loudness (
@@ -78,7 +78,7 @@ UPDATE loudness_settings
RETURNING *;
-- name: ListAlbumsNeedingLoudness :many
-- The album pass's work queue (#4996): albums whose present tracks or their
-- The album pass's work queue: albums whose present tracks or their
-- measurements have changed since album loudness was last computed, or that
-- never had it. The digest is over every present track's id and the
-- measurement it holds at the current version ('-' for none), so a track
@@ -154,7 +154,7 @@ SELECT integrated_lufs, true_peak_dbtp FROM album_loudness WHERE album_id = $1;
-- name: GetReplayGainByTrackIDs :many
-- Track and album loudness for a set of tracks, for every surface that hands
-- gains to a client (#4997). A measurement from an older analysis version is
-- gains to a client. A measurement from an older analysis version is
-- still delivered: it is a better gain than none until the backfill redoes it.
SELECT t.id,
tl.integrated_lufs AS track_lufs,
+1 -1
View File
@@ -1,4 +1,4 @@
-- Duplicate merge (Scribe #3911). Every statement here runs inside the one
-- Duplicate merge. Every statement here runs inside the one
-- transaction library.MergeDuplicateGroup opens, after the removed copy's file
-- is already gone. The loser's own track row is deleted last with DeleteTrack;
-- what these do is move everything it carries onto the survivor first, so that
+8 -8
View File
@@ -1,26 +1,26 @@
-- Missing-pair pass (M485). A track marked missing whose replacement is already
-- Missing-pair pass. A track marked missing whose replacement is already
-- on disk under another row — most often a quality upgrade, mp3 → flac at a new
-- path — is folded into that replacement, so its history moves across and
-- re-acquisition stops chasing music the library already has.
--
-- Move adoption (#2528) cannot catch these: it links a new file to a missing row
-- Move adoption cannot catch these: it links a new file to a missing row
-- at the moment the file is first inserted, and the replacement arrives as a
-- re-encode with no recording MBID, so it has nothing to match on. AcoustID may
-- give it an MBID later; by then both rows exist and only a fold can join them.
-- name: ListMissingTrackPairs :many
-- Every candidate (missing, present) pair, with how many candidates each side
-- has. Two rules, measured on the operator's library on 2026-10-07 (#5274):
-- has. Two rules:
--
-- mbid the same recording MBID, within the album group (the album row, or
-- albums sharing a release group). 142 of 359 pairs.
-- title the same album row and the same title, ignoring case. 213 more. The
-- albums sharing a release group).
-- title the same album row and the same title, ignoring case. The
-- replacements are often not the original file — another edition,
-- re-encoded, untagged — so the metadata is what is left to go on.
--
-- Deliberately NOT gated on duration or track position. On the 142 pairs known
-- to be one recording, 18% differed by over 2s (old mp3 durations are poor
-- estimates), and the poorly tagged set is exactly where numbering is broken.
-- Deliberately NOT gated on duration or track position. Pairs known to be one
-- recording often differ by over 2s (old mp3 durations are poor estimates),
-- and the poorly tagged set is exactly where numbering is broken.
--
-- A pair whose MBIDs are both set and differ is not a title match: the tags say
-- they are different recordings. The caller folds a pair only when each side has
+7 -7
View File
@@ -1,4 +1,4 @@
-- M489 notifications inbox (#726). Rows are written only through
-- Notifications inbox. Rows are written only through
-- internal/notifications.Notifier, which decides recipients and honours each
-- recipient's inbox preference before it reaches these.
@@ -18,10 +18,10 @@ RETURNING id;
-- ten. Every coalesced kind's event is "N more happened", never a restated
-- total, so the row always says what is new since it was last read.
--
-- created_at and emailed_at are left alone (M542): an update changes what the
-- notice says, not when it arrived. Moving created_at made the phone announce
-- it again and clearing emailed_at sent it again, so a notice the hourly
-- duplicate pass kept growing alerted every hour.
-- created_at and emailed_at are left alone: an update changes what the
-- notice says, not when it arrived. Moving created_at would make the phone
-- announce it again and clearing emailed_at would send it again, so a notice
-- that keeps growing would alert on every update.
INSERT INTO user_notifications (user_id, kind, payload, coalesce_key, emailed_at)
VALUES (sqlc.arg(user_id), sqlc.arg(kind), sqlc.arg(payload), sqlc.arg(coalesce_key),
CASE WHEN sqlc.arg(email_wanted)::boolean THEN NULL ELSE now() END)
@@ -66,7 +66,7 @@ UPDATE user_notifications
-- all read" queued offline and replayed later must not mark notices that
-- arrived in between, which the user never saw. A coalesced row that grew
-- since then is marked read with it: it arrived before they asked, and its
-- growth is a quiet update to a notice they had seen (M542).
-- growth is a quiet update to a notice they had seen.
UPDATE user_notifications
SET read_at = now()
WHERE user_id = sqlc.arg(user_id)
@@ -104,7 +104,7 @@ ON CONFLICT (user_id, kind) DO UPDATE SET
email = EXCLUDED.email,
updated_at = now();
-- Email digest (#5346) ------------------------------------------------------
-- Email digest ------------------------------------------------------
-- name: ListEmailPendingNotifications :many
-- Every unread, un-emailed row of a user who has an address, oldest first.
+3 -3
View File
@@ -55,7 +55,7 @@ RETURNING id, cover_path;
-- (track was removed from the library). The denormalized snapshot fields
-- on playlist_tracks remain authoritative for title/artist/album text.
--
-- Deliberately NOT filtered on missing_since (#2527), unlike every browse /
-- Deliberately NOT filtered on missing_since, unlike every browse /
-- discover / mix query. A playlist entry is something the user put here on
-- purpose, so a missing file stays in the list and renders as a dead row
-- rather than silently vanishing; missing_since rides along so the service
@@ -76,7 +76,7 @@ ORDER BY pt.position;
-- Inserts at the next available position. Snapshot fields are copied
-- from the tracks/albums/artists join at insert time. tracks.duration_ms
-- is converted to seconds for the snapshot. pick_kind is NULL for manual
-- playlists; the For You builder stamps 'taste'/'fresh' (#1249).
-- playlists; the For You builder stamps 'taste'/'fresh'.
INSERT INTO playlist_tracks (playlist_id, position, track_id, title, artist_name, album_title, duration_sec, pick_kind)
SELECT
sqlc.arg(playlist_id)::uuid,
@@ -120,5 +120,5 @@ LIMIT $2;
-- name: GetPlaylistsByIDs :many
-- Batched lookup used by /api/library/sync to hydrate upsert payloads
-- (#357). Mirror of GetArtistsByIDs.
--. Mirror of GetArtistsByIDs.
SELECT * FROM playlists WHERE id = ANY($1::uuid[]);
+8 -8
View File
@@ -1,6 +1,6 @@
-- Re-acquisition of missing files (milestone #290, reworked for M502 #5520).
-- Re-acquisition of missing files.
-- The unit is the album: an album whose files went missing becomes a want for
-- its release group, and the search schedule (#5521) looks for it.
-- its release group, and the search schedule looks for it.
-- name: GetReacquisitionSettings :one
SELECT * FROM reacquisition_settings WHERE id = true;
@@ -20,17 +20,17 @@ RETURNING *;
--
-- * it still has at least one track whose file has been missing longer than
-- the grace window. Measured on missing_since, which reconcile never
-- re-stamps (#2523), so it is a genuine "gone since" clock;
-- re-stamps, so it is a genuine "gone since" clock;
-- * MusicBrainz can name it: the artist has an MBID, and the album a
-- release MBID or a release group. The release MBID is only ever used to
-- find the group, and 238 albums matched to a group carried no release
-- id, so their losses were never wanted back (#6167). Albums failing this
-- are counted separately (CountAlbumsMissingWithoutMbid);
-- * it is on the wanted list (#5567): an album taken off the list, or
-- find the group, and an album matched to a group may carry no release
-- id. Albums failing this are counted separately
-- (CountAlbumsMissingWithoutMbid);
-- * it is on the wanted list: an album taken off the list, or
-- dropped into the library by hand, is not fetched back;
-- * no want already covers it: none open, and none held since its files
-- went missing. A want held after the loss means the library has a full
-- copy despite the missing rows (#5245), so asking again would loop.
-- copy despite the missing rows, so asking again would loop.
SELECT albums.id AS album_id,
albums.title AS album_title,
albums.mbid AS album_mbid,
+42 -49
View File
@@ -24,7 +24,7 @@ LEFT JOIN LATERAL (
WHERE user_id = $1 AND track_id = t.id
) pe ON true
WHERE t.id <> $2
AND t.missing_since IS NULL -- #2523: never offer a file that is gone
AND t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM play_events
WHERE user_id = $1 AND track_id = t.id
@@ -36,25 +36,22 @@ WHERE t.id <> $2
);
-- name: LoadRadioCandidatesV2 :many
-- M4c: similarity-driven candidate pool. 6-way UNION:
-- Similarity-driven candidate pool. 6-way UNION:
-- $1 user_id, $2 seed_track_id, $3 recently_played_hours,
-- $4 exclude (uuid[]), $5 lb_similar K, $6 similar_artists K,
-- $7 tag_overlap K, $8 likes_overlap K, $9 random_fill K,
-- $10 taste_overlap K (#796 phase 2b — tracks by the user's top
-- $10 taste_overlap K (tracks by the user's top
-- positively-weighted taste-profile artists, so taste-relevant tracks
-- enter the pool even when the similarity/random arms miss them; scored
-- in Go via TasteMatch, so sim_score here is 0 pool-inclusion),
-- $11 coplay_artists K (#1533 — tracks by artists co-played across the
-- $11 coplay_artists K (tracks by artists co-played across the
-- instance with the seed's artist; source='user_cooccurrence'),
-- $12 order_seed (text) — see below.
--
-- $12 REPLACES `ORDER BY random()` IN FOUR ARMS (#3889). Those arms returned
-- a stable set only while their LIMIT exceeded the rows eligible for them: at
-- that point they returned all of them and the order stopped mattering,
-- because the caller sorts by track id before scoring. Below that threshold
-- they returned a random SUBSET, and two builds on the same day drew
-- different ones — so "daily determinism" held by accident, and only for
-- libraries smaller than the limits.
-- $12 orders four arms by md5(id || seed), not random(). An arm whose LIMIT is
-- below the rows eligible for it returns a SUBSET, and with random() two
-- builds on the same day would draw different ones; the caller sorts by track
-- id before scoring, so only the subset matters.
--
-- md5(id || seed) keeps the intent — an arbitrary spread that changes when
-- the seed does — while making it reproducible for a given seed. The CALLER
@@ -63,7 +60,7 @@ WHERE t.id <> $2
-- keeps varying, which is what a radio should do.
-- Returns same shape as LoadRadioCandidates plus similarity_score column.
--
-- sim_score BANDS keep the tiers in order (rule 131, #6177). Similarity edges
-- sim_score BANDS keep the eligibility tiers in order. Similarity edges
-- are stored relative to the seed's best match, 0 to 1, so the arms are placed
-- in bands rather than compared raw:
-- tier 1 lb_similar 0.5 + 0.5 × edge (0.5, 1.0]
@@ -71,9 +68,9 @@ WHERE t.id <> $2
-- tag_overlap 0.5 × share of the seed's tags
-- likes_overlap 0.3, flat
-- tier 3 taste_overlap, random_fill 0
-- Without the bands, the seed's weaker LB edges fell below a shared genre tag
-- and the flat likes score, and the one arm that measures the seed lost to
-- the ones that guess.
-- Without the bands, the seed's weaker LB edges would fall below a shared
-- genre tag and the flat likes score, and the one arm that measures the seed
-- would lose to the ones that guess.
WITH
seed_artist AS (
@@ -108,11 +105,10 @@ lb_similar AS (
LIMIT $5
),
similar_artists AS (
-- Round-robin across the similar artists (#5297): each artist's first
-- Round-robin across the similar artists: each artist's first
-- track, best-scoring artist first, then each one's second, and so on.
-- Ordering by artist score alone let the closest similar artist's
-- catalogue fill the whole LIMIT; on the operator's library this arm held
-- exactly one artist for all 17 seeds measured (#3879).
-- Ordering by artist score alone would let the closest similar artist's
-- catalogue fill the whole LIMIT.
SELECT track_id, sim_score
FROM (
SELECT t.id AS track_id, asim.score * 0.5 AS sim_score, asim.score AS artist_score,
@@ -138,11 +134,10 @@ tag_overlap AS (
AND t.id <> $2
GROUP BY t.id
HAVING count(DISTINCT trim(g_split.g)) > 0
-- md5 breaks the ties (#3880). A seed with one tag gives every track of
-- that genre the same score, 7,804 tracks on the operator's library, and
-- with no tie-break the LIMIT took whichever rows Postgres reached first:
-- the same slice for every seed of the genre, and not one the order seed
-- controls.
-- md5 breaks the ties. A seed with one tag gives every track of that
-- genre the same score, and with no tie-break the LIMIT would take
-- whichever rows Postgres reached first: the same slice for every seed of
-- the genre, and not one the order seed controls.
ORDER BY sim_score DESC, md5(t.id::text || $12::text)
LIMIT $7
),
@@ -161,12 +156,10 @@ likes_overlap AS (
LIMIT $8
),
taste_overlap AS (
-- Round-robin across the taste artists (#3880), as similar_artists is
-- (#5297): each artist's first track, strongest artist first, then each
-- one's second. Ordered by weight then track id, the strongest artist's
-- catalogue filled the whole arm with the same lowest-id tracks every
-- time; on the operator's library that was 20 tracks of one artist of
-- 174, whose catalogue is 197 tracks.
-- Round-robin across the taste artists, as similar_artists is: each
-- artist's first track, strongest artist first, then each one's second.
-- Ordered by weight then track id, the strongest artist's catalogue would
-- fill the whole arm with the same lowest-id tracks every time.
SELECT track_id, sim_score
FROM (
SELECT t.id AS track_id, 0.0::float8 AS sim_score, tpa.weight,
@@ -183,7 +176,7 @@ taste_overlap AS (
LIMIT $10
),
coplay_artists AS (
-- Household co-play (#1533): tracks by artists co-played across the
-- Household co-play: tracks by artists co-played across the
-- instance with the seed's artist (source='user_cooccurrence', built by
-- the coplay worker). Mirrors similar_artists but from local co-occurrence
-- instead of ListenBrainz; empty on single-user servers. Same 0.5 damp as
@@ -237,7 +230,7 @@ FROM (
UNION ALL SELECT track_id, sim_score FROM coplay_artists
UNION ALL SELECT track_id, sim_score FROM random_fill
) u
JOIN tracks t ON t.id = u.track_id AND t.missing_since IS NULL -- #2523: never offer a file that is gone
JOIN tracks t ON t.id = u.track_id AND t.missing_since IS NULL -- never offer a file that is gone
JOIN albums al ON al.id = t.album_id
LEFT JOIN general_likes l ON l.user_id = $1 AND l.track_id = t.id
LEFT JOIN LATERAL (
@@ -255,10 +248,10 @@ GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
-- name: ListArtistContextPlayCountsForUser :many
-- Per-artist completed-play counts split by whether each play falls in the
-- CURRENT context cell, in the user's local timezone (#1531). The cell is
-- daypart × weekday-type, optionally narrowed by device class (#1551): when
-- $2 (the current device) is '', the device dimension is ignored (identical to
-- the time-only #1531 behaviour, used by the daily mixes which have no device);
-- CURRENT context cell, in the user's local timezone. The cell is
-- daypart × weekday-type, optionally narrowed by device class: when
-- $2 (the current device) is '', the device dimension is ignored (time only,
-- for the daily mixes, which have no device);
-- when $2 is set (radio), a play only counts in the cell if its device_class
-- matches. Feeds the context-affinity scoring term. Skips excluded; a 365-day
-- window bounds cost. Daypart buckets: night [22,5) morning [5,12)
@@ -308,7 +301,7 @@ FROM plays p
GROUP BY p.artist_id;
-- name: GetLatestPlayDeviceClassForUser :one
-- The device_class of the user's most recent play (#1551), used as the
-- The device_class of the user's most recent play, used as the
-- "current device" for radio context conditioning. NULL when the latest play
-- predates device capture or came from a client that didn't send one.
SELECT device_class
@@ -321,24 +314,24 @@ LIMIT 1;
-- Per-user artist suggestions ranked by taste signal x similarity, projected
-- through artist_similarity_unmatched (out-of-library candidates only).
--
-- Seeds are TIERED (rule #131) so the surface never empties:
-- Seeds are TIERED so the surface never empties:
-- tier 1 - taste_profile_artists.weight: engagement-graded, time-decayed and
-- SIGNED by internal/taste, so an artist the user has drifted away
-- from stops contributing instead of accumulating forever.
-- tier 2 - likes + completed plays, used ONLY when the profile has no rows
-- (new account, or before the first daily recompute).
--
-- The signal is log-damped: contribution is signal x similarity, and the old
-- undamped sum let one heavily-played artist's neighbours take every slot --
-- entrenching harder the MORE the user listened (issue #2367 mechanism 2).
-- The signal is log-damped: contribution is signal x similarity, and an
-- undamped sum would let one heavily-played artist's neighbours take every
-- slot -- entrenching harder the MORE the user listened.
--
-- Candidates already in the library, already requested and not terminal, or
-- snoozed by this user, are excluded. $1=user_id, $2=half_life_days (tier 2
-- decay), $3=limit.
WITH artist_plays AS (
-- Completed plays only. The previous seed query counted every play_event,
-- so skipping an artist repeatedly INCREASED its signal and pushed more of
-- its neighbours at the user (issue #2367 mechanism 3).
-- Completed plays only: counting every play_event would make skipping an
-- artist repeatedly INCREASE its signal and push more of its neighbours
-- at the user.
SELECT t.artist_id, count(*)::bigint AS play_count
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
@@ -395,7 +388,7 @@ contributions AS (
AND r.artist_mbid = u.candidate_mbid
AND r.status NOT IN ('rejected', 'failed')
)
-- Snoozed by this user and not yet expired (#2374). Time-boxed and
-- Snoozed by this user and not yet expired. Time-boxed and
-- per-user: the candidate returns on its own once snoozed_until
-- passes, and stays visible to everyone else meanwhile. Deliberately
-- filtered at the candidate stage, NOT folded into the score — a
@@ -421,7 +414,7 @@ ORDER BY total_score DESC
LIMIT $3;
-- name: ListMostPlayedTracksForUser :many
-- M6a: top-N tracks by completed-play count for the user. was_skipped
-- Top-N tracks by completed-play count for the user. was_skipped
-- excludes skips so a user spamming next doesn't fabricate a top track.
-- Quarantined tracks (per-user soft-hide from M5b) are filtered out.
--
@@ -442,7 +435,7 @@ FROM plays p
JOIN tracks t ON t.id = p.track_id
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
@@ -469,7 +462,7 @@ JOIN tracks t ON t.id = p.track_id
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = sqlc.arg(artist_id)
AND t.missing_since IS NULL -- #2523: never offer a file that is gone
AND t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = sqlc.arg(user_id) AND q.track_id = t.id
@@ -478,7 +471,7 @@ ORDER BY p.cnt DESC, t.id
LIMIT sqlc.arg(result_limit);
-- name: ListLastPlayedArtistsForUser :many
-- M6a: artists ranked by max(play_events.started_at) for the user, with
-- Artists ranked by max(play_events.started_at) for the user, with
-- a derived cover_album_id via a representative-album lateral join (most
-- recent album that has cover_art_path set). album_count joined for the
-- ArtistRef wire shape.
@@ -1,16 +1,16 @@
-- Recommendation observability (#796 phase 4). Per-source play outcomes so the
-- Recommendation observability. Per-source play outcomes so the
-- operator can see whether each recommendation surface is landing and tune the
-- taste weights. Source is stamped on play_events when a play is launched from
-- a recommendation surface; NULL means the user picked the track manually —
-- those rows are INCLUDED here as the baseline control group the surfaces are
-- judged against (milestone #127: delta-vs-baseline is what makes the numbers
-- actionable). Raw source strings are bucketed into stable surface families in
-- the Go handler; completion_n is carried so family merges can weight
-- judged against (delta-vs-baseline is what makes the numbers actionable).
-- Raw source strings are bucketed into stable surface families in the Go
-- handler; completion_n is carried so family merges can weight
-- avg_completion correctly.
-- name: RecommendationWeeklyTrends :many
-- Weekly per-source outcome series for the tuning lab's trend view
-- (#1251). Aggregated across ALL users: the tuning knobs are global,
--. Aggregated across ALL users: the tuning knobs are global,
-- so judging a knob turn needs global outcomes — rows carry rates
-- only, no track or user identity. NULL-source (manual) rows are
-- included as the baseline family.
@@ -42,12 +42,12 @@ ORDER BY 1, 2;
-- name: RecommendationSourceMetricsForUser :many
-- $1 user_id, $2 window_days. plays/skips are counts; avg_completion is the
-- mean completion ratio over the completion_n plays that recorded one.
-- pick_kind splits For You plays into taste/fresh/unattributed (#1249);
-- pick_kind splits For You plays into taste/fresh/unattributed;
-- it is NULL for every other source, so those still group to one row.
--
-- completion_sqsum carries the sum of SQUARED completion ratios so the Go
-- handler can compute a variance — needed for the margin of error on a
-- completion delta (#2495). It is the sum rather than `stddev_samp` on purpose:
-- completion delta. It is the sum rather than `stddev_samp` on purpose:
-- raw source rows get merged into surface families in Go, and sums of squares
-- add across groups exactly, whereas standard deviations cannot be combined
-- without them. Variance = (sqsum - sum²/n) / (n-1), with sum = avg × n.
@@ -1,9 +1,9 @@
-- Recommendation tuning lab queries (#1250). Seeding happens via the
-- Recommendation tuning lab queries. Seeding happens via the
-- recsettings boot reconcile; shipped defaults live in Go only.
-- name: UpsertWeightProfileDefaults :exec
-- Boot reconcile: insert the shipped defaults for a profile if the row
-- doesn't exist yet. Never overwrites operator-tuned values.
-- doesn't exist yet. Never overwrites admin-tuned values.
INSERT INTO recommendation_weight_profiles (
profile, base_weight, like_boost, recency_weight, skip_penalty,
jitter_magnitude, context_weight, similarity_weight, taste_weight,
@@ -54,8 +54,8 @@ UPDATE taste_tuning
RETURNING *;
-- name: UpsertDiscoverTuningDefaults :exec
-- Boot reconcile for the Discover scope (#2377). Never overwrites
-- operator-tuned values, same contract as the other two.
-- Boot reconcile for the Discover scope. Never overwrites
-- admin-tuned values, same contract as the other two.
INSERT INTO discover_tuning (singleton, tag_overlap_weight, snooze_days)
VALUES (true, $1, $2)
ON CONFLICT (singleton) DO NOTHING;
@@ -77,7 +77,7 @@ INSERT INTO recommendation_tuning_audit (scope, action, changes)
VALUES ($1, $2, $3);
-- name: ListTuningAudit :many
-- Newest first; consumed by the metrics trend view (#1251) to annotate
-- Newest first; consumed by the metrics trend view to annotate
-- knob turns on the timeline.
SELECT id, changed_at, scope, action, changes
FROM recommendation_tuning_audit
+1 -1
View File
@@ -111,7 +111,7 @@ SELECT * FROM requests
LIMIT 1;
-- name: RepointRequestsAlbumMbid :exec
-- A want moved from a release id to its group (#5241) takes the requests
-- A want moved from a release id to its group takes the requests
-- that name it along, so the reconciler finds it.
UPDATE requests
SET album_mbid = sqlc.arg(new_mbid),
+2 -2
View File
@@ -1,4 +1,4 @@
-- M7 #381: scan-run lifecycle queries.
-- Scan-run lifecycle queries.
-- name: InsertScanRun :one
-- Creates a new scan_runs row with started_at=now(). Stages are filled in
@@ -33,7 +33,7 @@ SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, artist
LIMIT 1;
-- name: UpdateScanRunArtistArtEnrich :exec
-- M7 cover-sources: persists the artist-art enrich stage tally JSON.
-- Persists the artist-art enrich stage tally JSON.
UPDATE scan_runs
SET artist_art_enrich = $2
WHERE id = $1;
+3 -3
View File
@@ -1,4 +1,4 @@
-- Searching for wants and grabbing releases (M502 #5521).
-- Searching for wants and grabbing releases.
-- name: GetAcquisitionSettings :one
SELECT * FROM acquisition_settings WHERE id = true;
@@ -24,7 +24,7 @@ UPDATE acquisition_settings SET last_pass_at = now(), last_pass = sqlc.arg(repor
-- Wants with no download running that were never searched or not since
-- @hours ago: open wants first (never-searched, then requests, then missing
-- files, then followed artists' releases), then held albums wanted for an
-- upgrade (#5524), which are searched at most weekly.
-- upgrade, which are searched at most weekly.
SELECT w.id, w.release_group_mbid, w.artist_name, w.title, w.reason, w.upgrade, w.held_tier, w.held_quality,
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
@@ -120,7 +120,7 @@ UPDATE grabs
-- name: ListReleaseBlocks :many
-- Every release set aside and still held back: a stalled one lifts by itself
-- at expires_at (#5523).
-- at expires_at.
SELECT * FROM release_blocks
WHERE expires_at IS NULL OR expires_at > now()
ORDER BY blocked_at DESC;
+2 -1
View File
@@ -41,7 +41,8 @@ DELETE FROM sessions WHERE id = $1;
DELETE FROM sessions WHERE token_hash = $1;
-- name: DeleteSessionForUser :execrows
-- Scoped by user_id, not just id (rule #47). Keyed on the id alone, any
-- Scoped by user_id, not just id: household data is per-user. Keyed on the
-- id alone, any
-- household member could revoke another member's session by guessing a uuid.
-- execrows lets the handler answer 404 rather than a false 204 when the row
-- isn't theirs.
+5 -5
View File
@@ -3,8 +3,8 @@
-- 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.
-- counts as fetched; reading the edges would let 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
@@ -70,9 +70,9 @@ ON CONFLICT (artist_id) DO UPDATE SET fetched_at = now(), returned = EXCLUDED.re
--
-- 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
-- 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.
+2 -2
View File
@@ -1,4 +1,4 @@
-- Song links (M498 #5438): copies of one song on different releases share a
-- Song links: copies of one song on different releases share a
-- song_key (migration 0076).
-- name: LinkTracksAsSong :one
@@ -41,7 +41,7 @@ UPDATE tracks SET song_id = NULL
-- name: ListTrackSongKeys :many
-- What the mix writer needs to place a track: its song, and whether it is a
-- video rip held back from the mixes (#5439).
-- video rip held back from the mixes.
SELECT id, song_key, (source_verdict IS NOT DISTINCT FROM 'suspect')::boolean AS held_back
FROM tracks WHERE id = ANY(sqlc.arg(ids)::uuid[]);
+1 -1
View File
@@ -1,4 +1,4 @@
-- Time-boxed "not right now" on a Discover artist suggestion (#2374).
-- Time-boxed "not right now" on a Discover artist suggestion.
--
-- See 0049_suggestion_snoozes.up.sql for why this is a snooze and not a
-- dismissal: it records no verdict on the music, expires on its own, and
+37 -40
View File
@@ -1,18 +1,18 @@
-- Candidate queries for the discovery system-playlist mixes
-- (#419-423). Each returns (id, album_id, artist_id) rows; the Go
--. Each returns (id, album_id, artist_id) rows; the Go
-- producer caps + truncates and converts to rankedCandidate. Daily-
-- deterministic md5(id||dateStr) ordering (where used) keeps a mix
-- stable within a day while rotating across days; shuffle-on-play
-- gives per-play variety on top.
-- name: ListDeepCutsTracks :many
-- #419 Deep Cuts: low-play tracks (<=2 plays) from artists the user
-- Deep Cuts: low-play tracks (<=2 plays) from artists the user
-- has liked OR played heavily (>=5 non-skip plays across the artist).
-- Tracks the user has skipped twice or more don't qualify (#1257):
-- eligibility used to count only unskipped plays, so the most actively
-- rejected tracks read as "barely heard" and kept being re-offered.
-- Threshold 2 so a single accidental skip doesn't banish a track;
-- passive-signal only (no dislike UI, rule #101).
-- Tracks the user has skipped twice or more don't qualify: counting only
-- unskipped plays would make the most actively rejected tracks read as
-- "barely heard" and keep re-offering them. Threshold 2 so a single
-- accidental skip doesn't banish a track; passive signal only (there is no
-- dislike button).
-- $1 user_id, $2 date string.
WITH affinity_artists AS (
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
@@ -40,7 +40,7 @@ SELECT t.id, t.album_id, t.artist_id
JOIN affinity_artists aa ON aa.artist_id = t.artist_id
LEFT JOIN play_counts pc ON pc.track_id = t.id
LEFT JOIN skip_counts sc ON sc.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND COALESCE(pc.c, 0) <= 2
AND COALESCE(sc.c, 0) < 2
AND NOT EXISTS (
@@ -51,11 +51,10 @@ SELECT t.id, t.album_id, t.artist_id
LIMIT 200;
-- name: ListRediscoverTracks :many
-- #420 Rediscover: tracks the user played a lot but has drifted away
-- from. One blended pool (issue #1246: the old two-tier UNION was
-- all-or-nothing — a single ">=6 months cold" row suppressed the whole
-- ">=30 days" tier, which is how a one-song playlist shipped; the 6mo
-- tier was also a strict subset of the 30d tier). Eligibility is >=3
-- Rediscover: tracks the user played a lot but has drifted away
-- from. One blended pool, not a two-tier UNION: the ">=6 months cold" tier is
-- a strict subset of the ">=30 days" one, and a UNION that let one row of it
-- suppress the wider tier would ship a one-song playlist. Eligibility is >=3
-- non-skip plays and >=30 days cold; the bar sits at 3 rather than 5
-- so a weeks-old library still fields a pool — on young histories the
-- >=5-play tracks are precisely the ones still in rotation. Ordering
@@ -71,18 +70,18 @@ WITH stats AS (
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
JOIN stats s ON s.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND s.c >= 3
AND s.last_at <= now() - interval '30 days'
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
-- Declined-resurfacing cooldown (#1258): a skip on a
-- Declined-resurfacing cooldown: a skip on a
-- rediscover-sourced play is the user explicitly saying "I've moved
-- on" to the exact invitation this mix extends — such tracks sit
-- out ~90 days instead of re-qualifying the next day. Passive
-- signal only (rule #101).
-- signal only.
AND NOT EXISTS (
SELECT 1 FROM play_events rp
WHERE rp.user_id = $1
@@ -97,14 +96,13 @@ SELECT t.id, t.album_id, t.artist_id
LIMIT 200;
-- name: ListNewForYouTracks :many
-- #421 New for you, tiered per project rule #131 (#1267). The old
-- single hard rule (added <30d AND direct affinity) had two failure
-- modes with one root: no notion of consumption — the album you
-- devoured in week one crowded the mix for three more weeks, and
-- after a quiet month nothing qualified at all.
-- New for you, tiered so the mix degrades rather than vanishes. A
-- single hard rule (added <30d AND direct affinity) has no notion of
-- consumption: the album you devoured in week one would crowd the mix
-- for three more weeks, and after a quiet month nothing would qualify.
--
-- "Consumed" = any track attempted for >=30s (the First Listens
-- threshold, #1268): once you've meaningfully engaged, the album is
-- threshold): once you've meaningfully engaged, the album is
-- no longer news. Applies to every tier — played albums leave the
-- mix the next build.
--
@@ -151,7 +149,7 @@ albums_tiered AS (
SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
FROM tracks t
JOIN albums_tiered alt ON alt.album_id = t.album_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
@@ -161,18 +159,18 @@ SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
LIMIT 300;
-- name: ListOnThisDayTracks :many
-- #422 On This Day: tracks the user played around this calendar date
-- On This Day: tracks the user played around this calendar date
-- (±10 day-of-year) in the past, excluding the very recent (>30 days
-- ago) so it's nostalgic, not just "last week". Weighted by how much
-- they were played in those windows. Floor relaxed from 60→30 days
-- and window ±7→±10 so it surfaces on a months-old library instead
-- of needing a full year of history; still skips cleanly (no rows →
-- no playlist) when there's no qualifying history yet.
-- Day-of-year distance is circular (#1256): plain ABS made Dec 28 vs
-- Jan 3 read as 359 days apart, silently gutting the window for ~3
-- weeks around every New Year — precisely when holiday nostalgia is
-- the point. LEAST(d, 365-d) wraps the boundary; leap-year drift of
-- ±1 is absorbed by the ±10 window. The build date ($2, already here
-- they were played in those windows. The 30-day floor and ±10 window
-- let it surface on a months-old library rather than needing a full
-- year of history; it still skips cleanly (no rows → no playlist)
-- when there's no qualifying history yet.
-- Day-of-year distance is circular: plain ABS would read Dec 28 vs
-- Jan 3 as 359 days apart, gutting the window for ~3 weeks around
-- every New Year — precisely when holiday nostalgia is the point.
-- LEAST(d, 365-d) wraps the boundary; leap-year drift of ±1 is absorbed
-- by the ±10 window. The build date ($2, already here
-- for the md5 rotation) anchors "today" instead of now() so the
-- window is testable and consistent with the mix's daily determinism.
-- $1 user_id, $2 date string.
@@ -190,7 +188,7 @@ WITH windowed AS (
SELECT t.id, t.album_id, t.artist_id
FROM tracks t
JOIN windowed w ON w.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
@@ -199,11 +197,10 @@ SELECT t.id, t.album_id, t.artist_id
LIMIT 200;
-- name: ListFirstListensTracks :many
-- #423 First Listens: songs the user has never heard or even
-- attempted. "Attempted" is track-level with a >=30s listen threshold
-- (#1268, operator decision 2026-07-03) — the old version disqualified
-- a whole album on ANY play_event, so a 2-second accidental brush
-- banished it permanently.
-- First Listens: songs the user has never heard or even
-- attempted. "Attempted" is track-level with a >=30s listen threshold:
-- disqualifying a whole album on ANY play_event would let a 2-second
-- accidental brush banish it permanently.
--
-- tier 1 the exact desire: albums with ZERO attempted tracks
-- tier 2 step back: barely-attempted albums (<=25% of tracks
@@ -244,7 +241,7 @@ SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
FROM tracks t
JOIN albums al ON al.id = t.album_id
JOIN albums_tiered alt ON alt.album_id = al.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
WHERE t.missing_since IS NULL -- never offer a file that is gone
AND alt.tier IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM attempted a WHERE a.track_id = t.id)
AND NOT EXISTS (
@@ -1,4 +1,4 @@
-- Rotation state for system-playlist sample-history dedup (#415).
-- Rotation state for system-playlist sample-history dedup.
-- One row per (user, playlist_kind). played_track_ids accumulates
-- tracks heard from that playlist in the current rotation; shuffle
-- prefers ids NOT in this set, then resets when the snapshot is
+10 -10
View File
@@ -1,4 +1,4 @@
-- Track picks here join `tracks ... AND t.missing_since IS NULL` (#2523): a
-- Track picks here join `tracks ... AND t.missing_since IS NULL`: a
-- seed or For-You candidate has to be something that can actually play. Note
-- this only affects newly GENERATED playlists — already-stored system
-- playlists keep their rows until the next daily rebuild, which is why the
@@ -6,7 +6,7 @@
-- also serves user-curated playlists, where hiding a track the user added
-- themselves would be wrong).
-- M7 #352 slice 2: system-generated playlist queries.
-- System-generated playlist queries.
-- name: ListActiveUsersForSystemPlaylists :many
-- Active = had a play in the last 7 days. The cron iterates this list.
@@ -56,22 +56,22 @@ UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true;
-- name: PickSeedArtists :many
-- Top-12 most-engaged distinct artist candidates, tiered so the
-- "Songs like X" mixes never silently vanish (#1255): the old hard
-- 7-day window emptied the seed pool after a quiet week, and the
-- daily atomic-replace build then deleted every existing mix until
-- the user played something again. Same fallback shape as
-- "Songs like X" mixes never silently vanish: a hard 7-day window
-- would empty the seed pool after a quiet week, and the daily
-- atomic-replace build would then delete every existing mix until the
-- user played something again. Same fallback shape as
-- PickTopPlayedTracksForUser (For You's seeds):
-- tier 0 engagement in the last 7 days
-- tier 1 (only if tier 0 empty) last 30 days
-- tier 2 (only if tiers 0+1 empty) all-time
-- tier 3 (only if 0-2 empty) liked artists with no play history
-- All returned rows share one tier; produceSeedMixes maps it onto the
-- rule-#131 pick-kind ladder and stamps the built tracks, so metrics
-- tiered pick-kind ladder and stamps the built tracks, so metrics
-- can compare mixes seeded from fresh vs stale engagement.
-- Score = unskipped-play count + 5 if user has liked the artist. The
-- Go-side picker (pickSeedArtistsForDay) shuffles the 12
-- daily-deterministically and takes songsLikeSeedCount (6) so the mix
-- set both rotates day-to-day and fills the dedicated Home row (#1491).
-- set both rotates day-to-day and fills the dedicated Home row.
WITH liked AS (
SELECT gla.artist_id FROM general_likes_artists gla WHERE gla.user_id = $1
),
@@ -163,7 +163,7 @@ alltime AS (
),
-- A likes-only read still joins tracks: a like outlives its file, and a
-- seed whose file is gone points For You at something the user can't hear
-- (#2701). The other two tiers get the same filter from their play_events
--. The other two tiers get the same filter from their play_events
-- join.
liked AS (
SELECT gl.track_id AS id, 0::bigint AS c, 2 AS tier
@@ -206,7 +206,7 @@ SELECT COALESCE(
FROM tracks t
JOIN albums a ON a.id = t.album_id
WHERE t.artist_id = $2
AND t.missing_since IS NULL -- #2701: the play branch filters; this one must too
AND t.missing_since IS NULL -- the play branch filters; this one must too
ORDER BY a.release_date DESC NULLS LAST,
t.disc_number NULLS LAST,
t.track_number NULLS LAST,
+2 -2
View File
@@ -1,4 +1,4 @@
-- Tag-enrichment provider settings queries (milestone #160, #1490 Step 2).
-- Tag-enrichment provider settings queries.
-- The tags.SettingsService consumes these — parallel to the cover-art
-- SettingsService (coverart_settings.sql). Keeping the two independent
-- means a new tag source is added without touching the art settings.
@@ -12,7 +12,7 @@ SELECT provider_id, enabled, api_key, display_order, created_at, updated_at
-- name: UpsertTagProviderSettings :exec
-- INSERT a default row for a newly-registered provider, or refresh an
-- existing row's display_order on boot. Does NOT overwrite enabled /
-- api_key on conflict — the operator's settings persist across restarts.
-- api_key on conflict — an admin's settings persist across restarts.
INSERT INTO tag_provider_settings (provider_id, enabled, display_order)
VALUES ($1, $2, $3)
ON CONFLICT (provider_id) DO UPDATE
+7 -7
View File
@@ -1,13 +1,13 @@
-- Taste profile (#796 phase 1). The daily build computes signed artist/tag
-- Taste profile. The daily build computes signed artist/tag
-- weights in Go (graded engagement + decay + like bonus) and atomic-replaces
-- them here. Read queries serve "top taste" lookups (used in phase 2).
-- them here. Read queries serve the scorer's "top taste" lookups.
-- name: ListPlayEngagementInputsForUser :many
-- One row per play in the decay-relevant window. completion is the play's
-- completion ratio (precomputed column when present, else duration_played /
-- track duration, clamped to [0,1]); age_days drives the time-decay. Genre
-- is split into tags in Go; release_date derives the decade for the era
-- facet (#1530). Quarantined tracks are excluded.
-- facet. Quarantined tracks are excluded.
SELECT
t.id AS track_id,
t.artist_id,
@@ -31,7 +31,7 @@ WHERE pe.user_id = $1
-- name: ListLikedTrackTasteInputsForUser :many
-- (track_id, artist_id, genre, release_date) for each track the user has
-- explicitly liked. Feeds the track-like bonus into the liked track's artist,
-- tags, and era (#1530); track_id keys the enriched track_tags lookup (#1490).
-- tags, and era; track_id keys the enriched track_tags lookup.
SELECT t.id AS track_id, t.artist_id, t.genre, a.release_date
FROM general_likes gl
JOIN tracks t ON t.id = gl.track_id
@@ -57,7 +57,7 @@ INSERT INTO taste_profile_tags (user_id, tag, weight)
VALUES ($1, $2, $3);
-- name: ListTasteProfileArtistsForUser :many
-- Top-weighted taste artists (phase-2 consumption + tests).
-- Top-weighted taste artists, for the scorer and tests.
SELECT artist_id, weight
FROM taste_profile_artists
WHERE user_id = $1
@@ -79,7 +79,7 @@ INSERT INTO taste_profile_eras (user_id, era, weight)
VALUES ($1, $2, $3);
-- name: ListTasteProfileErasForUser :many
-- Top-weighted taste eras (#1530); consumed by the scorer's era term.
-- Top-weighted taste eras; consumed by the scorer's era term.
SELECT era, weight
FROM taste_profile_eras
WHERE user_id = $1
@@ -94,7 +94,7 @@ INSERT INTO taste_profile_moods (user_id, mood, weight)
VALUES ($1, $2, $3);
-- name: ListTasteProfileMoodsForUser :many
-- Top-weighted taste moods (#1534); consumed by the scorer's mood term.
-- Top-weighted taste moods; consumed by the scorer's mood term.
SELECT mood, weight
FROM taste_profile_moods
WHERE user_id = $1
+3 -3
View File
@@ -1,4 +1,4 @@
-- Track-tag enrichment queries (milestone #160, #1490). The tag enricher
-- Track-tag enrichment queries. The tag enricher
-- drains ListTracksMissingTags, fetches from the provider chain, and
-- writes the merged top-K via DeleteTrackTags + InsertTrackTag, then
-- stamps SetTrackTagSource. Mirrors the coverart artist-enricher shape.
@@ -8,7 +8,7 @@
-- or previously settled 'none' under an older provider version. Returns
-- the fields the provider chain needs — recording MBID (nullable) for
-- keyed lookups, title + artist name for name-based fallback, and the
-- artist MBID (nullable) for MusicBrainz's artist-tag fallback (#1519).
-- artist MBID (nullable) for MusicBrainz's artist-tag fallback.
-- $1 = current tag_sources_version, $2 = limit.
SELECT t.id, t.mbid, t.title, a.name AS artist_name, a.mbid AS artist_mbid
FROM tracks t
@@ -58,7 +58,7 @@ SELECT tt.track_id, tt.tag, tt.weight
WHERE gl.user_id = $1;
-- name: ListTrackTagsForTracks :many
-- Enriched tags for a set of candidate tracks (#1534), so the scorer can
-- Enriched tags for a set of candidate tracks, so the scorer can
-- derive each candidate's mood buckets at scoring time. One row per (track,
-- tag); the caller filters to mood words via the internal/mood vocabulary.
SELECT tt.track_id, tt.tag
+28 -29
View File
@@ -1,7 +1,7 @@
-- name: UpsertTrack :one
-- file_path is the canonical identity for library scan; mbid is secondary.
-- mbid is the file's tag. A looked-up id (mbid_source 'acoustid', M401)
-- survives a re-read that finds no tag id, and a tag id replaces it (D4).
-- mbid is the file's tag. A looked-up id (mbid_source 'acoustid')
-- survives a re-read that finds no tag id, and a tag id replaces it.
INSERT INTO tracks (
title, album_id, artist_id, track_number, disc_number,
duration_ms, file_path, file_size, file_format, bitrate, mbid, genre,
@@ -25,7 +25,7 @@ ON CONFLICT (file_path) DO UPDATE SET
WHEN tracks.mbid_source = 'acoustid' THEN 'acoustid' END,
genre = EXCLUDED.genre,
-- Stamped on update too, so a tag-repair pass marks rows as done and the
-- next scan can short-circuit them again (#2499).
-- next scan can short-circuit them again.
tag_read_version = EXCLUDED.tag_read_version,
updated_at = now()
RETURNING *;
@@ -34,7 +34,7 @@ RETURNING *;
-- Track recording-MBID backfill: tracks with NULL mbid that still have
-- a file to re-read. $1 caps the batch (mirrors the album backfill).
-- Missing tracks are skipped: there is no file, and opening it anyway
-- logged a warning per track on every scan (#5139).
-- would log a warning per track on every scan.
SELECT id, file_path
FROM tracks
WHERE mbid IS NULL
@@ -92,7 +92,7 @@ WHERE title ILIKE '%' || $1::text || '%'
);
-- name: ListArtistTracksForUser :many
-- M6a: every track for the artist across their albums, with album_title
-- Every track for the artist across their albums, with album_title
-- and artist_name joined. Honors per-user quarantine. Used by
-- /api/artists/{id}/tracks for the artist-card play affordance, which
-- shuffles client-side. Ordering matches album/track natural order so
@@ -112,7 +112,7 @@ ORDER BY albums.release_date NULLS LAST, albums.sort_title,
t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.id;
-- name: ListRandomTracksForUser :many
-- #427 S4: backing query for GET /api/library/shuffle — the online
-- Backing query for GET /api/library/shuffle — the online
-- source for "Shuffle all". N random tracks across the whole
-- library, per-user-quarantine filtered. $1 user_id, $2 limit.
SELECT sqlc.embed(t),
@@ -135,7 +135,7 @@ LIMIT $2;
SELECT COUNT(*) FROM tracks WHERE artist_id = $1;
-- name: DeleteTrack :one
-- M7 #372: hard delete with FK cascade. The CASCADE on track_id from
-- Hard delete with FK cascade. The CASCADE on track_id from
-- play_events / general_likes_tracks / quarantine /
-- quarantine_actions handles their cleanup. RETURNING gives us
-- album_id + artist_id for the album-empty / artist-empty cascade
@@ -145,11 +145,11 @@ RETURNING id, album_id, artist_id, file_path, mbid;
-- name: GetTracksByIDs :many
-- Batched lookup used by /api/library/sync to hydrate upsert payloads
-- (#357). Mirror of GetArtistsByIDs.
--. Mirror of GetArtistsByIDs.
SELECT * FROM tracks WHERE id = ANY($1::uuid[]);
-- name: FindMissingTrackByMbid :many
-- Move detection, strongest signal (#2528). A file that turned up at a new path
-- Move detection, strongest signal. A file that turned up at a new path
-- carrying a recording MBID we already have on a MISSING row is that recording,
-- moved — not a new track.
--
@@ -166,11 +166,11 @@ SELECT id, file_path FROM tracks
LIMIT 2;
-- name: FindMissingTrackByAudioHash :many
-- Move detection fallback for files with no MBID (#2528, #3914). The audio stream
-- Move detection fallback for files with no MBID. The audio stream
-- hash identifies the encoded audio itself, so it survives a rename, a move and a
-- retag — anything short of a re-encode. It replaced (file_size, duration_ms),
-- which could pair two unrelated files that happened to share a byte count and a
-- duration, and missed a file retagged in place, whose size changes.
-- retag — anything short of a re-encode. (file_size, duration_ms) would pair two
-- unrelated files that share a byte count and a duration, and miss a file
-- retagged in place, whose size changes.
--
-- Same missing-only constraint and same LIMIT 2 rationale as the MBID variant.
SELECT t.id, t.file_path
@@ -196,7 +196,7 @@ UPDATE tracks
-- name: ListTrackPathsForReconcile :many
-- Every row's path + current missing mark, for the scanner's reconcile pass
-- (#2523). Deliberately unfiltered and unpaged: reconcile has to compare the
--. Deliberately unfiltered and unpaged: reconcile has to compare the
-- WHOLE table against what the walk saw, and a filtered subset would let rows
-- outside it drift forever. Three narrow columns keep it cheap even on a
-- library of a few hundred thousand tracks.
@@ -217,10 +217,10 @@ UPDATE tracks
AND missing_since IS NULL;
-- name: ClearTrackMbids :exec
-- Forgets the recording MBID of rows whose file was another song's (M498's
-- wrong-slot removal). The MBID came from that file, so it names the song kept
-- elsewhere on the album, and missing-pair folding would fold the emptied slot
-- into it: twelve slots went that way on 2026-10-20 and were never wanted back.
-- Forgets the recording MBID of rows whose file was another song's (the
-- duplicate resolver's wrong-slot removal). The MBID came from that file, so it
-- names the song kept elsewhere on the album, and missing-pair folding would
-- fold the emptied slot into it, so it would never be wanted back.
-- mbid_source goes with it (tracks_mbid_source_pair).
UPDATE tracks
SET mbid = NULL, mbid_source = NULL
@@ -245,19 +245,18 @@ UPDATE tracks
AND missing_since IS NOT NULL;
-- name: ListMissingTracks :many
-- The admin review surface for files the scan could not find (#2527).
-- The admin review surface for files the scan could not find.
--
-- Ordered by directory, then by the file's own position within its album,
-- because the unit an operator actually reasons about is a FOLDER: the case
-- this was built for was three whole albums that had been reorganised, and a
-- flat list ordered by timestamp presents that as forty unrelated decisions.
-- because the unit an admin actually reasons about is a FOLDER: a reorganised
-- album, listed flat by timestamp, reads as dozens of unrelated decisions.
-- Grouping happens in the handler; the ordering here is what makes a group
-- contiguous, so a page boundary splits a directory at worst.
--
-- last_played_at is a correlated MAX rather than a join so a track with no
-- plays stays in the result with NULL. It is here because "gone six months,
-- never played" and "gone yesterday, played 200 times" deserve opposite
-- reactions, and the operator can't tell them apart from a path.
-- reactions, and an admin can't tell them apart from a path.
SELECT t.id,
t.title,
t.file_path,
@@ -285,7 +284,7 @@ SELECT COUNT(*) FROM tracks WHERE missing_since IS NOT NULL;
-- name: ListSuspectSourceTracks :many
-- The admin report of present tracks whose filename looks like a video rip
-- (#5410): "(Official Video)", "[Audio]", "Visualizer" and the like.
--: "(Official Video)", "[Audio]", "Visualizer" and the like.
--
-- pattern is built in Go (internal/api/admin_suspect_sources.go) from the
-- same marker list that labels each row, so the filter and the labels cannot
@@ -308,7 +307,7 @@ SELECT t.id,
t.source_verdict,
t.source_verdict_at,
-- The album's place on the wanted list: wanted for a clean copy, and
-- when that was last searched for (#5524).
-- when that was last searched for.
COALESCE(w.upgrade, '')::text AS want_upgrade,
w.searched_at AS searched_at
FROM tracks t
@@ -330,8 +329,8 @@ SELECT COUNT(*) FROM tracks t
AND regexp_replace(t.file_path, '^.*/', '') ~* sqlc.arg(pattern)::text;
-- name: FlagSuspectSourceTracks :execrows
-- Holds back present tracks whose basename carries a video-rip marker (M498
-- #5439), the same pattern the report lists. A track the operator called fine
-- Holds back present tracks whose basename carries a video-rip marker, the
-- same pattern the report lists. A track an admin called fine
-- keeps that verdict.
UPDATE tracks
SET source_verdict = 'suspect', source_verdict_at = now()
@@ -348,7 +347,7 @@ UPDATE tracks
AND NOT regexp_replace(file_path, '^.*/', '') ~* sqlc.arg(pattern)::text;
-- name: SetTrackSourceVerdict :execrows
-- The operator's call on a flagged track: 'fine' lets it back into radio and
-- An admin's call on a flagged track: 'fine' lets it back into radio and
-- the mixes for good; 'suspect' holds it back again.
UPDATE tracks
SET source_verdict = sqlc.arg(verdict)::text, source_verdict_at = now()
@@ -357,7 +356,7 @@ UPDATE tracks
-- name: ListStillMissingTrackLabels :many
-- Which of the tracks a scan marked missing still are once it has adopted
-- moves and folded replacements (M542): the ones worth telling an admin
-- moves and folded replacements: the ones worth telling an admin
-- about, by name. A quality upgrade folds into its replacement and drops out.
SELECT ar.name AS artist_name, t.title
FROM tracks t
+1 -1
View File
@@ -37,7 +37,7 @@ SELECT count(*) FROM users;
-- name: SetSubsonicPassword :exec
-- Stores (or clears with NULL) the per-user Subsonic legacy credential used
-- for t/s and p auth on /rest/*. Must be plaintext; see migration 0003. Only
-- ever a server-generated value, never the login password (#5026).
-- ever a server-generated value, never the login password.
UPDATE users SET subsonic_password = $2 WHERE id = $1;
-- name: GetUserByID :one
+21 -21
View File
@@ -1,4 +1,4 @@
-- The wanted list (M502 #5520): release-type profiles, follows and wants.
-- The wanted list: release-type profiles, follows and wants.
-- name: ListReleaseTypeProfiles :many
SELECT p.*,
@@ -145,10 +145,10 @@ SELECT a.id AS album_id,
COALESCE(bool_or(t.missing_since IS NULL AND t.added_at > sqlc.arg(since)::timestamptz), false)::bool AS arrived_since,
COALESCE(bool_or(t.missing_since IS NOT NULL AND t.missing_since < sqlc.arg(since)::timestamptz), false)::bool AS missing_before,
-- Missing tracks no track on disk stands in for, by position or
-- title. A missing row whose song is back at a new path (#5245) is
-- title. A missing row whose song is back at a new path is
-- covered; a lost bonus track is not. A track with no disc number is
-- on disc 1: Air's Love 2 stayed wanted, and was downloaded again for
-- nothing, over a missing track 6 with no disc beside a present 1/6.
-- on disc 1, so a missing track 6 with no disc beside a present 1/6
-- does not keep the album wanted.
COUNT(*) FILTER (WHERE t.missing_since IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM tracks p
WHERE p.album_id = a.id AND p.missing_since IS NULL
@@ -171,7 +171,7 @@ UPDATE wants
WHERE id = sqlc.arg(id) AND fulfilled_at IS NULL;
-- name: RepointWant :exec
-- A want recorded under a release id before #5241 is moved to its group.
-- A want recorded under a release id is moved to its group.
UPDATE wants SET release_group_mbid = $2, updated_at = now() WHERE id = $1;
-- name: DeleteWant :execrows
@@ -180,13 +180,13 @@ DELETE FROM wants WHERE id = $1;
-- name: ListWants :many
-- The Wanted page: open wants (all but missing ones, or only missing ones),
-- the held ones, or all of them; artist_mbid narrows to one artist's page,
-- want_ids to given wants. Ordered like the library (#5572): artists A–Z by the
-- want_ids to given wants. Ordered like the library: artists A–Z by the
-- library's sort name when it holds the artist, each artist's albums by
-- release date, so the page groups them as they arrive. q narrows to an
-- artist or album title; total counts every match, past the page.
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason,
w.album_id, w.wanted_at, w.fulfilled_at,
-- Why a held album is wanted again, and the copy it has (#5524).
-- Why a held album is wanted again, and the copy it has.
w.upgrade, w.held_quality,
-- A request names an album before the catalogue has read it: the
-- catalogue's type and date fill in once it has.
@@ -196,10 +196,10 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
u.username AS requested_by,
la.id AS library_artist_id,
COUNT(*) OVER () AS total,
-- The search schedule's last look (#5521), and the download running.
-- The search schedule's last look, and the download running.
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
-- Its latest download, running or ended, and what the import made of
-- it (#5522).
-- it.
gr.id AS grab_id, gr.name AS grab_name, gr.grabbed_at, gr.origin AS grab_origin,
gr.quality AS grab_quality, gr.ended_at AS grab_ended_at, gr.end_reason AS grab_end_reason,
gr.progress AS grab_progress, gr.import_state AS grab_import_state,
@@ -219,7 +219,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
END
AND (sqlc.arg(artist_mbid)::text = '' OR w.artist_mbid = sqlc.arg(artist_mbid)::text)
-- want_ids narrows to given wants: the Missing page's folders (M504).
-- want_ids narrows to given wants: the Missing page's folders.
AND (COALESCE(cardinality(sqlc.arg(want_ids)::uuid[]), 0) = 0 OR w.id = ANY(sqlc.arg(want_ids)::uuid[]))
AND (sqlc.arg(q)::text = ''
OR strpos(lower(w.artist_name), lower(sqlc.arg(q)::text)) > 0
@@ -230,7 +230,7 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
-- name: ListWantArtists :many
-- The Wanted view's main page (#5572): every artist on the list, A–Z like
-- The Wanted view's main page: every artist on the list, A–Z like
-- the library, with how many of their albums are held of how many are on
-- the list. q matches the artist's name or any of their albums' titles;
-- artist_mbid narrows to one artist, for their page's header.
@@ -263,7 +263,7 @@ SELECT a.artist_mbid, a.artist_name::text AS artist_name, a.held, a.wanted, a.mi
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
-- name: GetWantAlbum :one
-- What the Wanted view's track list needs of a want (#5572).
-- What the Wanted view's track list needs of a want.
SELECT id, release_group_mbid, album_id FROM wants WHERE id = $1;
-- name: ListLibraryAlbumTracks :many
@@ -278,15 +278,15 @@ SELECT title, disc_number, track_number, duration_ms, (missing_since IS NOT NULL
SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::bigint AS wanted,
COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason = 'missing')::bigint AS missing,
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
-- Held albums wanted for a better copy (#5524).
-- Held albums wanted for a better copy.
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL AND upgrade <> '')::bigint AS upgrades,
COUNT(DISTINCT artist_mbid)::bigint AS artists,
-- Library albums waiting to be matched by hand (#5579).
-- Library albums waiting to be matched by hand.
(SELECT COUNT(*) FROM library_album_matches m
WHERE m.outcome = 'unmatched'
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = m.album_id))::bigint AS unmatched,
(SELECT COUNT(*) FROM follows)::bigint AS following,
-- The Wanted view's other tabs (#5522): downloads running, those
-- The Wanted view's other tabs: downloads running, those
-- waiting on a person, and releases set aside.
(SELECT COUNT(*) FROM grabs WHERE ended_at IS NULL)::bigint AS activity,
(SELECT COUNT(*) FROM grabs
@@ -295,7 +295,7 @@ SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::big
WHERE b.expires_at IS NULL OR b.expires_at > now())::bigint AS blocked
FROM wants;
-- The one-time listing of the library (#5567). ---------------------------
-- The one-time listing of the library. ---------------------------
-- name: GetLibraryListing :one
SELECT library_listing_started_at, library_listed_at, library_unlisted
@@ -332,7 +332,7 @@ ON CONFLICT (release_group_mbid) DO NOTHING;
-- name: RecordListedAlbums :exec
-- The albums ListLibraryAlbums takes are decided as listed by their tags,
-- so the matching that follows leaves them alone (#5569).
-- so the matching that follows leaves them alone.
INSERT INTO library_album_matches (album_id, outcome, method, release_group_mbid)
SELECT a.id, 'listed', 'tags', a.release_group_mbid
FROM albums a
@@ -346,9 +346,9 @@ ON CONFLICT (album_id) DO NOTHING;
-- name: ListAlbumsToMatch :many
-- Library albums the listing has not decided, or that an older matcher
-- refused, or whose tracks changed since they were refused: each is matched
-- to MusicBrainz (#5569). A library album is one from before the cutoff, or
-- to MusicBrainz. A library album is one from before the cutoff, or
-- one whose tracks were: a re-tag files a track under a new album row but
-- keeps the track (#5579). An album row with no tracks is not an album in
-- keeps the track. An album row with no tracks is not an album in
-- the library; one a person left off stays off.
SELECT a.id, a.title, a.mbid, a.release_group_mbid, ar.name AS artist_name, ar.mbid AS artist_mbid
FROM albums a
@@ -423,7 +423,7 @@ UPDATE acquisition_settings
RETURNING library_listed_at, library_unlisted;
-- name: ListUnmatchedLibraryAlbums :many
-- The Collection's Unmatched section (#5579): the albums the listing could
-- The Collection's Unmatched section: the albums the listing could
-- not match, and below them the ones a person left off. An album whose
-- tracks have all moved to another row is gone, not unmatched.
SELECT m.album_id, a.title, ar.name AS artist_name, m.detail, m.outcome, m.decided_at,
@@ -459,7 +459,7 @@ UPDATE library_album_matches SET outcome = sqlc.arg(outcome), decided_at = now()
WHERE album_id = sqlc.arg(album_id) AND outcome = sqlc.arg(was);
-- Upgrades (#5524). -------------------------------------------------------
-- Upgrades. -------------------------------------------------------
-- name: ListHeldWantFormats :many
-- What each held album's copy is made of, for judging whether it is worth
+1 -1
View File
@@ -1,4 +1,4 @@
-- "You might like" Home rows (#790). The daily build computes ranked
-- "You might like" Home rows. The daily build computes ranked
-- album/artist IDs in Go (similarity roll-up + cold-start gate) and
-- atomic-replaces them here; /api/home reads them back hydrated.