You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Status**: `spawn_status_poller` (1s loop) refreshes `PlayerStatus` and broadcasts; queue mutations broadcast immediately (`broadcast_queue_now`). `/api/ws` sends a Status+Queue snapshot on connect, then live `StatusEvent`s (tagged serde enum `{"type":"status"}` / `{"type":"queue"}`), lag-tolerant. MPD error `No such song/file/directory` → `spawn_blocking` DB delete + `next` + `clear_error`; MPD unreachable → status reset to stopped.
24
28
-**Frontend → backend**: single `api` object of typed fetch wrappers (`frontend/src/api.ts`, 69 methods); `usePlayerStatus` (`frontend/src/ws.ts`) holds one WS to `/api/ws` with backoff reconnect (1s → 10s max) plus a one-shot REST fallback (on constructor throw or a 1.5s open-timeout). `useVisualizer` opens a second WS to `/api/visualizer` (~40fps `{bins, level}` frames; the server seeds a zeroed baseline frame on connect, while the hook itself holds `null` until the first real frame — `Visualizer.tsx` renders idle-pulsing bars for `null` frames while playing; under prefers-reduced-motion it paints a single static frame (bars at idle height) and halts the loop, while the truly empty frame appears only when stopped (`playing === false`)).
25
29
-**DB/scanner work** is offloaded via `spawn_blocking` (SQLite is `Arc<Mutex<rusqlite::Connection>>`).
- Naming: snake_case modules/files, body structs `*Body`, handler return `AppResult<Json<..>>`.
88
94
89
95
### Frontend
96
+
90
97
- No router/state/data libraries — custom path routing in `App.tsx`; `types.ts` mirrors backend JSON exactly.
91
98
- One `api` object (`api.ts`) with a `json<T>()` helper that throws `Error(body.error)`; `toPlayRef(t)` builds the `{uri, start, end, track_id}` envelope for play-next/clear-play/playlist-add.
92
99
- Live state is WS-push driven (`usePlayerStatus`); views fetch REST on mount; transport actions are optimistic via callbacks in `App.tsx`.
- Suites that exercise API calls mock the api module with `vi.mock('../api', ...)` (libraryView with `importOriginal` to keep real `toPlayRef`; RadioView with `importOriginal` overriding only the radio methods with spies; ConfigView full-mock); suites that render components without api calls (TrackMenu, pwa, persistence) need no mock.
96
103
97
104
### MPD gotchas (memory — easy to get wrong; smoke MPD is 0.24.0)
105
+
98
106
-**`playid` does not exist** → always seek/play by 0-based position (`Mpd::play_position`; `play_song_id` resolves id→pos). Never reintroduce `playid`.
99
107
-**Shuffle = MPD `random` mode toggle** (`random 0|1`), not a one-shot queue reorder. `POST /api/playback/shuffle {"on":bool}`; `PlayerStatus.random` reflects state.
100
108
-**Remove from queue = `delete <pos>`** → `POST /api/playback/remove {"pos":u32}`.
- Track rows (LibraryView) are a CSS grid; the `@media (max-width: 640px)` block sets columns for ALL FOUR variants `.row / .rowActive / .rowPlaying / .rowPaused` — keep all four in the selector list. (`.rowActive` is currently dead CSS; SearchView has its own mobile block with 3 variants, no `.rowActive`.)
108
117
- The 3-dot `TrackMenu` (play-next / clear-play / add-to-playlist / file-info) appears on track rows and album headers.
109
118
- Bluetooth input (A2DP sink via bluealsa) is a TODO: Linux `input.rs` bails "not yet implemented (U4)" → 400.
0 commit comments