--- phase: 13-library-views-phantom-tracks plan: 01 type: execute wave: 1 depends_on: [] files_modified: - backend/database/sql/queries/audio_files.sql - backend/database/sql/queries/release_groups.sql - backend/database/sql/queries/artists.sql - backend/database/sql/queries/genres.sql - backend/database/search.go - backend/library/query.go autonomous: true requirements: [VIEW-01, VIEW-02, VIEW-03, VIEW-04] must_haves: truths: - "Backend returns all tracks when no library filter is active (unified view)" - "Backend returns only tracks from a specific library when library_id is provided" - "Albums, artists, and genres are filtered to only show entities that have tracks in the selected library" - "FTS5 search returns results scoped to a specific library when library_id is provided" - "All existing unfiltered queries continue to work unchanged" artifacts: - path: "backend/database/sql/queries/audio_files.sql" provides: "GetAllTracksWithFullMetadataByLibrary query" contains: "WHERE af.library_id" - path: "backend/database/sql/queries/release_groups.sql" provides: "GetAllAlbumsWithDetailsByLibrary, GetAlbumsByArtistByLibrary queries" contains: "WHERE af.library_id" - path: "backend/database/sql/queries/artists.sql" provides: "GetAlbumArtistsByLibrary query" contains: "WHERE af.library_id" - path: "backend/database/sql/queries/genres.sql" provides: "GetAllGenresWithCountsByLibrary, GetTracksByGenreByLibrary queries" contains: "WHERE af.library_id" - path: "backend/database/search.go" provides: "SearchFTSTracksByLibrary method" contains: "AND tm.library_id" - path: "backend/library/query.go" provides: "GetAllTracksByLibrary, GetAllAlbumsByLibrary, GetAllArtistsByLibrary, GetAllGenresWithCountsByLibrary, GetTracksByGenreByLibrary, GetAlbumsByArtistByLibrary, GetAlbumTracksByLibrary, SearchTracksByLibrary methods" exports: ["GetAllTracksByLibrary", "SearchTracksByLibrary"] key_links: - from: "backend/library/query.go" to: "backend/database/sql/queries/*.sql" via: "sqlc-generated Queries methods" pattern: "l\\.db\\.Queries\\." - from: "backend/library/query.go" to: "backend/database/search.go" via: "l.db.SearchFTSTracksByLibrary" pattern: "SearchFTSTracksByLibrary" --- Add library-filtered SQL query variants for all browse views and FTS search so the frontend can request data scoped to a specific library. Purpose: Phase 13 requires backend filtering for 150K+ track collections (per CONTEXT.md locked decision). Every existing unfiltered query that powers a browse view needs a `ByLibrary` variant accepting a `library_id` parameter. Existing unfiltered queries remain unchanged for the "All Libraries" default view. Output: sqlc-generated query methods + Go wrapper methods on Library struct + filtered FTS search method on DB struct @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/13-library-views-phantom-tracks/13-CONTEXT.md From backend/library/query.go — existing types to reuse: ```go type Track struct { TrackName, ArtistName, TrackLength, FilePath string TrackNumber, DiscNumber int64 Album string; Genre []string; Year int64 Composer, FileType string SampleRate, BitDepth, Channels, Bitrate, FileSize int64 } type Artist struct { ID int64; Name string } type Album struct { ID int64; Name, ArtistName, CoverArtPath string CoverArtSmall, CoverArtMedium, CoverArtLarge string Year int64 } type GenreWithCount struct { Name string; TrackCount int64 } // Helper used by GetAllTracks, SearchTracks, GetTracksByGenre: func mapTrackRow(...) Track ``` From backend/database/search.go — existing FTS search: ```go type SearchTrackRow struct { FilePath string; LengthMilliseconds int64 Title, ArtistName string TrackNumber, DiscNumber sql.NullInt64 Album, Genre string; Year int64 Composer, FileType string SampleRate, BitDepth, Channels, Bitrate, FileSize int64 } func (d *DB) SearchFTSTracks(query string, limit int) ([]SearchTrackRow, error) ``` From track_metadata VIEW (already includes library_id): ```sql CREATE VIEW IF NOT EXISTS track_metadata AS SELECT af.id, af.file_path, ... af.library_id FROM audio_files af LEFT JOIN recordings r ON ... LEFT JOIN release_group_recordings rgr ON ... LEFT JOIN release_groups rg ON ... LEFT JOIN artist_credits ac ON ... LEFT JOIN artists a ON ... LEFT JOIN cover_art ca ON ... ``` Task 1: Add library-filtered sqlc queries for all browse views backend/database/sql/queries/audio_files.sql backend/database/sql/queries/release_groups.sql backend/database/sql/queries/artists.sql backend/database/sql/queries/genres.sql Add `ByLibrary` variants of each query used by browse views. Each variant is a copy of the existing query with an added `WHERE af.library_id = ?` condition (or equivalent JOIN condition). The `track_metadata` VIEW already includes `af.library_id` as the last column. **audio_files.sql** — add these queries: 1. `GetAllTracksWithFullMetadataByLibrary` — copy of `GetAllTracksWithFullMetadata` (line 75) but add `WHERE af.library_id = ?1` to the outer query. The existing query JOINs audio_files, so the filter goes on `af.library_id`. Use sqlc parameter annotation `-- :arg library_id`. 2. `GetAudioFilesByReleaseGroupByLibrary` — copy of `GetAudioFilesByReleaseGroup` (line 139) but add `AND af.library_id = ?` alongside the existing `WHERE rgr.release_group_id = ?`. **release_groups.sql** — add these queries: 3. `GetAllAlbumsWithDetailsByLibrary` — copy of `GetAllAlbumsWithDetails` (line 48). This query doesn't directly JOIN audio_files, so add an `EXISTS (SELECT 1 FROM audio_files af WHERE af.library_id = ? AND EXISTS (SELECT 1 FROM recordings r JOIN release_group_recordings rgr ON rgr.recording_id = r.id WHERE rgr.release_group_id = rg.id AND r.id = (SELECT recording_id FROM audio_files WHERE id = af.id)))` — actually simpler: add `WHERE rg.id IN (SELECT DISTINCT rgr2.release_group_id FROM release_group_recordings rgr2 JOIN recordings r2 ON r2.id = rgr2.recording_id JOIN audio_files af2 ON af2.recording_id = r2.id WHERE af2.library_id = ?)`. Check the existing query structure first and find the simplest approach. Likely: wrap the existing query body and add a subquery filter on `rg.id` to only include albums that have at least one track in the given library. 4. `GetAlbumsByArtistByLibrary` — copy of `GetAlbumsByArtist` (line 67). Add the same album-in-library subquery filter alongside the existing artist_id filter. **artists.sql** — add: 5. `GetAlbumArtistsByLibrary` — copy of `GetAlbumArtists` (line 34). Filter to only artists that have at least one album with at least one track in the given library. Use subquery: `WHERE a.id IN (SELECT DISTINCT ac2.artist_id FROM artist_credits ac2 JOIN release_groups rg2 ON rg2.artist_credit_id = ac2.id JOIN release_group_recordings rgr2 ON rgr2.release_group_id = rg2.id JOIN recordings r2 ON r2.id = rgr2.recording_id JOIN audio_files af2 ON af2.recording_id = r2.id WHERE af2.library_id = ?)`. **genres.sql** — add: 6. `GetAllGenresWithCountsByLibrary` — copy of `GetAllGenresWithCounts` (line 66). Filter track counts to only count tracks in the given library. The existing query JOINs through recording_genres → recordings → audio_files, so add `AND af.library_id = ?` to the existing JOINs. 7. `GetTracksByGenreByLibrary` — copy of `GetTracksByGenre` (line 26). Add `AND af.library_id = ?` alongside the existing genre name filter. After adding all queries, run `make generate` (or `sqlc generate` from `backend/database/`) to regenerate Go code. Verify compilation with `go build -tags webkit2_41 ./...`. **Important:** Do NOT modify existing queries — only add new ones. The unfiltered variants serve the "All Libraries" default view. cd /mnt/vault/dev/golang/yellowjacket && go generate ./backend/database/... && go build -tags webkit2_41 ./... Seven new sqlc queries exist (ByLibrary variants), sqlc generate succeeds, go build compiles cleanly Task 2: Add library-filtered Go query methods and FTS search backend/library/query.go backend/database/search.go **backend/library/query.go** — Add `ByLibrary` wrapper methods that mirror each existing method but accept `libraryID int64` and call the `ByLibrary` sqlc query variant. Reuse `mapTrackRow` and cover art URL resolution logic identically. Add these exported methods to the Library struct: 1. `GetAllTracksByLibrary(libraryID int64) ([]Track, error)` — calls `l.db.Queries.GetAllTracksWithFullMetadataByLibrary(l.ctx, libraryID)`, maps via `mapTrackRow`. Do NOT return `errNoTracksInLibrary` for empty result — a library with no tracks is valid (not an error). Return empty slice. 2. `GetAllAlbumsByLibrary(libraryID int64) ([]Album, error)` — calls `GetAllAlbumsWithDetailsByLibrary`, maps with cover art URL resolution. 3. `GetAllArtistsByLibrary(libraryID int64) ([]Artist, error)` — calls `GetAlbumArtistsByLibrary`. 4. `GetAlbumsByArtistByLibrary(artistID, libraryID int64) ([]Album, error)` — calls `GetAlbumsByArtistByLibrary`. 5. `GetAllGenresWithCountsByLibrary(libraryID int64) ([]GenreWithCount, error)` — calls `GetAllGenresWithCountsByLibrary`. 6. `GetTracksByGenreByLibrary(genreName string, libraryID int64) ([]Track, error)` — calls `GetTracksByGenreByLibrary`. 7. `GetAlbumTracksByLibrary(albumID, libraryID int64) ([]Track, error)` — calls `GetAudioFilesByReleaseGroupByLibrary`. 8. `SearchTracksByLibrary(query string, libraryID int64) ([]Track, error)` — calls `l.db.SearchFTSTracksByLibrary(query, searchTrackLimit, libraryID)`, maps via `mapTrackRow`. **backend/database/search.go** — Add `SearchFTSTracksByLibrary`: ```go func (d *DB) SearchFTSTracksByLibrary( query string, limit int, libraryID int64, ) ([]SearchTrackRow, error) ``` Copy from `SearchFTSTracks` but add `AND tm.library_id = ?` to the WHERE clause and pass `libraryID` as the third parameter. The hand-crafted SQL already JOINs `track_metadata tm ON tm.id = si.rowid`, so the filter is trivial. Add SAFETY comment following project convention. All methods must follow project conventions: - Error wrapping with `fmt.Errorf("...: %w", err)` - slog structured logging with method context - godot doc comments ending with period - Lines under 100 chars (break as needed) - nlreturn blank line after error returns Verify with `go build -tags webkit2_41 ./...` and `make lint`. cd /mnt/vault/dev/golang/yellowjacket && go build -tags webkit2_41 ./... && make lint Eight new Go methods on Library struct + one new SearchFTSTracksByLibrary on DB struct; all compile cleanly, lint passes. Wails binding generation will pick up the new exported methods automatically. - `go build -tags webkit2_41 ./...` compiles without errors - `make lint` passes (golangci-lint v2 with strict rules) - `sqlc generate` succeeds in backend/database/ - All seven new ByLibrary SQL queries exist in their respective .sql files - All eight new Go methods exist on Library struct - SearchFTSTracksByLibrary exists on DB struct - Existing unfiltered queries and methods are unchanged - Backend can serve track/album/artist/genre data filtered to a specific library_id - Backend can serve FTS5 search results filtered to a specific library_id - All new methods are Wails-bindable (exported, on exported struct) - No regression in existing unfiltered queries After completion, create `.planning/phases/13-library-views-phantom-tracks/13-01-SUMMARY.md`