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>
373 lines
12 KiB
Go
373 lines
12 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: browse.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const countAlbumsByGenre = `-- name: CountAlbumsByGenre :one
|
|
SELECT COUNT(*) FROM albums
|
|
WHERE EXISTS (
|
|
SELECT 1
|
|
FROM tracks
|
|
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
|
|
WHERE tracks.album_id = albums.id
|
|
AND tracks.missing_since IS NULL
|
|
AND trim(g.genre) = trim($1::text)
|
|
)
|
|
`
|
|
|
|
// Total for the paging envelope. EXISTS mirrors the list query exactly; a
|
|
// JOIN + DISTINCT here would count differently the moment an album has two
|
|
// tracks carrying the same genre.
|
|
func (q *Queries) CountAlbumsByGenre(ctx context.Context, genre string) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAlbumsByGenre, genre)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countAlbumsByYearRange = `-- name: CountAlbumsByYearRange :one
|
|
SELECT COUNT(*) FROM albums
|
|
WHERE release_date IS NOT NULL
|
|
AND EXTRACT(YEAR FROM release_date)::int
|
|
BETWEEN $1::int AND $2::int
|
|
AND EXISTS (
|
|
SELECT 1 FROM tracks
|
|
WHERE tracks.album_id = albums.id
|
|
AND tracks.missing_since IS NULL
|
|
)
|
|
`
|
|
|
|
type CountAlbumsByYearRangeParams struct {
|
|
YearFrom int32
|
|
YearTo int32
|
|
}
|
|
|
|
func (q *Queries) CountAlbumsByYearRange(ctx context.Context, arg CountAlbumsByYearRangeParams) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAlbumsByYearRange, arg.YearFrom, arg.YearTo)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const listAlbumYearsWithCount = `-- name: ListAlbumYearsWithCount :many
|
|
SELECT EXTRACT(YEAR FROM release_date)::int AS year, COUNT(*)::bigint AS album_count
|
|
FROM albums
|
|
WHERE release_date IS NOT NULL
|
|
AND EXISTS (
|
|
SELECT 1 FROM tracks
|
|
WHERE tracks.album_id = albums.id
|
|
AND tracks.missing_since IS NULL
|
|
)
|
|
GROUP BY year
|
|
ORDER BY year DESC
|
|
`
|
|
|
|
type ListAlbumYearsWithCountRow struct {
|
|
Year int32
|
|
AlbumCount int64
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListAlbumYearsWithCount(ctx context.Context) ([]ListAlbumYearsWithCountRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumYearsWithCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumYearsWithCountRow
|
|
for rows.Next() {
|
|
var i ListAlbumYearsWithCountRow
|
|
if err := rows.Scan(&i.Year, &i.AlbumCount); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsByGenreWithArtist = `-- name: ListAlbumsByGenreWithArtist :many
|
|
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, artists.name AS artist_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
WHERE EXISTS (
|
|
SELECT 1
|
|
FROM tracks
|
|
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
|
|
WHERE tracks.album_id = albums.id
|
|
AND tracks.missing_since IS NULL
|
|
AND trim(g.genre) = trim($1::text)
|
|
)
|
|
ORDER BY albums.sort_title, albums.id
|
|
LIMIT $3 OFFSET $2
|
|
`
|
|
|
|
type ListAlbumsByGenreWithArtistParams struct {
|
|
Genre string
|
|
Off int32
|
|
Lim int32
|
|
}
|
|
|
|
type ListAlbumsByGenreWithArtistRow struct {
|
|
Album Album
|
|
ArtistName string
|
|
}
|
|
|
|
// Albums for one genre, joined with artist_name for the browse grid.
|
|
// An album belongs to a genre when ANY of its tracks carry it. Splits and
|
|
// trims identically to ListGenresWithCount — if the list is built by
|
|
// splitting and the detail matched exactly, every multi-genre track would
|
|
// produce a genre row that leads to an empty page.
|
|
func (q *Queries) ListAlbumsByGenreWithArtist(ctx context.Context, arg ListAlbumsByGenreWithArtistParams) ([]ListAlbumsByGenreWithArtistRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsByGenreWithArtist, arg.Genre, arg.Off, arg.Lim)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsByGenreWithArtistRow
|
|
for rows.Next() {
|
|
var i ListAlbumsByGenreWithArtistRow
|
|
if err := rows.Scan(
|
|
&i.Album.ID,
|
|
&i.Album.Title,
|
|
&i.Album.SortTitle,
|
|
&i.Album.ArtistID,
|
|
&i.Album.ReleaseDate,
|
|
&i.Album.Mbid,
|
|
&i.Album.CoverArtPath,
|
|
&i.Album.CreatedAt,
|
|
&i.Album.UpdatedAt,
|
|
&i.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&i.ArtistName,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAlbumsByYearRangeWithArtist = `-- name: ListAlbumsByYearRangeWithArtist :many
|
|
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, artists.name AS artist_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
WHERE albums.release_date IS NOT NULL
|
|
AND EXTRACT(YEAR FROM albums.release_date)::int
|
|
BETWEEN $1::int AND $2::int
|
|
AND EXISTS (
|
|
SELECT 1 FROM tracks
|
|
WHERE tracks.album_id = albums.id
|
|
AND tracks.missing_since IS NULL
|
|
)
|
|
ORDER BY albums.sort_title, albums.id
|
|
LIMIT $4 OFFSET $3
|
|
`
|
|
|
|
type ListAlbumsByYearRangeWithArtistParams struct {
|
|
YearFrom int32
|
|
YearTo int32
|
|
Off int32
|
|
Lim int32
|
|
}
|
|
|
|
type ListAlbumsByYearRangeWithArtistRow struct {
|
|
Album Album
|
|
ArtistName string
|
|
}
|
|
|
|
// Albums released within an inclusive year range, for the albums-page filter.
|
|
func (q *Queries) ListAlbumsByYearRangeWithArtist(ctx context.Context, arg ListAlbumsByYearRangeWithArtistParams) ([]ListAlbumsByYearRangeWithArtistRow, error) {
|
|
rows, err := q.db.Query(ctx, listAlbumsByYearRangeWithArtist,
|
|
arg.YearFrom,
|
|
arg.YearTo,
|
|
arg.Off,
|
|
arg.Lim,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAlbumsByYearRangeWithArtistRow
|
|
for rows.Next() {
|
|
var i ListAlbumsByYearRangeWithArtistRow
|
|
if err := rows.Scan(
|
|
&i.Album.ID,
|
|
&i.Album.Title,
|
|
&i.Album.SortTitle,
|
|
&i.Album.ArtistID,
|
|
&i.Album.ReleaseDate,
|
|
&i.Album.Mbid,
|
|
&i.Album.CoverArtPath,
|
|
&i.Album.CreatedAt,
|
|
&i.Album.UpdatedAt,
|
|
&i.Album.CoverArtSource,
|
|
&i.Album.CoverArtSourcesVersion,
|
|
&i.Album.ReleaseGroupMbid,
|
|
&i.ArtistName,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listGenresForAlbum = `-- name: ListGenresForAlbum :many
|
|
SELECT DISTINCT trim(g.genre) AS genre
|
|
FROM tracks
|
|
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
|
|
WHERE tracks.album_id = $1 AND trim(g.genre) <> ''
|
|
AND tracks.missing_since IS NULL
|
|
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.
|
|
func (q *Queries) ListGenresForAlbum(ctx context.Context, albumID pgtype.UUID) ([]string, error) {
|
|
rows, err := q.db.Query(ctx, listGenresForAlbum, albumID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []string
|
|
for rows.Next() {
|
|
var genre string
|
|
if err := rows.Scan(&genre); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, genre)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listGenresForArtist = `-- name: ListGenresForArtist :many
|
|
SELECT DISTINCT trim(g.genre) AS genre
|
|
FROM tracks
|
|
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
|
|
WHERE tracks.artist_id = $1 AND trim(g.genre) <> ''
|
|
AND tracks.missing_since IS NULL
|
|
ORDER BY trim(g.genre)
|
|
`
|
|
|
|
// Same, across everything by one artist. Alphabetical rather than by count:
|
|
// an artist's genre set is small, and a stable order reads better than a
|
|
// frequency ranking nobody asked about.
|
|
func (q *Queries) ListGenresForArtist(ctx context.Context, artistID pgtype.UUID) ([]string, error) {
|
|
rows, err := q.db.Query(ctx, listGenresForArtist, artistID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []string
|
|
for rows.Next() {
|
|
var genre string
|
|
if err := rows.Scan(&genre); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, genre)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listGenresWithCount = `-- name: ListGenresWithCount :many
|
|
|
|
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
|
|
WHERE trim(g.genre) <> ''
|
|
AND tracks.missing_since IS NULL
|
|
GROUP BY trim(g.genre)
|
|
ORDER BY track_count DESC, trim(g.genre)
|
|
`
|
|
|
|
type ListGenresWithCountRow struct {
|
|
Genre string
|
|
TrackCount int64
|
|
}
|
|
|
|
// 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, 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 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.
|
|
//
|
|
// 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 —
|
|
// a track tagged "Rock;Pop" must count toward both, and diverging from the
|
|
// established pattern here would make the browse surface disagree with what
|
|
// the recommendation engine believes the library contains.
|
|
//
|
|
// 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, 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 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) {
|
|
rows, err := q.db.Query(ctx, listGenresWithCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListGenresWithCountRow
|
|
for rows.Next() {
|
|
var i ListGenresWithCountRow
|
|
if err := rows.Scan(&i.Genre, &i.TrackCount); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|