release / govulncheck (push) Successful in 22s
release / web (push) Successful in 1m14s
release / go (push) Successful in 1m33s
release / integration (push) Successful in 4m36s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 5m24s
release / Build signed APK (releases and dev) (push) Canceled after 5m29s
albums.mbid is the release id (Picard's musicbrainz_albumid, one edition). Lidarr names albums by release group, so re-acquisition and request completion need that id too (#5241). Migration 0070 adds albums.release_group_mbid (nullable, non-unique index: several releases share a group). The scanner reads musicbrainz_releasegroupid through extractReleaseGroupMBID, writes it on insert and heals it onto existing rows when NULL. tagReadVersion goes to 3 so the next scan fills it for the library already indexed, bound by tag reads (no ffprobe, no decode). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
380 lines
12 KiB
Go
380 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 (#367). 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 — the two
|
|
// diverging is exactly the bug #367 had to fix in ListAlbumsByGenre.
|
|
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` (#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
|
|
// 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.
|
|
//
|
|
// 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.
|
|
//
|
|
// 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).
|
|
//
|
|
// 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 for v1, 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.
|
|
// 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
|
|
}
|