feat: navidrome playlists tab, favorites, dedicated playlist tables
TUI:
- Move Playlists under the My Music tab and add a Favorites sub-tab
(Tracks / Albums / Artists / Favorites / Playlists), backed by the
Navidrome (Subsonic-compatible) API.
- Favorites list the user's starred songs and stream via Navidrome; they
can be played, queued (next/last/insert), and added to playlists.
- Playlists: create, delete, play, add/remove tracks; add the selected
track to a playlist (;), and save the current queue as a playlist (Q -> s).
- Like/unlike now goes through the Navidrome star/unstar API (keyed by
track id) for both the selection and the currently playing track.
- Search: Ctrl+N / Ctrl+L queue the selected upload result; every queue
source now carries a trackId so export/star works.
- Infinite scroll for tracks/albums/artists; R force-refreshes feeds.
Navidrome crate:
- Real createPlaylist / updatePlaylist / deletePlaylist endpoints, now
backed by dedicated navidrome_playlists / navidrome_playlist_tracks
tables (isolated from playlists ingested from other sources).
- Fix getPlaylist crashing when tracks exist (select the storage columns
TrackWithUpload requires via the shared TRACK_SELECT).
DB: add drizzle schema + migration 0015_navidrome_playlists.
Docs/CI: README TUI section + aligned shortcut table, FlakeHub badge,
ubuntu-24.04-arm runner, cachix cache.
feat(remote-ws): Elixir/Phoenix 1:1 port of the player remote-control WS
Phase 1 of migrating apps/api/src/websocket/handler.ts to Elixir/Phoenix/Ecto.
The existing Node /ws server stays authoritative; this is a parallel, wire- and
NATS-compatible reimplementation.
New service at remote-ws/ (OTP app :remote_ws):
- Raw-JSON WebSocket at GET /ws via WebSock+Bandit (not Phoenix Channels) to keep
the exact wire protocol: ping/pong, register, command, message(track|status).
- RemoteWs.Devices: duplicate Registry keyed by DID, replacing the Node
devices/deviceNames/userDevices maps (auto-cleans on disconnect).
- RemoteWs.StopDebouncer: GenServer port of the 15s song.stopped debounce.
- RemoteWs.NowPlaying: faithful port of the enrichment + ws_lastsong gating,
same Redis keys/TTLs and song.changed/song.stopped NATS payloads.
- RemoteWs.Auth: Joken HS256 (ignore-expiration) + access-token jti revoke check.
- Redis/NATS/Store behind behaviours (Redix, Gnat, Ecto) so the gating logic is
unit-tested against in-memory doubles — 29 tests, no live services.
- Reuses the same env var names as apps/api (JWT_SECRET, XATA_POSTGRES_URL,
REDIS_URL, NATS_URL); listen port is REMOTE_WS_PORT.
Ops:
- systemd/rocksky-remote-ws.service
- CI: remote-ws job added to .github/workflows/tests.yml (paths-filtered)