# Roadmap / TODO

Near-term work after the working MVP (connect + auto-connect, now-playing with
transport/volume/options, DataStore-persisted settings). Roughly ordered by
priority; not a commitment.

Status legend: `[ ]` todo · `[~]` in progress · `[x]` done

## [x] Queue / "Up Next" view (post-roadmap)

Done. `QueueScreen` (queue icon on the player) shows **"Up Next"** — the tracks
that will auto-play from the current one to the end of the queue, so the count
reflects "how many will play before MPD stops."

- Reads the full queue via `playlistinfo` (`MpdClient.queue`), then slices from
  `status.song` (current queue index) to the end. The already-played prefix is
  dropped, and the slice re-derives from the live current position, so the list
  shrinks as playback advances **even without consume mode** (MPD keeps played
  tracks in the queue unless `consume` is on; only the current-song pointer
  moves — that's why a plain queue view looked static).
- Current track is first, highlighted (bold + tinted + speaker icon). Tapping any
  track jumps to it (`playid`). Full queue re-fetched on queue-version change.
- Captions the cases where the count isn't literally "tracks until stop":
  repeat (loops), single (stops after current), repeat+single (repeats current).
- Rows show art thumbnails (disk-cached); art-less tracks show the disc
  placeholder. Overlay nav via `PlayerOverlay.Queue`.

---

## [x] 1. Proper icons

Done. Replaced the placeholder unicode glyphs in `NowPlayingScreen.kt` with real
Material icons (`Icons.Filled.SkipPrevious/PlayArrow/Pause/SkipNext/VolumeUp`,
plus `Repeat`/`Shuffle` leading icons on the chips) from
`androidx.compose.material:material-icons-extended`.

- That artifact bundles thousands of vectors, so the **debug** APK grew ~7 MB
  (expected — debug can't shrink). Enabled **R8 + resource shrinking** on the
  `release` build type, which strips the unused icons: release APK is ~1.2 MB.
- Considered `material-icons-core` (too small — no media icons) and bundling
  individual vector drawables (leaner but manual); the extended dep + R8 was the
  best effort/quality trade.

## [x] 2. Settings menu

Done. `SettingsScreen` (reached via a gear icon on the now-playing screen) shows
the current server (host/port) with **Change server** (disconnect → connect
form) and **Reset settings** (confirm dialog → clears DataStore + disconnects).

- `SettingsRepository.clear()` wipes DataStore; `MpdConnectionManager.resetSettings()`
  clears + disconnects.
- Navigation is lightweight state (`showSettings` in `EncoreApp`, only over
  the player screen; a `LaunchedEffect` drops it when leaving the player) — no
  Navigation-Compose dependency yet.
- **Playback section**: a **Consume mode** toggle (live-bound to `status.consume`,
  writes via the `consume` command). Consume is a dynamic MPD playback option, not
  static config — toggling it persists server-side (MPD state file). Verified the
  switch round-trips to the server (`consume: 0 → 1`).
- Room to grow: password field, connection timeout, theme, keep-screen-on, and
  the other playback toggles (single/repeat/random) could join the Playback
  section. If screens multiply, revisit adopting Navigation-Compose.

## [x] 3. Cast-style volume + OS media integration

Done — full MALP-style **OS integration**, not just in-app. Architecture:

- **`MpdConnectionManager`** (app-scoped, held by `EncoreApplication`) now
  owns the single `MpdClient`, so the connection survives Activity recreation and
  runs while the service is up. `PlayerViewModel` is a thin delegate over it.
- **`PlaybackService`** — a `mediaPlayback` foreground service hosting a
  `MediaSessionCompat`:
  - **Cast-style volume**: `setPlaybackToRemote(VolumeProviderCompat)` (absolute,
    0–100). The OS routes hardware volume keys to the *server* volume
    **system-wide, even when the app is backgrounded** (verified: 50→40 via
    injected keys from the launcher), and shows the remote-volume UI. Replaced
    the earlier in-app `onKeyDown` hack. Rapid presses accumulate via
    `pendingVolume`.
  - **Now-playing notification / QS / lock-screen** via a `MediaStyle`
    notification bound to the session, with prev/play-pause/next actions
    (`MediaButtonReceiver`) and metadata/position mirrored from the flows.
  - Needs `POST_NOTIFICATIONS` (requested in `MainActivity`) +
    `FOREGROUND_SERVICE[_MEDIA_PLAYBACK]`.
- In-app **`CastIndicator`** ("Controlling <host>") still shown above the slider.
- **Foreground key handling** (`VolumeKeyDispatcher`, delegated from
  `MainActivity.dispatchKeyEvent`): while focused, volume keys are consumed and
  drive the server silently — no system volume slider (the app's own slider is
  the feedback). Backgrounded, the `VolumeProvider` takes over (system UI is fine
  there). Gated by `MpdConnectionManager.isControllingVolume`; passes through to
  local volume when not connected.

Notes: verified on the FiiO via injected key events (a rotary volume knob may not
emit `VOLUME_UP/DOWN` — hardware-dependent). Album art in the notification/session
is pending item #5.

## [x] 4. Library browse — albums

Done. `AlbumsScreen` (library icon on the player) lists every album via
`list album group albumartist` (`MpdCommands.listAlbums` → `MpdAlbum.listFrom`),
sorted by artist then title. Tapping an album replaces the queue and plays it
(`MpdClient.playAlbum` = `clear` + `findadd album … albumartist …` + `play`),
then returns to the now-playing screen.

- Verified live: 149 albums listed and played correctly on the FiiO.
- Placeholder disc icon per row (real art is item #5).
- Interaction choice: tap = **replace queue + play** (the direct "play this
  album" gesture). Could later add a long-press / menu for "add to queue" and an
  album-detail/track view. Artist browse is a future extension.

## [x] 5. Album / artist images

Done — art on the now-playing screen, the album grid, and the media
notification / lock-screen / QS card.

- `MpdClient.songArt(uri)` / `albumArt(album, artist)` loop the chunked art
  protocol: try `albumart` (folder cover), fall back to `readpicture` (embedded);
  `albumArt` resolves a track via `find … window 0:1` first. `binarylimit` is
  raised on connect so covers transfer in ~1 round-trip.
- **Coil** (`io.coil-kt.coil3`) with a custom `Fetcher`/`Keyer` over the
  `MpdArtData` model (`SongArt`/`AlbumArt`), wired as the app's
  `SingletonImageLoader.Factory`. **Disk cache** means each cover is fetched from
  the server once ever — important for the Pi. `ArtImage` composable shows a disc
  placeholder underneath. Service pulls the bitmap via Coil (shared cache) into
  `METADATA_KEY_ALBUM_ART`.
- The now-playing screen is now vertically scrollable so art doesn't clip.
- **Art connection pool** (`MpdClient`, `ART_POOL_SIZE = 4`): covers are fetched
  on a dedicated pool of connections, not the command connection — so they load
  ~4× in parallel while scrolling *and* never block play/pause/volume. (Measured:
  a batch of 8 covers went from ~1.8s serialized on the tail to ~4-way parallel.)
  Each album cover is 3 round-trips (`find` → `albumart` ACK → `readpicture`), so
  the Pi is the remaining limit; disk cache means it's a one-time cost per cover.
- **Disk-cache persistence:** Coil's disk cache is only auto-managed by its
  *network* fetchers — a custom fetcher must read/write `imageLoader.diskCache`
  itself, or art is only memory-cached (re-fetched every cold start). `MpdArtFetcher`
  now serves from the disk snapshot when present and write-through-caches misses.
  Verified: after force-stop + relaunch, 19/19 covers loaded as DISK hits, 0
  server fetches. (Known minor gap: art-*less* albums aren't negative-cached, so
  they re-probe the server each cold start — future optimization if it matters.)

Gotchas hit & fixed:
- **Portability bug (not art-specific):** `MpdAckException`'s ACK regex had bare
  `]`/`}` — fine on the JVM (tests passed) but Android's ICU engine rejects them,
  so the first ACK parsed on-device threw `ExceptionInInitializerError`. Escaped
  them. This had masked all art (albumart's "no cover" ACK).
- Coil's reified `components { add(factory) }` didn't match the sealed-interface
  subtypes; had to register with the explicit `add(factory, MpdArtData::class)`.
- Coil pinned to **3.0.4**: 3.5+ requires `compileSdk 36` (we're on 35). Bumping
  later means updating `flake.nix` platform + AGP.

Future: online art fallback (MusicBrainz/Last.fm) for albums with no local art,
like MALP; artist images.

## [x] 6. BUG: idle connection drops after a few minutes → kicked to connect page

After a few minutes idling, the app surfaces **"connection closed mid-response"**
and falls back to the connect screen. MALP does not do this.

- Error origin: `MpdConnection.readResponse()` hits EOF and throws
  `MpdConnectionException("connection closed mid-response")`; the idle loop's
  `catch (IOException)` calls `failAndClose()` → `MpdConnectionState.Error` →
  UI shows the connect form.
- Likely causes to investigate:
  - **Android Doze / WiFi power-save** tearing down sockets when the screen is
    off or the app is backgrounded (most likely on a portable DAP/phone).
  - MPD's `connection_timeout` (default 60s) closing a connection it considers
    idle — a parked `idle` should count as active, but the *command* connection
    sits silent; a periodic `ping` keepalive may be needed.
  - NAT/router idle-connection reaping (less likely on LAN).
- Fix direction: don't treat an idle-connection drop as a fatal error — instead
  **auto-reconnect transparently** (re-open connections, re-issue `idle`, resync
  state) and keep showing the player. Consider a keepalive ping and, for
  backgrounded playback control, a foreground service / partial wakelock.
- PARTIALLY ADDRESSED by item #3: the `mediaPlayback` foreground service keeps
  the process/connection alive in the background.

**FIXED.** Root cause was two-fold: (a) the quiet command connection was reaped
by MPD's `connection_timeout`, and (b) the idle loop treated *any* `IOException`
(including that reap) as fatal → `Error` → connect page. Plus a Pi-Zero overload
angle: volume-key floods + 2-round-trip refreshes. Fixes, all in `MpdClient`
/`MpdConnectionManager`/`MpdConnection`:

- **Keepalive `ping`** every 25s on the command connection — prevents the
  `connection_timeout` reap and detects a dead socket early.
- **Transparent auto-reconnect** (`triggerReconnect`/`reconnectLoop`): a transport
  drop reopens both sockets in the background (state → `Connecting`, "Connecting…"
  splash, player stays), retrying with backoff; only after
  `MAX_RECONNECT_ATTEMPTS` does it surface `Error`. `MpdAckException` is
  explicitly *not* treated as a connection failure.
- **Volume throttle**: `setvol` writes are coalesced through a conflated channel
  (~1 per 120 ms, latest value) — kills the flood from holding volume keys while
  keeping accurate accumulation via `pendingVolume`.
- **Command-list refresh**: `status`+`currentsong` fetched in one round-trip
  (`MpdConnection.executeList`, `command_list_ok_begin`).

Verified on the FiiO: Wi-Fi drop → "Connecting…" → auto-recovers to the player
(no connect-page bounce); 6 rapid volume presses = exactly −30, throttled.
