# Project Spec: Smart Music Client (companion app) ## 1. Overview A mobile music player client built as the intentional companion to the Smart Music Server (see `smart-music-server-spec.md`). This is not a general-purpose Subsonic client — it is the client that knows how to consume the server's extended API and surface its novel features (contextual likes, session-aware radio, Lidarr integration, server-canonical state) as first-class UX. The thesis driving this project, restated: **state and intelligence belong on the server.** The client's job is to be a great playback surface and input device for the server's decisions, not to reimplement them. Compared to existing Subsonic clients (Symfonium, Tempo, Substreamer), this client is thinner in its "brains" and heavier in its integration with a specific server. **Scope of this spec:** v1 mobile client. Read the server spec first; this document assumes its terminology and data model. ## 2. Goals and non-goals ### Goals - Native-feeling mobile app on both iOS and Android from a single codebase. - Full playback: background audio, lock screen controls, notification controls, Bluetooth/headset button handling, CarPlay/Android Auto *awareness* (not necessarily full integration in v1 — see non-goals). - Offline cache with explicit user control. - First-class UX for the dual-like model: clear, fast distinction between "like generally" and "like in this context." - Real-time session awareness: the client knows what session it's in, shows the user what the server thinks the current context is, and surfaces contextually relevant actions. - Radio flow that feels like a single gesture (seed → queue → playing) with clear affordances for "keep this vibe," "shift vibe," and "stop." - Lidarr search and add directly from the player when listening to suggested additions. - Multi-device consistency: state (likes, play counts, current queue if desired) reflects the canonical server state. Two devices signed in to the same user should see the same world. ### Non-goals (v1) - CarPlay / Android Auto full integration. v1 should not *break* when these launch (lock screen controls must work, basic media session must be present) but dedicated CarPlay UI is deferred. - Desktop client. The server's web UI covers desktop for v1. - Cross-server support. This client talks to the Smart Music Server specifically and uses its extended API. Pointing it at a stock Navidrome is not a goal. - Chromecast / AirPlay. Nice-to-have, not v1. - Lyrics display. - Equalizer / DSP. - Playlist editing UI beyond basic add/remove (full playlist management lives in the server web UI). - Social features (sharing, following, comments). ## 3. Technology choices - **Framework:** Flutter. Rationale in the server spec conversation; the audio ecosystem (`just_audio`, `audio_service`) is the most mature cross-platform option for this use case. - **Language:** Dart. - **State management:** Riverpod. Rationale: testable, compile-time safe, well-suited to the amount of server state this app reflects. Avoid Bloc's boilerplate; avoid Provider's looseness. - **HTTP:** `dio` with a typed client layer generated from the server's OpenAPI spec if the server publishes one; otherwise hand-written. - **Local storage:** - `sqflite` or `drift` for structured local data (cached library, offline play event queue). - `path_provider` + direct filesystem for cached audio files. - `flutter_secure_storage` for credentials. - **Audio:** `just_audio` for playback, `just_audio_background` / `audio_service` for lock screen and background. - **Navigation:** `go_router`. - **Packaging:** standard Flutter build pipeline, published as APK/AAB for Android and TestFlight build for iOS. App store publication is out of scope for v1 (sideload / personal use). ## 4. Architecture ### Layers 1. **Transport layer** — wraps the server's Subsonic and extended APIs. Handles auth, retries, connectivity changes. Exposes typed methods to higher layers. 2. **Repository layer** — merges local cache and remote data. Owns the decision of "when do I serve cached data vs. fetch fresh." Exposes streams that UI subscribes to. 3. **Playback engine** — wraps `just_audio`. Owns the queue, position, playback state. Emits events consumed by the session reporter. 4. **Session reporter** — background worker that translates playback engine events into server event API calls (play start, skip, seek, complete). Batches and retries when offline. 5. **Offline cache manager** — owns the disk cache of audio files and cover art. Implements eviction policy, explicit user-pinned downloads. 6. **UI layer** — Flutter widgets consuming Riverpod providers that wrap repositories. ### Key architectural decisions - **Server is source of truth, local is cache.** The client does not hold authoritative state for likes, play counts, or queue membership. All user actions are fire-and-persist-locally, then sync to server. Conflict resolution favors server on write failures (with local retry). - **Offline mode is first-class.** Every user action — liking, skipping, playing — must work offline and sync later. This means local event queue with retry, and UI that doesn't lie about server state it couldn't confirm. - **No client-side recommendation logic.** Shuffle, radio, "play similar" all round-trip to the server. If offline, fall back to a simple random-from-cache with a UI indicator that smart features are degraded. - **Optimistic UI.** Liking a song updates the UI immediately; sync happens in the background. Failed syncs surface as a non-blocking indicator, not a modal. ### Threading model Flutter's main isolate handles UI. Playback and audio service run on their platform-native background threads (handled by `audio_service`). The session reporter and cache manager run as long-lived providers on the main isolate but do their work in async. Heavy operations (e.g. bulk cache cleanup) use `compute()` to offload to a worker isolate. ## 5. Data flow examples ### Starting a play session 1. User taps a track. 2. UI calls playback engine: load queue, start playback. 3. Playback engine emits "track started" event. 4. Session reporter calls `POST /api/v1/events/play` with start metadata. 5. Server creates or updates session, computes session vector, returns session info. 6. Client updates a "current session" provider; UI can display session context (e.g. "tagged: rock, energetic"). 7. As playback progresses, session reporter periodically sends progress; on completion or skip, sends terminal event. ### Liking a song Two buttons in the player UI: **Like** (general) and **Like this vibe** (contextual). Contextual like is only enabled when there is an active session of sufficient length (per server spec, ≥3 tracks in session). 1. User taps Like this vibe. 2. UI optimistically marks the track as contextually liked in this session; shows a brief toast confirming. 3. Client calls `POST /api/v1/likes/contextual`. The server computes the session vector from its own records (the client does not send it — the server is authoritative). 4. On success, the like is confirmed. On failure, the action is queued for retry; UI shows a subtle indicator. ### Starting radio 1. User long-presses a track or taps a "Radio from here" affordance. 2. UI calls `GET /api/v1/radio?seed_track=`. 3. Server returns a queue and optional "suggested additions" (tracks not in library). 4. Playback engine replaces queue; UI renders the queue with suggested additions grouped separately at the bottom with Lidarr-add buttons. 5. As the queue drains, client requests a refresh at 80% consumption. ### Offline playback 1. User goes offline. 2. Client continues playback from cached files. 3. Play/skip events accumulate in the local event queue with their timestamps. 4. On reconnect, event queue is flushed to server in order. Server is expected to backfill events with their original timestamps, reconstruct sessions correctly even retroactively. 5. If a user liked a track offline, the like is queued the same way. ## 6. UI structure ### Screens - **Sign-in** — server URL, username, password. Remember server. Option to save multiple servers (for dev/prod, single household use). - **Home** — suggested radios, recently played, recent likes, continue listening. All served by server endpoints. - **Library** — artists, albums, tracks, playlists browse. Subsonic-standard. - **Search** — searches server. Also offers "search Lidarr for this" affordance on empty results. - **Now playing** — full-screen player. Primary affordances: play/pause/skip/seek, like general, like contextual, radio-from-here, add-to-playlist, queue view, session inspector. - **Queue** — current queue with drag-to-reorder. Shows suggested additions below the real queue. - **Session inspector** — shows the server's current session vector in human-readable form (recent tracks, detected tags, detected artists). Useful for debugging and for user trust in the "why is this playing" question. Accessible from the now-playing screen. - **Downloads** — pinned/cached content, cache size, eviction controls. - **Settings** — account, offline behavior, audio quality, cache size, server info, log out. ### Now-playing affordances (design constraint) The now-playing screen is where the novel UX lives. It must make the following feel natural: - A single-tap way to like this song generally. - A single-tap way to like this song in this context. Visually distinct from general like. - Clear indication of whether either or both likes are active for the current track. - An obvious but not-dominant "radio from here" action. - When playback is inside a radio queue, a "keep this vibe" action that pins the current context, and a "shift" action that mutates it. (Exact semantics tunable; start with "keep" = re-seed radio from current track with current session vector, "shift" = re-seed ignoring session vector.) The exact visual design is deferred; the spec requires these *capabilities* be reachable without drilling into menus. Buried features won't get used and the contextual like is the whole point. ### Design constraint: don't lie about server state If an action hasn't succeeded on the server yet, the UI may show the optimistic result but must not present it as fully confirmed. A subtle pending indicator on the like button, for instance. Errors that affect user perception of their library (e.g. a like that never synced) must be surfaceable, not silently dropped. ## 7. Offline cache ### Two kinds of cached content 1. **Transient cache** — populated automatically during playback. LRU-evicted. Size-capped per user config (default 2 GB). 2. **Pinned downloads** — user-explicit "download this album / playlist." Not evicted automatically. Pinned content can be browsed in the Downloads screen. ### Cache decisions - Cached files are stored at the audio quality the server was asked to deliver. Quality is chosen per user config (e.g. "always original," "transcode to 192k on cellular"). - Cover art cached separately, smaller budget. - Cache survives app reinstall if possible (Android: scoped external storage; iOS: app container — accepting that iOS reinstall loses it). - User-visible cache size accounting must be accurate. ### Offline UI An offline indicator appears in the top chrome when the client can't reach the server. All server-dependent actions show degraded affordances (e.g. smart shuffle shows "shuffling cached tracks only"). Pinned downloads remain fully usable. ## 8. API usage ### Endpoints consumed From the server's Subsonic surface: `ping`, `getMusicFolders`, `getIndexes`/`getArtists`, `getArtist`, `getAlbum`, `getAlbumList2`, `search3`, `stream`, `getCoverArt`, `getPlaylists`, `getPlaylist`, `createPlaylist`, `updatePlaylist`, `getRandomSongs`, `star`/`unstar` (for general likes — though the extended API's equivalent is preferred for future-proofing). From the server's extended API: all of `/api/v1/*` described in the server spec. Specifically depended on: - `POST /api/v1/events/play` (with all event types the server supports) - `POST /api/v1/likes/contextual` - `DELETE /api/v1/likes/contextual/:id` - `GET /api/v1/session/current` - `GET /api/v1/radio` - `POST /api/v1/lidarr/search` - `POST /api/v1/lidarr/add` - `POST /api/v1/quarantine/:track_id` - `GET /api/v1/stats/user` ### Auth Standard Subsonic auth for Subsonic endpoints (token + salt scheme). Bearer token for extended API. Client holds both; transport layer adds them transparently. ### Versioning Client sends its version in a header on every extended API request. Server may respond with compatibility warnings. Major server API changes bump a version the client checks on startup. ## 9. Reliability ### Event queue durability Play/skip/like events are written to local SQL before any network attempt. The sync worker drains the queue FIFO with exponential backoff on failures. Events older than 30 days in the queue without successful sync are logged and dropped (with user visibility — a "some events failed to sync" notification). ### Playback robustness - Network interruption during streaming: if the track is partially buffered, finish it from buffer; on exhaustion, pause and show offline state. - Track not found (deleted between queue build and playback): skip forward, log, don't crash. - Corrupt cache file: purge it, redownload or skip. ### Battery and data - Don't prefetch aggressively on cellular unless user opts in. - Audio session must release properly when playback ends; no background drain. ## 10. Testing - Widget tests for the novel UI components (like buttons, session inspector, radio queue). - Unit tests for the event queue and sync logic — this is where silent bugs lose user trust. - Integration tests against a local server instance for the full happy-path flows. - Manual test matrix documented: playback across lock/unlock, app backgrounded, headphone disconnect, network loss mid-stream, receive call mid-stream. ## 11. Implementation order The client should not start before server step 4 (usable Subsonic baseline). Once that milestone is reached, client work proceeds in parallel: 1. Project skeleton, Riverpod setup, routing, theme. 2. Auth + sign-in screen. Talk to `ping`. Store credentials securely. 3. Library browse: artists, albums, tracks. Read-only against Subsonic API. 4. Playback engine: `just_audio` integration, queue, background audio, lock screen. **Milestone: can play music from the server.** 5. Event ingestion against server (requires server step 5 complete): play start, complete, skip events wired to server. 6. Like buttons (general) wired to Subsonic `star`. 7. Contextual like button + session inspector (requires server step 8). 8. Radio flow (requires server step 11). 9. Lidarr search/add integration (requires server step 12). 10. Offline cache manager. 11. Pinned downloads. 12. Quarantine action from player. 13. Settings polish, stats screen. 14. Packaging, release pipeline. Steps 5, 7, 8, 9 gate on server features. Plan accordingly; don't block client work on those, sequence around them. ## 12. Open questions to resolve during implementation - Whether queue state should itself be server-synced (so starting playback on one device resumes on another). Technically appealing but a significant complication — may defer to v2. - Whether to offer a "this song is playing because of these contextual likes" explanation affordance. Requires server API to expose the reasoning, and it isn't currently in the server spec. Worth considering adding. - Exact wording and iconography for general vs. contextual like. This needs user testing (even if the user is just you). - Whether offline-created contextual likes are a good idea at all — the server is the authority on session vectors, and the client's view of a session may drift offline. Possibly contextual likes require connectivity while general likes do not. - How aggressive to be about prefetching the radio queue in advance for offline continuation. ## 13. Out of scope reminders If during development any of the following come up, stop and reconsider scope: - CarPlay / Android Auto native UI - Chromecast / AirPlay - Lyrics - EQ / DSP - Cross-server support - Desktop port - Social / sharing