feat(10-02): add sqlc queries for libraries and update playlist queries for phantom support

- Create libraries.sql with 7 CRUD queries (create, get, get-by-path, list, update, delete, count)
- Update playlists.sql: AddPlaylistTrack now accepts 9 params including phantom metadata
- Update playlist queries to use LEFT JOIN for nullable audio_file_id
- Add GetTrackPhantomMetadata helper query for eager phantom population
- Add is_phantom computed column to metadata queries
- Add GetAudioFilesByLibrary and CountAudioFilesByLibrary queries
- Regenerate all sqlc code
This commit is contained in:
2026-03-09 09:47:30 -04:00
parent 2c77368482
commit 02548dd55e
6 changed files with 385 additions and 51 deletions
@@ -127,6 +127,12 @@ SELECT id, file_path, title, artist_name
FROM track_metadata
WHERE file_path IN (sqlc.slice('paths'));
-- name: GetAudioFilesByLibrary :many
SELECT * FROM audio_files WHERE library_id = ?;
-- name: CountAudioFilesByLibrary :one
SELECT COUNT(*) AS count FROM audio_files WHERE library_id = ?;
-- name: DeleteAllAudioFiles :exec
DELETE FROM audio_files;
@@ -0,0 +1,21 @@
-- name: CreateLibrary :one
INSERT INTO libraries (name, path) VALUES (?, ?)
RETURNING *;
-- name: GetLibrary :one
SELECT * FROM libraries WHERE id = ? LIMIT 1;
-- name: GetLibraryByPath :one
SELECT * FROM libraries WHERE path = ? LIMIT 1;
-- name: GetAllLibraries :many
SELECT * FROM libraries ORDER BY name;
-- name: UpdateLibraryName :exec
UPDATE libraries SET name = ? WHERE id = ?;
-- name: DeleteLibrary :exec
DELETE FROM libraries WHERE id = ?;
-- name: CountLibraries :one
SELECT COUNT(*) AS count FROM libraries;
+56 -21
View File
@@ -18,13 +18,20 @@ DELETE FROM playlists WHERE id = ?;
SELECT COUNT(*) AS count FROM playlists WHERE name = ?;
-- name: AddPlaylistTrack :one
INSERT INTO playlist_tracks (playlist_id, audio_file_id, position) VALUES (?, ?, ?)
INSERT INTO playlist_tracks (
playlist_id, audio_file_id, position,
phantom_title, phantom_artist, phantom_album,
phantom_duration_ms, phantom_genre, phantom_cover_art_path
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: GetPlaylistTracks :many
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position, af.file_path
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position,
COALESCE(af.file_path, '') AS file_path,
pt.phantom_title, pt.phantom_artist, pt.phantom_album,
pt.phantom_duration_ms, pt.phantom_genre, pt.phantom_cover_art_path
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ?
ORDER BY pt.position;
@@ -40,14 +47,15 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
@@ -66,14 +74,15 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
@@ -93,16 +102,16 @@ SELECT COALESCE(MAX(position), -1) + 1 AS next_position
FROM playlist_tracks WHERE playlist_id = ?;
-- name: GetPlaylistTrackFilePaths :many
SELECT af.file_path
SELECT COALESCE(af.file_path, '') AS file_path
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ?
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ? AND pt.audio_file_id IS NOT NULL
ORDER BY pt.position;
-- name: IsTrackInPlaylist :one
SELECT EXISTS(
SELECT 1 FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ? AND af.file_path = ?
) AS in_playlist;
@@ -111,3 +120,29 @@ DELETE FROM playlist_tracks
WHERE playlist_id = ? AND audio_file_id = (
SELECT id FROM audio_files WHERE file_path = ?
);
-- name: GetTrackPhantomMetadata :one
SELECT
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
af.length_milliseconds AS duration_ms,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(ca.file_path, '') AS cover_art_path
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
WHERE af.id = ?;
@@ -22,6 +22,17 @@ func (q *Queries) CountAudioFiles(ctx context.Context) (int64, error) {
return count, err
}
const countAudioFilesByLibrary = `-- name: CountAudioFilesByLibrary :one
SELECT COUNT(*) AS count FROM audio_files WHERE library_id = ?
`
func (q *Queries) CountAudioFilesByLibrary(ctx context.Context, libraryID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countAudioFilesByLibrary, libraryID)
var count int64
err := row.Scan(&count)
return count, err
}
const createAudioFile = `-- name: CreateAudioFile :one
INSERT INTO audio_files (file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id
@@ -358,6 +369,46 @@ func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (Audi
return i, err
}
const getAudioFilesByLibrary = `-- name: GetAudioFilesByLibrary :many
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id FROM audio_files WHERE library_id = ?
`
func (q *Queries) GetAudioFilesByLibrary(ctx context.Context, libraryID int64) ([]AudioFile, error) {
rows, err := q.db.QueryContext(ctx, getAudioFilesByLibrary, libraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AudioFile
for rows.Next() {
var i AudioFile
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.LengthMilliseconds,
&i.FileTypeID,
&i.RecordingID,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.Basename,
&i.LibraryID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAudioFilesByReleaseGroup = `-- name: GetAudioFilesByReleaseGroup :many
SELECT
af.file_path,
@@ -0,0 +1,130 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: libraries.sql
package sqlcgen
import (
"context"
)
const countLibraries = `-- name: CountLibraries :one
SELECT COUNT(*) AS count FROM libraries
`
func (q *Queries) CountLibraries(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countLibraries)
var count int64
err := row.Scan(&count)
return count, err
}
const createLibrary = `-- name: CreateLibrary :one
INSERT INTO libraries (name, path) VALUES (?, ?)
RETURNING id, name, path, created_at
`
type CreateLibraryParams struct {
Name string
Path string
}
func (q *Queries) CreateLibrary(ctx context.Context, arg CreateLibraryParams) (Library, error) {
row := q.db.QueryRowContext(ctx, createLibrary, arg.Name, arg.Path)
var i Library
err := row.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.CreatedAt,
)
return i, err
}
const deleteLibrary = `-- name: DeleteLibrary :exec
DELETE FROM libraries WHERE id = ?
`
func (q *Queries) DeleteLibrary(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteLibrary, id)
return err
}
const getAllLibraries = `-- name: GetAllLibraries :many
SELECT id, name, path, created_at FROM libraries ORDER BY name
`
func (q *Queries) GetAllLibraries(ctx context.Context) ([]Library, error) {
rows, err := q.db.QueryContext(ctx, getAllLibraries)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Library
for rows.Next() {
var i Library
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLibrary = `-- name: GetLibrary :one
SELECT id, name, path, created_at FROM libraries WHERE id = ? LIMIT 1
`
func (q *Queries) GetLibrary(ctx context.Context, id int64) (Library, error) {
row := q.db.QueryRowContext(ctx, getLibrary, id)
var i Library
err := row.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.CreatedAt,
)
return i, err
}
const getLibraryByPath = `-- name: GetLibraryByPath :one
SELECT id, name, path, created_at FROM libraries WHERE path = ? LIMIT 1
`
func (q *Queries) GetLibraryByPath(ctx context.Context, path string) (Library, error) {
row := q.db.QueryRowContext(ctx, getLibraryByPath, path)
var i Library
err := row.Scan(
&i.ID,
&i.Name,
&i.Path,
&i.CreatedAt,
)
return i, err
}
const updateLibraryName = `-- name: UpdateLibraryName :exec
UPDATE libraries SET name = ? WHERE id = ?
`
type UpdateLibraryNameParams struct {
Name string
ID int64
}
func (q *Queries) UpdateLibraryName(ctx context.Context, arg UpdateLibraryNameParams) error {
_, err := q.db.ExecContext(ctx, updateLibraryName, arg.Name, arg.ID)
return err
}
+121 -30
View File
@@ -11,18 +11,38 @@ import (
)
const addPlaylistTrack = `-- name: AddPlaylistTrack :one
INSERT INTO playlist_tracks (playlist_id, audio_file_id, position) VALUES (?, ?, ?)
INSERT INTO playlist_tracks (
playlist_id, audio_file_id, position,
phantom_title, phantom_artist, phantom_album,
phantom_duration_ms, phantom_genre, phantom_cover_art_path
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, playlist_id, audio_file_id, position, phantom_title, phantom_artist, phantom_album, phantom_duration_ms, phantom_genre, phantom_cover_art_path
`
type AddPlaylistTrackParams struct {
PlaylistID int64
AudioFileID sql.NullInt64
Position int64
PlaylistID int64
AudioFileID sql.NullInt64
Position int64
PhantomTitle sql.NullString
PhantomArtist sql.NullString
PhantomAlbum sql.NullString
PhantomDurationMs sql.NullInt64
PhantomGenre sql.NullString
PhantomCoverArtPath sql.NullString
}
func (q *Queries) AddPlaylistTrack(ctx context.Context, arg AddPlaylistTrackParams) (PlaylistTrack, error) {
row := q.db.QueryRowContext(ctx, addPlaylistTrack, arg.PlaylistID, arg.AudioFileID, arg.Position)
row := q.db.QueryRowContext(ctx, addPlaylistTrack,
arg.PlaylistID,
arg.AudioFileID,
arg.Position,
arg.PhantomTitle,
arg.PhantomArtist,
arg.PhantomAlbum,
arg.PhantomDurationMs,
arg.PhantomGenre,
arg.PhantomCoverArtPath,
)
var i PlaylistTrack
err := row.Scan(
&i.ID,
@@ -100,14 +120,15 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
@@ -131,6 +152,7 @@ type GetAllPlaylistTracksWithMetadataRow struct {
Artist string
Album string
CoverArtPath string
IsPhantom int64
}
func (q *Queries) GetAllPlaylistTracksWithMetadata(ctx context.Context) ([]GetAllPlaylistTracksWithMetadataRow, error) {
@@ -153,6 +175,7 @@ func (q *Queries) GetAllPlaylistTracksWithMetadata(ctx context.Context) ([]GetAl
&i.Artist,
&i.Album,
&i.CoverArtPath,
&i.IsPhantom,
); err != nil {
return nil, err
}
@@ -228,10 +251,10 @@ func (q *Queries) GetPlaylist(ctx context.Context, id int64) (Playlist, error) {
}
const getPlaylistTrackFilePaths = `-- name: GetPlaylistTrackFilePaths :many
SELECT af.file_path
SELECT COALESCE(af.file_path, '') AS file_path
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ?
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ? AND pt.audio_file_id IS NOT NULL
ORDER BY pt.position
`
@@ -259,19 +282,28 @@ func (q *Queries) GetPlaylistTrackFilePaths(ctx context.Context, playlistID int6
}
const getPlaylistTracks = `-- name: GetPlaylistTracks :many
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position, af.file_path
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position,
COALESCE(af.file_path, '') AS file_path,
pt.phantom_title, pt.phantom_artist, pt.phantom_album,
pt.phantom_duration_ms, pt.phantom_genre, pt.phantom_cover_art_path
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ?
ORDER BY pt.position
`
type GetPlaylistTracksRow struct {
ID int64
PlaylistID int64
AudioFileID sql.NullInt64
Position int64
FilePath string
ID int64
PlaylistID int64
AudioFileID sql.NullInt64
Position int64
FilePath string
PhantomTitle sql.NullString
PhantomArtist sql.NullString
PhantomAlbum sql.NullString
PhantomDurationMs sql.NullInt64
PhantomGenre sql.NullString
PhantomCoverArtPath sql.NullString
}
func (q *Queries) GetPlaylistTracks(ctx context.Context, playlistID int64) ([]GetPlaylistTracksRow, error) {
@@ -289,6 +321,12 @@ func (q *Queries) GetPlaylistTracks(ctx context.Context, playlistID int64) ([]Ge
&i.AudioFileID,
&i.Position,
&i.FilePath,
&i.PhantomTitle,
&i.PhantomArtist,
&i.PhantomAlbum,
&i.PhantomDurationMs,
&i.PhantomGenre,
&i.PhantomCoverArtPath,
); err != nil {
return nil, err
}
@@ -309,14 +347,15 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom
FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
@@ -341,6 +380,7 @@ type GetPlaylistTracksWithMetadataRow struct {
Artist string
Album string
CoverArtPath string
IsPhantom int64
}
func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID int64) ([]GetPlaylistTracksWithMetadataRow, error) {
@@ -363,6 +403,7 @@ func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID
&i.Artist,
&i.Album,
&i.CoverArtPath,
&i.IsPhantom,
); err != nil {
return nil, err
}
@@ -377,10 +418,60 @@ func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID
return items, nil
}
const getTrackPhantomMetadata = `-- name: GetTrackPhantomMetadata :one
SELECT
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
af.length_milliseconds AS duration_ms,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(ca.file_path, '') AS cover_art_path
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
WHERE af.id = ?
`
type GetTrackPhantomMetadataRow struct {
Title string
Artist string
Album string
DurationMs int64
Genre string
CoverArtPath string
}
func (q *Queries) GetTrackPhantomMetadata(ctx context.Context, id int64) (GetTrackPhantomMetadataRow, error) {
row := q.db.QueryRowContext(ctx, getTrackPhantomMetadata, id)
var i GetTrackPhantomMetadataRow
err := row.Scan(
&i.Title,
&i.Artist,
&i.Album,
&i.DurationMs,
&i.Genre,
&i.CoverArtPath,
)
return i, err
}
const isTrackInPlaylist = `-- name: IsTrackInPlaylist :one
SELECT EXISTS(
SELECT 1 FROM playlist_tracks pt
JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
WHERE pt.playlist_id = ? AND af.file_path = ?
) AS in_playlist
`