-- 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. -- name: ListGenresWithCount :many -- 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. 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) -- Ordered by the expression, not the output alias: `ORDER BY genre` is -- ambiguous between the alias and tracks.genre, and sqlc rejects it. ORDER BY track_count DESC, trim(g.genre); -- name: ListAlbumsByGenreWithArtist :many -- 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. SELECT sqlc.embed(albums), 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(sqlc.arg(genre)::text) ) ORDER BY albums.sort_title, albums.id LIMIT sqlc.arg(lim) OFFSET sqlc.arg(off); -- name: CountAlbumsByGenre :one -- 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. 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(sqlc.arg(genre)::text) ); -- name: ListAlbumYearsWithCount :many -- 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. 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; -- name: ListAlbumsByYearRangeWithArtist :many -- Albums released within an inclusive year range, for the albums-page filter. SELECT sqlc.embed(albums), 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 sqlc.arg(year_from)::int AND sqlc.arg(year_to)::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 sqlc.arg(lim) OFFSET sqlc.arg(off); -- name: CountAlbumsByYearRange :one SELECT COUNT(*) FROM albums WHERE release_date IS NOT NULL AND EXTRACT(YEAR FROM release_date)::int BETWEEN sqlc.arg(year_from)::int AND sqlc.arg(year_to)::int AND EXISTS ( SELECT 1 FROM tracks WHERE tracks.album_id = albums.id AND tracks.missing_since IS 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. 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); -- name: ListGenresForArtist :many -- 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. 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);