diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 86a6d57..eda5690 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -23,9 +23,9 @@ Requirements for v1.1 Multi-Library Support milestone. Each maps to roadmap phas ### Library Management -- [ ] **LIB-01**: User can add a new library directory via a folder picker dialog -- [ ] **LIB-02**: User can rename a library (display name) -- [ ] **LIB-03**: User can remove a library — tracks are deleted from DB, shared entities (artists, albums, genres) are cleaned up only if no other library references them +- [x] **LIB-01**: User can add a new library directory via a folder picker dialog +- [x] **LIB-02**: User can rename a library (display name) +- [x] **LIB-03**: User can remove a library — tracks are deleted from DB, shared entities (artists, albums, genres) are cleaned up only if no other library references them - [x] **LIB-04**: Libraries are stored in SQLite (not TOML config) with CRUD through the UI - [x] **LIB-05**: Existing single-directory config is migrated seamlessly to the libraries table on first run after upgrade - [ ] **LIB-06**: Library list is displayed in a management UI (settings or sidebar section) @@ -50,13 +50,13 @@ Requirements for v1.1 Multi-Library Support milestone. Each maps to roadmap phas - [ ] **PLAY-01**: Playlists can contain tracks from multiple libraries (cross-library playlists) - [ ] **PLAY-02**: When a library is removed, playlist entries for that library's tracks become phantom tracks (preserved with cached metadata, not cascade-deleted) - [ ] **PLAY-03**: Phantom tracks are visually distinguished in playlist views (e.g., greyed out, icon indicator) -- [ ] **PLAY-04**: Queue tracks from a removed library are cascade-deleted (queue is ephemeral) +- [x] **PLAY-04**: Queue tracks from a removed library are cascade-deleted (queue is ephemeral) ### Data Integrity - [x] **DATA-01**: Schema migration adds `libraries` table and `library_id` FK on `audio_files` -- [ ] **DATA-02**: Orphan cleanup after library removal: reference-counting bottom-up deletes for artists, albums, genres only referenced by removed library's tracks -- [ ] **DATA-03**: FTS5 index entries for removed tracks are cleaned up (handling contentless table limitations) +- [x] **DATA-02**: Orphan cleanup after library removal: reference-counting bottom-up deletes for artists, albums, genres only referenced by removed library's tracks +- [x] **DATA-03**: FTS5 index entries for removed tracks are cleaned up (handling contentless table limitations) - [x] **DATA-04**: All library operations are transactional — no partial state on failure ## Future Requirements @@ -180,9 +180,9 @@ Which phases cover which requirements. Updated during roadmap creation. | KEY-03 | Phase 9 | Complete | | KEY-04 | Phase 9 | Complete | | KEY-05 | Phase 9 | Complete | -| LIB-01 | Phase 12 | Pending | -| LIB-02 | Phase 12 | Pending | -| LIB-03 | Phase 12 | Pending | +| LIB-01 | Phase 12 | Complete | +| LIB-02 | Phase 12 | Complete | +| LIB-03 | Phase 12 | Complete | | LIB-04 | Phase 10 | Complete | | LIB-05 | Phase 10 | Complete | | LIB-06 | Phase 12 | Pending | @@ -198,15 +198,15 @@ Which phases cover which requirements. Updated during roadmap creation. | PLAY-01 | Phase 13 | Pending | | PLAY-02 | Phase 13 | Pending | | PLAY-03 | Phase 13 | Pending | -| PLAY-04 | Phase 12 | Pending | +| PLAY-04 | Phase 12 | Complete | | DATA-01 | Phase 10 | Complete | -| DATA-02 | Phase 12 | Pending | -| DATA-03 | Phase 12 | Pending | +| DATA-02 | Phase 12 | Complete | +| DATA-03 | Phase 12 | Complete | | DATA-04 | Phase 10 | Complete | **Coverage:** -- v1.1 requirements: 31 total (8 complete + 23 pending) -- Mapped to phases: 31/31 ✓ (Phase 9: 8 complete, Phases 10-13: 23 pending) +- v1.1 requirements: 31 total (19 complete + 12 pending) +- Mapped to phases: 31/31 ✓ (Phase 9: 8, Phase 10: 5, Phase 11: 4, Phase 12: 6 of 7 — 12 pending in Phase 12-13) - No orphaned requirements --- diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4a3a67b..989f072 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -94,7 +94,7 @@ Plans: 5. Queue tracks from a removed library are cascade-deleted; the queue continues playing from the next valid track **Plans:** 2 plans Plans: -- [ ] 12-01-PLAN.md — Backend CRUD API + orphan cleanup + queue compaction + events +- [x] 12-01-PLAN.md — Backend CRUD API + orphan cleanup + queue compaction + events - [ ] 12-02-PLAN.md — Frontend library management UI in settings + sidebar cleanup ### Phase 13: Library Views & Phantom Tracks @@ -124,7 +124,7 @@ Plans: | 9. Scan Cancellation & Keyboard Shortcuts | v1.1 | 5/5 | Complete | 2026-03-07 | | 10. Schema & Migration | 2/2 | Complete | 2026-03-09 | - | | 11. Per-Library Scan Pipeline | 3/3 | Complete | 2026-03-09 | - | -| 12. Library CRUD & Data Integrity | v1.1 | 0/2 | Planned | - | +| 12. Library CRUD & Data Integrity | v1.1 | 1/2 | In Progress | - | | 13. Library Views & Phantom Tracks | v1.1 | 0/? | Not started | - | --- diff --git a/.planning/STATE.md b/.planning/STATE.md index 7280f16..d67960a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -22,11 +22,11 @@ See: .planning/PROJECT.md (updated 2026-03-08) ## Current Position -Phase: 11 — Per-Library Scan Pipeline -Plan: 3 of 3 in Phase (Plan 03 complete) -Status: Phase 11 complete — all plans done -Progress: ████████████████░░░░ 3/5 phases complete (Phase 11 complete) -Last activity: 2026-03-09 — Completed 11-03 wire auto-scan and legacy cleanup +Phase: 12 — Library CRUD & Data Integrity +Plan: 1 of 2 in Phase (Plan 01 complete) +Status: Plan 12-01 complete — backend CRUD API implemented +Progress: ████████████████░░░░ 3/5 phases complete (Phase 12 in progress) +Last activity: 2026-03-12 — Completed 12-01 library CRUD backend API ### Phase Overview @@ -35,7 +35,7 @@ Last activity: 2026-03-09 — Completed 11-03 wire auto-scan and legacy cleanup | 9. Scan Cancellation & Keyboard Shortcuts | Complete (5/5 plans) ✅ | | 10. Schema & Migration | Complete (2/2 plans) ✅ | | 11. Per-Library Scan Pipeline | Complete (3/3 plans) ✅ | -| 12. Library CRUD & Data Integrity | Not started | +| 12. Library CRUD & Data Integrity | In progress (1/2 plans) | | 13. Library Views & Phantom Tracks | Not started | ## Performance Metrics @@ -55,6 +55,7 @@ Last activity: 2026-03-09 — Completed 11-03 wire auto-scan and legacy cleanup | Phase 11-01 P01 | 7 min | 2 tasks | 11 files | | Phase 11-03 P03 | 10 min | 1 task | 3 files | | Phase 11-02 P02 | 4 min | 2 tasks | 2 files | +| Phase 12-01 P01 | 6 min | 2 tasks | 6 files | ## Accumulated Context @@ -108,6 +109,16 @@ Decisions from v1.0 are archived in PROJECT.md Key Decisions table. Key patterns | LibraryConfigChanged handler removed entirely | Multi-library model uses CRUD API (Phase 12), not event-driven config updates | | Scan() wrapper deleted | Only callers were handleConfigUpdate (deleted) and FullRescan (updated to scanInternal) | +### Phase 12 Decisions + +| Decision | Rationale | +|----------|-----------| +| Application-level name uniqueness check (iterate GetAllLibraries) | Avoids migration 7; rename is infrequent, check is simple | +| RemovalHooks callback struct (StopPlayback + CompactQueue) | Mirrors RescanHooks pattern; breaks circular dependency between library, player, queue packages | +| querySingleInt64 helper for hand-crafted SQL aggregates | DB type has QueryContext (returns *sql.Rows) but no QueryRowContext; helper wraps scan-close cycle | +| Sentinel errors for all validation per err113 | errLibraryNameEmpty, errLibraryNameTooLong, errLibraryNameDuplicate, errLibraryPathNotExist | +| Pre-populate phantom metadata BEFORE cascade delete | Avoids lost join data — playlist_tracks need track metadata after audio_files rows are gone | + ### Warnings (carry forward) - Player lock ordering (`p.mu` before `speaker.Lock()`, goroutine dispatch in beep callback) — carry forward @@ -129,10 +140,10 @@ Decisions from v1.0 are archived in PROJECT.md Key Decisions table. Key patterns ### Last Session -**Date:** 2026-03-09 -**What happened:** Executed Phase 11, Plan 03 — wired auto-scan on app launch via ScanAllLibraries goroutine in OnDomReady, removed legacy LibraryConfigChanged handler, handleConfigUpdate, and deprecated Scan() wrapper. Updated FullRescan to resolve library from DB via GetAllLibraries. -**Where we stopped:** Completed 11-03-PLAN.md — Phase 11 complete -**Next action:** Plan Phase 12 (Library CRUD & Data Integrity) +**Date:** 2026-03-12 +**What happened:** Executed Phase 12, Plan 01 — implemented backend Library CRUD API (AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact) with full orphan cleanup pipeline, phantom track preservation, FTS5 rebuild, queue compaction via CompactAfterLibraryRemoval, and RemovalHooks wiring in app.go. +**Where we stopped:** Completed 12-01-PLAN.md — Plan 01 of Phase 12 complete +**Next action:** Execute Phase 12 Plan 02 (Frontend library management UI) --- *State initialized: 2026-02-27* @@ -145,4 +156,4 @@ Decisions from v1.0 are archived in PROJECT.md Key Decisions table. Key patterns | 18 | add multi-column metadata display to playlist-details | 2026-03-08 | ce23177 | [18-add-multi-column-metadata-display-to-pla](./quick/18-add-multi-column-metadata-display-to-pla/) | Last activity: 2026-03-08 - Completed quick task 18: add multi-column metadata display to playlist-details -*Last updated: 2026-03-09 — Completed 11-03-PLAN.md (Phase 11 complete)* +*Last updated: 2026-03-12 — Completed 12-01-PLAN.md (Phase 12 Plan 01 complete)* diff --git a/.planning/phases/12-library-crud-data-integrity/12-01-SUMMARY.md b/.planning/phases/12-library-crud-data-integrity/12-01-SUMMARY.md new file mode 100644 index 0000000..87e7594 --- /dev/null +++ b/.planning/phases/12-library-crud-data-integrity/12-01-SUMMARY.md @@ -0,0 +1,146 @@ +--- +phase: 12-library-crud-data-integrity +plan: 01 +subsystem: library +tags: [crud, orphan-cleanup, data-integrity, queue-compaction, phantom-tracks, events] + +# Dependency graph +requires: + - phase: 11-per-library-scan-pipeline + provides: ScanLibrary, ScanAllLibraries, scan queue coordinator + - phase: 10-schema-migration + provides: libraries table, library_id FK, phantom columns on playlist_tracks +provides: + - AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact backend API + - Orphan cleanup pipeline (recordings → genres → release_groups → artist_credits → artists → cover_art) + - Queue CompactAfterLibraryRemoval method + - Phantom metadata population before cascade delete + - LibraryAdded, LibraryRenamed, LibraryRemoved events +affects: [12-02-frontend-library-ui, 13-library-views-phantom-tracks] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "RemovalHooks callback struct — breaks circular dependency between library, player, and queue packages" + - "Bottom-up orphan cleanup in single transaction — reference-counting DELETE WHERE NOT IN subqueries" + - "Pre-populate phantom metadata BEFORE cascade delete — avoids lost join data" + - "querySingleInt64 helper for hand-crafted SQL returning single aggregate values" + +key-files: + created: + - backend/library/crud.go + modified: + - backend/library/library.go + - backend/events/events.go + - frontend/src/events.ts + - backend/queue/queue.go + - backend/app.go + +key-decisions: + - "Application-level name uniqueness check (iterate GetAllLibraries) rather than DB UNIQUE constraint — avoids migration 7" + - "RemovalHooks struct pattern (StopPlayback + CompactQueue callbacks) wired in app.go — mirrors existing RescanHooks pattern" + - "querySingleInt64 helper wraps DB.QueryContext returning *sql.Rows since DB has no QueryRowContext method" + - "Sentinel errors for all validation (errLibraryNameEmpty, errLibraryNameTooLong, errLibraryNameDuplicate, errLibraryPathNotExist) per err113 linter rule" + - "Context parameter placed first in querySingleInt64 per revive context-as-argument rule" + +patterns-established: + - "RemovalHooks callback struct for cross-package lifecycle coordination" + - "querySingleInt64 for hand-crafted aggregate SQL queries" + +requirements-completed: [LIB-01, LIB-02, LIB-03, DATA-02, DATA-03, PLAY-04] + +# Metrics +duration: 6min +completed: 2026-03-12 +--- + +# Phase 12 Plan 01: Library CRUD Backend API Summary + +**Backend CRUD API with AddLibrary/RenameLibrary/RemoveLibrary, full orphan cleanup pipeline, phantom track preservation, FTS5 rebuild, queue compaction, and event emission** + +## Performance + +- **Duration:** 6 min +- **Started:** 2026-03-12T23:32:27Z +- **Completed:** 2026-03-12T23:38:30Z +- **Tasks:** 2 +- **Files created:** 1 +- **Files modified:** 5 + +## Accomplishments + +- **AddLibrary(path)** — validates path exists, auto-names from folder base, creates DB row via sqlc, emits LibraryAdded, starts async ScanLibrary +- **RenameLibrary(id, newName)** — validates 1-50 char length, checks name uniqueness across all libraries (application-level), updates via sqlc, emits LibraryRenamed +- **GetRemovalImpact(libraryID)** — read-only queries returning track count, affected playlists count, queue items count for confirmation dialog +- **RemoveLibrary(id)** — the critical 23-step method: + 1. Cancel active scan for library + 2. Stop playback if current track belongs to library + 3. Pre-count metrics for summary + 4. Begin transaction + 5. Populate phantom metadata on playlist_tracks (BEFORE cascade delete) + 6. DELETE audio_files WHERE library_id (CASCADE on queue_tracks, SET NULL on playlist_tracks) + 7. Bottom-up orphan cleanup: recordings → recording_genres → release_group_recordings → release_groups → artist_credits (dual FK check) → artist_credit_artists → artists → genres → cover_art + 8. DELETE library row + 9. Commit transaction + 10. Post-commit: RebuildSearchIndex (FTS5), delete cover art files, CompactQueue, emit events +- **CompactAfterLibraryRemoval()** on Queue — reloads surviving tracks from DB, detects if current track survived, resets index, unloads player if needed, clears shuffle order, emits QueueChanged +- **RemovalHooks** wired in app.go: StopPlayback → player.UnloadTrack(), CompactQueue → queue.CompactAfterLibraryRemoval() +- Three new event constants: LibraryAdded, LibraryRenamed, LibraryRemoved — auto-generated to frontend events.ts + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Implement Library CRUD methods and orphan cleanup pipeline** — `bd44f83` (feat) + - Created backend/library/crud.go (525 lines) with all CRUD methods + - Added 3 event constants to backend/events/events.go + - Added removalHooks field to Library struct + - Regenerated frontend/src/events.ts +2. **Task 2: Add queue compaction method and wire removal hooks in app.go** — `5995dfd` (feat) + - Added CompactAfterLibraryRemoval() to backend/queue/queue.go (80 lines) + - Wired RemovalHooks in backend/app.go OnStartup + +## Files Created/Modified + +- `backend/library/crud.go` (NEW) — AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact, cancelLibraryScan, currentTrackBelongsToLibrary, querySingleInt64, RemovalHooks type, sentinel errors +- `backend/library/library.go` — Added removalHooks RemovalHooks field to Library struct +- `backend/events/events.go` — Added LibraryAdded, LibraryRenamed, LibraryRemoved constants +- `frontend/src/events.ts` — Regenerated with new library CRUD event constants +- `backend/queue/queue.go` — Added CompactAfterLibraryRemoval method +- `backend/app.go` — Wired RemovalHooks in OnStartup (StopPlayback + CompactQueue callbacks) + +## Decisions Made + +- **Application-level name uniqueness:** Iterate GetAllLibraries to check for duplicate names rather than adding a UNIQUE constraint to the libraries table. Avoids needing migration 7; the check is only done during rename which is infrequent. +- **RemovalHooks callback struct:** Follows the existing RescanHooks pattern to break circular dependencies between library → player and library → queue packages. Wired in app.go where all subsystems are accessible. +- **querySingleInt64 helper:** The project's `database.DB` type exposes `QueryContext` returning `*sql.Rows` but no `QueryRowContext`. The helper wraps the full scan-close cycle for single-value aggregate queries. +- **Sentinel errors per err113:** Defined `errLibraryNameEmpty`, `errLibraryNameTooLong`, `errLibraryNameDuplicate`, `errLibraryPathNotExist` as package-level vars to satisfy the golangci-lint err113 rule. +- **Context-first parameter order:** `querySingleInt64(ctx, db, query, args...)` follows `revive` linter's context-as-argument rule. + +## Deviations from Plan + +None — plan executed exactly as written. + +## Issues Encountered + +None. + +## User Setup Required + +None — no external service configuration required. + +## Next Plan Readiness + +- Plan 12-01 complete — backend CRUD API fully implemented +- Ready for Plan 12-02: Frontend library management UI in settings + sidebar cleanup +- All Wails-bindable methods (AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact) are available for frontend consumption +- Events (LibraryAdded, LibraryRenamed, LibraryRemoved) are defined for frontend reactive updates + +## Self-Check: PASSED + +All files verified present, all commits verified in git log. + +--- +*Phase: 12-library-crud-data-integrity* +*Completed: 2026-03-12*