Implements feedBack#636 item 2 (P1) — saved library filters that stay live,
the homelab primitive FeedBack was missing (Plex smart collections / Navidrome
.nsp / *arr custom filters).
A collection is a saved /api/library query surfaced as a registered library
provider, so it appears in the v3 source picker and inherits the whole Songs
UI (paging, stats, A–Z rail, art) with no new screen.
- Storage reuses the playlist subsystem: a `playlists.rules` JSON column
(additive, idempotent migration). A row with rules != NULL is a smart
collection; list_playlists + get_playlist filter `rules IS NULL`, so
collections are excluded from the manual-playlist list and read-only to
every playlist mutation that gates on get_playlist.
- SmartCollectionProvider (kind="local" — matched songs are local rows, so the
client's play/art paths stay on the local branch) delegates query_page/
query_stats/query_artists to the local DB with the stored rules applied;
tuning_names/get_art delegate straight through. Registered via a boot scan +
on create/update (replace=True) / delete.
- Rules mirror the raw /api/library query params; `_sanitize_collection_rules`
drops unknown keys and is applied at API ingress AND on provider load, so a
hand-edited / imported bad value can't crash a query.
- API: GET/POST/PUT/DELETE /api/collections. Frontend: a "+ Save as
collection" action in the v3 filter drawer (local provider + active filters
only) that names the current filter set and switches to it.
Reviewed by Codex; 3 findings fixed (local-kind playback path, save gated to
local provider, re-sanitize persisted rules).
Tests: tests/test_collections_api.py (CRUD, provider filtering, restart
re-registration, kind=local, corrupt-rule tolerance, playlist isolation),
tests/js/v3_collections.test.js.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>