feat(10-01): update SQL schema files for multi-library fresh installs

- Create _libraries.sql with libraries table (name, path, created_at)
- Add library_id FK column and index to audio_files.sql
- Update playlist_tracks.sql with nullable audio_file_id, SET NULL FK, and 6 phantom columns
- Add af.library_id to track_metadata VIEW
- Regenerate sqlc code for updated schemas
- Fix playlist.go to use sql.NullInt64 for nullable audio_file_id
This commit is contained in:
2026-03-09 09:36:47 -04:00
parent 6f44512ebc
commit 535855b383
8 changed files with 68 additions and 22 deletions
@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS libraries (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
+6 -1
View File
@@ -10,9 +10,14 @@ CREATE TABLE IF NOT EXISTS audio_files (
bitrate int NOT NULL DEFAULT 0, bitrate int NOT NULL DEFAULT 0,
file_size int NOT NULL DEFAULT 0, file_size int NOT NULL DEFAULT 0,
basename text NOT NULL DEFAULT '', basename text NOT NULL DEFAULT '',
library_id int NOT NULL DEFAULT 0,
FOREIGN KEY(file_type_id) REFERENCES file_types(id), FOREIGN KEY(file_type_id) REFERENCES file_types(id),
FOREIGN KEY(recording_id) REFERENCES recordings(id) FOREIGN KEY(recording_id) REFERENCES recordings(id),
FOREIGN KEY(library_id) REFERENCES libraries(id)
); );
CREATE INDEX IF NOT EXISTS idx_audio_files_recording_id CREATE INDEX IF NOT EXISTS idx_audio_files_recording_id
ON audio_files(recording_id); ON audio_files(recording_id);
CREATE INDEX IF NOT EXISTS idx_audio_files_library_id
ON audio_files(library_id);
@@ -1,10 +1,16 @@
CREATE TABLE IF NOT EXISTS playlist_tracks ( CREATE TABLE IF NOT EXISTS playlist_tracks (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
playlist_id INTEGER NOT NULL, playlist_id INTEGER NOT NULL,
audio_file_id INTEGER NOT NULL, audio_file_id INTEGER,
position INTEGER NOT NULL, position INTEGER NOT NULL,
phantom_title TEXT,
phantom_artist TEXT,
phantom_album TEXT,
phantom_duration_ms INTEGER,
phantom_genre TEXT,
phantom_cover_art_path TEXT,
FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE CASCADE FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE SET NULL
); );
CREATE INDEX IF NOT EXISTS idx_playlist_tracks_playlist_id CREATE INDEX IF NOT EXISTS idx_playlist_tracks_playlist_id
@@ -22,7 +22,8 @@ SELECT
af.bit_depth, af.bit_depth,
af.channels, af.channels,
af.bitrate, af.bitrate,
af.file_size af.file_size,
af.library_id
FROM audio_files af FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.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 artist_credit ac ON r.artist_credit_id = ac.id
@@ -24,7 +24,7 @@ func (q *Queries) CountAudioFiles(ctx context.Context) (int64, error) {
const createAudioFile = `-- name: CreateAudioFile :one 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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 RETURNING id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id
` `
type CreateAudioFileParams struct { type CreateAudioFileParams struct {
@@ -66,6 +66,7 @@ func (q *Queries) CreateAudioFile(ctx context.Context, arg CreateAudioFileParams
&i.Bitrate, &i.Bitrate,
&i.FileSize, &i.FileSize,
&i.Basename, &i.Basename,
&i.LibraryID,
) )
return i, err return i, err
} }
@@ -122,7 +123,7 @@ func (q *Queries) GetAllAudioFilePaths(ctx context.Context) ([]GetAllAudioFilePa
} }
const getAllAudioFiles = `-- name: GetAllAudioFiles :many const getAllAudioFiles = `-- name: GetAllAudioFiles :many
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename FROM audio_files 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
` `
func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error) { func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error) {
@@ -146,6 +147,7 @@ func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error) {
&i.Bitrate, &i.Bitrate,
&i.FileSize, &i.FileSize,
&i.Basename, &i.Basename,
&i.LibraryID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -307,7 +309,7 @@ func (q *Queries) GetAllTracksWithFullMetadata(ctx context.Context) ([]GetAllTra
} }
const getAudioFile = `-- name: GetAudioFile :one const getAudioFile = `-- name: GetAudioFile :one
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename FROM audio_files 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 id = ? LIMIT 1 WHERE id = ? LIMIT 1
` `
@@ -326,12 +328,13 @@ func (q *Queries) GetAudioFile(ctx context.Context, id int64) (AudioFile, error)
&i.Bitrate, &i.Bitrate,
&i.FileSize, &i.FileSize,
&i.Basename, &i.Basename,
&i.LibraryID,
) )
return i, err return i, err
} }
const getAudioFileByPath = `-- name: GetAudioFileByPath :one const getAudioFileByPath = `-- name: GetAudioFileByPath :one
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename FROM audio_files 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 file_path = ? LIMIT 1 WHERE file_path = ? LIMIT 1
` `
@@ -350,6 +353,7 @@ func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (Audi
&i.Bitrate, &i.Bitrate,
&i.FileSize, &i.FileSize,
&i.Basename, &i.Basename,
&i.LibraryID,
) )
return i, err return i, err
} }
@@ -448,7 +452,7 @@ func (q *Queries) GetAudioFilesByReleaseGroup(ctx context.Context, releaseGroupI
} }
const getAudioFilesNeedingMetadata = `-- name: GetAudioFilesNeedingMetadata :many const getAudioFilesNeedingMetadata = `-- name: GetAudioFilesNeedingMetadata :many
SELECT id, file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename FROM audio_files 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 recording_id = 0 WHERE recording_id = 0
` `
@@ -473,6 +477,7 @@ func (q *Queries) GetAudioFilesNeedingMetadata(ctx context.Context) ([]AudioFile
&i.Bitrate, &i.Bitrate,
&i.FileSize, &i.FileSize,
&i.Basename, &i.Basename,
&i.LibraryID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
+19 -4
View File
@@ -37,6 +37,7 @@ type AudioFile struct {
Bitrate int64 Bitrate int64
FileSize int64 FileSize int64
Basename string Basename string
LibraryID int64
} }
type CoverArt struct { type CoverArt struct {
@@ -56,6 +57,13 @@ type Genre struct {
Name string Name string
} }
type Library struct {
ID int64
Name string
Path string
CreatedAt time.Time
}
type PlayerState struct { type PlayerState struct {
ID int64 ID int64
Volume int64 Volume int64
@@ -72,10 +80,16 @@ type Playlist struct {
} }
type PlaylistTrack struct { type PlaylistTrack struct {
ID int64 ID int64
PlaylistID int64 PlaylistID int64
AudioFileID int64 AudioFileID sql.NullInt64
Position int64 Position int64
PhantomTitle sql.NullString
PhantomArtist sql.NullString
PhantomAlbum sql.NullString
PhantomDurationMs sql.NullInt64
PhantomGenre sql.NullString
PhantomCoverArtPath sql.NullString
} }
type Queue struct { type Queue struct {
@@ -155,4 +169,5 @@ type TrackMetadatum struct {
Channels int64 Channels int64
Bitrate int64 Bitrate int64
FileSize int64 FileSize int64
LibraryID int64
} }
+12 -5
View File
@@ -7,16 +7,17 @@ package sqlcgen
import ( import (
"context" "context"
"database/sql"
) )
const addPlaylistTrack = `-- name: AddPlaylistTrack :one 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) VALUES (?, ?, ?)
RETURNING id, playlist_id, audio_file_id, position 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 { type AddPlaylistTrackParams struct {
PlaylistID int64 PlaylistID int64
AudioFileID int64 AudioFileID sql.NullInt64
Position int64 Position int64
} }
@@ -28,6 +29,12 @@ func (q *Queries) AddPlaylistTrack(ctx context.Context, arg AddPlaylistTrackPara
&i.PlaylistID, &i.PlaylistID,
&i.AudioFileID, &i.AudioFileID,
&i.Position, &i.Position,
&i.PhantomTitle,
&i.PhantomArtist,
&i.PhantomAlbum,
&i.PhantomDurationMs,
&i.PhantomGenre,
&i.PhantomCoverArtPath,
) )
return i, err return i, err
} }
@@ -116,7 +123,7 @@ ORDER BY pt.playlist_id, pt.position
type GetAllPlaylistTracksWithMetadataRow struct { type GetAllPlaylistTracksWithMetadataRow struct {
ID int64 ID int64
PlaylistID int64 PlaylistID int64
AudioFileID int64 AudioFileID sql.NullInt64
Position int64 Position int64
FilePath string FilePath string
LengthMilliseconds int64 LengthMilliseconds int64
@@ -262,7 +269,7 @@ ORDER BY pt.position
type GetPlaylistTracksRow struct { type GetPlaylistTracksRow struct {
ID int64 ID int64
PlaylistID int64 PlaylistID int64
AudioFileID int64 AudioFileID sql.NullInt64
Position int64 Position int64
FilePath string FilePath string
} }
@@ -326,7 +333,7 @@ ORDER BY pt.position
type GetPlaylistTracksWithMetadataRow struct { type GetPlaylistTracksWithMetadataRow struct {
ID int64 ID int64
PlaylistID int64 PlaylistID int64
AudioFileID int64 AudioFileID sql.NullInt64
Position int64 Position int64
FilePath string FilePath string
LengthMilliseconds int64 LengthMilliseconds int64
+5 -4
View File
@@ -3,6 +3,7 @@ package playlist
import ( import (
"context" "context"
"database/sql"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -867,7 +868,7 @@ func (s *Service) ImportPlaylist(
s.db.Ctx, s.db.Ctx,
sqlcgen.AddPlaylistTrackParams{ sqlcgen.AddPlaylistTrackParams{
PlaylistID: created.ID, PlaylistID: created.ID,
AudioFileID: audioFile.ID, AudioFileID: sql.NullInt64{Int64: audioFile.ID, Valid: true},
Position: int64(position), Position: int64(position),
}, },
) )
@@ -1065,7 +1066,7 @@ func (s *Service) restoreSinglePlaylist(
s.db.Ctx, s.db.Ctx,
sqlcgen.AddPlaylistTrackParams{ sqlcgen.AddPlaylistTrackParams{
PlaylistID: playlistID, PlaylistID: playlistID,
AudioFileID: audioFile.ID, AudioFileID: sql.NullInt64{Int64: audioFile.ID, Valid: true},
Position: int64(position), Position: int64(position),
}, },
) )
@@ -1125,7 +1126,7 @@ func (s *Service) addSingleTrack(
s.db.Ctx, s.db.Ctx,
sqlcgen.AddPlaylistTrackParams{ sqlcgen.AddPlaylistTrackParams{
PlaylistID: playlistID, PlaylistID: playlistID,
AudioFileID: audioFile.ID, AudioFileID: sql.NullInt64{Int64: audioFile.ID, Valid: true},
Position: position, Position: position,
}, },
) )
@@ -1654,7 +1655,7 @@ func (s *Service) ResolvePhantomTracks(
s.db.Ctx, s.db.Ctx,
sqlcgen.AddPlaylistTrackParams{ sqlcgen.AddPlaylistTrackParams{
PlaylistID: playlistID, PlaylistID: playlistID,
AudioFileID: audioFile.ID, AudioFileID: sql.NullInt64{Int64: audioFile.ID, Valid: true},
Position: nextPos + int64(resolved), Position: nextPos + int64(resolved),
}, },
) )