- Rename milestone from 'Features & Extensibility' to 'Multi-Library Support' - Replace old phases 10-14 (TAG/SMRT/GAP/MB/LAYOUT/PLUG) with 20 multi-library requirements (LIB/LSCAN/VIEW/PLAY/DATA) - Move deferred features to Future Requirements - Update research files with multi-library research (Stack, Features, Architecture, Pitfalls) - Architecture: hybrid model (library_id on audio_files only), libraries in DB, sequential scanning, phantom tracks
119 lines
4.3 KiB
Markdown
119 lines
4.3 KiB
Markdown
# Architecture Research: Multi-Library Integration
|
|
|
|
**Researched:** 2026-03-08
|
|
**Confidence:** HIGH
|
|
|
|
## Design Decision: Hybrid Model
|
|
|
|
- **`library_id` on `audio_files` only** — physical file binding
|
|
- **Artists, albums, recordings, genres stay global** — shared reference data
|
|
- **Unified presentation by default** — optional library filter
|
|
- **Cross-library playlists** — playlists reference audio_file_id
|
|
- **Phantom tracks on removal** — playlist entries preserved with metadata
|
|
|
|
## Database Changes
|
|
|
|
### New Table: libraries
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS libraries (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL UNIQUE,
|
|
scan_concurrency TEXT NOT NULL DEFAULT 'auto',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_scanned_at DATETIME
|
|
);
|
|
```
|
|
|
|
### Migration 6: Multi-Library Support
|
|
|
|
Order of operations:
|
|
1. Create `libraries` table
|
|
2. Read TOML `DirectoryPath`, insert as default library
|
|
3. `ALTER TABLE audio_files ADD COLUMN library_id INTEGER NOT NULL DEFAULT {defaultLibID}`
|
|
4. Create index on `audio_files(library_id)`
|
|
5. Rebuild `playlist_tracks` with SET NULL FK + phantom columns
|
|
6. Drop and recreate `track_metadata` VIEW with `library_id`
|
|
7. Set `PRAGMA user_version = 6`
|
|
|
|
### playlist_tracks Rebuild (for phantom support)
|
|
|
|
```sql
|
|
CREATE TABLE playlist_tracks_new (
|
|
id INTEGER PRIMARY KEY,
|
|
playlist_id INTEGER NOT NULL,
|
|
audio_file_id INTEGER, -- NOW NULLABLE
|
|
position INTEGER NOT NULL,
|
|
phantom_file_path TEXT,
|
|
phantom_title TEXT,
|
|
phantom_artist TEXT,
|
|
phantom_album TEXT,
|
|
FOREIGN KEY(playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE SET NULL
|
|
);
|
|
```
|
|
|
|
Two-phase library removal:
|
|
1. Populate phantom metadata BEFORE deleting audio_files
|
|
2. Delete audio_files -> SET NULL triggers -> phantom columns preserve display info
|
|
|
|
### track_metadata VIEW (updated)
|
|
|
|
Add `af.library_id` to SELECT list. Same JOIN structure. Consumers get library_id for filtering.
|
|
|
|
## Scan Pipeline Changes
|
|
|
|
- `Scan()` -> `ScanLibrary(libraryID int64)` — accepts library ID, loads path from DB
|
|
- `ScanAllLibraries()` — sequential iteration, one at a time
|
|
- Orphan cleanup scoped to library being scanned
|
|
- Entity cache (artists, albums) remains per-scan and works correctly (shared entities)
|
|
- Progress events include library_id and library_name
|
|
|
|
## Orphan Cleanup (Library Removal)
|
|
|
|
Reference-counting bottom-up deletes in single transaction:
|
|
```
|
|
audio_files (library_id = X) -> DELETE
|
|
recordings (no remaining audio_files) -> DELETE
|
|
release_group_recordings (orphaned) -> DELETE
|
|
recording_genres (orphaned) -> DELETE
|
|
release_groups (no remaining recordings) -> DELETE
|
|
artist_credit (no remaining references) -> DELETE
|
|
artist_credit_artist (orphaned) -> DELETE
|
|
artists (no remaining credits) -> DELETE
|
|
genres (no remaining recording links) -> DELETE
|
|
cover_art (no remaining release_groups) -> DELETE
|
|
```
|
|
|
|
## FTS5 Search Index
|
|
|
|
Contentless FTS5 (`content=''`) works naturally:
|
|
- Search queries JOIN `search_index` on `track_metadata` (which now has `library_id`)
|
|
- Library-filtered search: add `AND tm.library_id = ?` to WHERE clause
|
|
- Stale entries after library removal filtered out by JOIN (same as current orphan behavior)
|
|
- Consider migrating to `contentless_delete=1` (SQLite 3.43.0+) for per-row DELETE support
|
|
|
|
## Frontend Architecture
|
|
|
|
- `libraryStore` gains: library list, active filter (null = all), persistence in localStorage
|
|
- Backend filtering (not frontend) — pass libraryID to backend queries
|
|
- `library-manager` component redesigned: library list view, add/remove/rename, per-library scan
|
|
- All browse views check active filter when fetching data
|
|
- New events: LibraryAdded, LibraryRemoved, LibraryRenamed
|
|
- Existing scan events gain library_id in payload
|
|
|
|
## Config Migration
|
|
|
|
- TOML `[Library].DirectoryPath` read once during migration 6, inserted as default library
|
|
- Post-migration: library management through DB only
|
|
- `ScanConcurrency` moves per-library (DB column) with global default fallback
|
|
- `SetLibraryDirectory()` and `GetLibraryDirectory()` deprecated
|
|
|
|
## Build Order
|
|
|
|
1. Schema & Migration (foundation)
|
|
2. Backend scan pipeline (per-library scanning)
|
|
3. Backend API (CRUD, filtered queries, events)
|
|
4. Frontend (library manager, filter, store updates)
|