3.0 KiB
3.0 KiB
005 — Smart Playlists
Rule-based dynamic playlists: saved queries that evaluate against the library on demand, with a typeable combobox rule editor, live preview, and queue-snapshot-on-play semantics. AND-only logic with multi-value "is any of" — no nested boolean groups.
Shipped: 2026-03-22 · Slices: S01-S04 (M002 in GSD)
What landed
- S01 — Schema & rule engine. Migration 9 adds
is_smart+smart_rules(JSON) columns to the existingplayliststable — extends, doesn't duplicate.backend/smartplaylist/builds parameterized WHERE clauses via a hardcodedfieldMapwhitelist (16 columns fromtrack_metadata). 7 text operators (is,is_not,contains,does_not_contain,starts_with,ends_with,is_any_of) and 5 numeric (is,is_not,greater_than,less_than,between). 49 unit tests cover all operators, the genre dual-path, and SQL-injection rejection. Optional result limit + sort (whitelisted fields,ORDER BY RANDOM()allowed). - S02 — CRUD & sidebar. sqlc regenerated for the new columns;
IsSmart boolpropagated throughplaylist.Summaryto TypeScript models. Smart playlists render with a filter icon and "Smart" badge. Newsmart-playlist-detailsLit element follows thegenre-detailspattern; refresh/play/shuffle wired. Drag-drop onto smart playlists blocked. - S03 — Rule editor UI. Reusable
yj-comboboxLitElement (typeable input, ARIA, keyboard nav).smart-playlist-editorbuilds rules row-by-row with field combobox, operator switch (text/numeric auto), value input. Live preview at 300 ms debounce callingPreviewSmartPlaylist. - S04 — Integration. Create → navigate →
auto-editflow — detail view awaits initial data load (avoidsMaxOpenConns(1)deadlock) before opening the editor.
Key decisions retained
- One playlists table —
is_smartflag, not a parallel table. One code path for listing. - Saved queries, not saved track lists — no
playlist_tracksrows for smart playlists; results evaluated on demand. - Snapshot to queue on play. Queue must be stable during playback, not dynamically re-evaluating.
- Genre dual-path. Exact ops (
is,is_not,is_any_of) use a subquery againstrecording_genres JOIN genresbecause theGROUP_CONCAT(g.name, '||')column intrack_metadatacan't match individual names. LIKE-based ops use the concatenated column. - Wails TS bindings for the 5 smart-playlist methods are manually maintained (
Service.d.ts/Service.js). No build-time check catches drift if Go signatures change.
Fragility notes
- Combobox blur-vs-click race. The
mousedown+preventDefault()+requestAnimationFramefallback incombobox.tsis battle-tested but brittle if anyone restructures option rendering into a separate shadow DOM. Re-verify click-to-select in any combobox refactor. MaxOpenConns(1)deadlock. Holding*sql.Rowsopen while calling functions that query the same*database.DBwill deadlock. Close rows explicitly before downstream calls; never rely ondeferhere.