248 lines
12 KiB
Markdown
248 lines
12 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
|
|
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/13-library-views-phantom-tracks/13-CONTEXT.md
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
|
|
|
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 ...
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add library-filtered sqlc queries for all browse views</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /mnt/vault/dev/golang/yellowjacket && go generate ./backend/database/... && go build -tags webkit2_41 ./...</automated>
|
|
</verify>
|
|
<done>Seven new sqlc queries exist (ByLibrary variants), sqlc generate succeeds, go build compiles cleanly</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add library-filtered Go query methods and FTS search</name>
|
|
<files>
|
|
backend/library/query.go
|
|
backend/database/search.go
|
|
</files>
|
|
<action>
|
|
**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`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /mnt/vault/dev/golang/yellowjacket && go build -tags webkit2_41 ./... && make lint</automated>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/13-library-views-phantom-tracks/13-01-SUMMARY.md`
|
|
</output>
|