network addition

This commit is contained in:
Forrest 2026-08-04 16:39:18 -04:00
parent 3bd713d667
commit 2099d3d64d
27 changed files with 2237 additions and 40 deletions

View file

@ -6,7 +6,7 @@ These are ranked in order of importance and should be tackled one element at a t
- [x] 1. Drag n Drop Queue Reorder
- [x] 2. Filtering and Sorting Options
- [ ] 3. Network Connectivity
- [x] 3. Network Connectivity
- [x] 4. Simoultaneous Download Setting
- [x] 5. Playlist Sharing
- [ ] 6. In-App Purchase
@ -65,5 +65,33 @@ These are ranked in order of importance and should be tackled one element at a t
### 6. In-App "Buy the Dev a Coffee" ($5) — Medium, client + store config
- **Resolved:** route through IAP (review-safe). Flutter `in_app_purchase`, modeled as a **consumable** $5 product configured in App Store Connect + Google Play Console. Apple requires digital tips to go through IAP (no external payment link on iOS). Optional button in `lib/screens/settings_screen.dart`.
### 3. Network Connectivity / Cross-Device Control — Deferred deep-dive
Biggest lift. Client-only + Subsonic offers no shared-state or device-to-device channel, and the app has zero realtime code. Candidate directions to evaluate in a dedicated session: (a) Subsonic **jukebox mode** (server-side playback control, not another client — limited), (b) **LAN mDNS discovery + direct socket** (no server, same-Wi-Fi only), (c) relay service (would break client-only). No work until scoped separately.
### 3. Network Connectivity / Cross-Device Control — Large, LAN-only
**Scope (locked):** LAN-only · remote-control topology (one device plays audio, others act as remotes) · full remote (transport + live queue mirror + browse/enqueue/reorder). Both devices must be signed into the **same Subsonic server** — that constraint is what makes full remote cheap: the remote sends a track by `id` / `Song` JSON and the *host* resolves its own stream URI locally via the existing `streamUriFor`. No server changes; discovery + transport are pure LAN.
**Why not cross-network:** Subsonic/Navidrome exposes no realtime channel, pub/sub, or shared scratch space, so two instances can't coordinate over the internet without a relay (breaks client-only) or a hacky high-latency polling abuse of playlists. LAN-only is the robust client-only answer.
**Architecture**
- **Transport:** host runs an in-process WebSocket server (`dart:io` `HttpServer` + `WebSocketTransformer.upgrade` — no new dep); remote is a `WebSocket.connect` client.
- **Discovery:** mDNS/Bonjour via a new dep — `nsd` (register + browse; `bonsoir` is the alt). Service type `_timbre._tcp`; TXT record carries `serverKey`, protocol version, device name.
- **Auth (no manual pairing):** both devices already know the server password → derive an HMAC key from it; host sends a nonce, remote returns `HMAC-SHA256(nonce)`. Proves same-user without a PIN (`crypto` already a dep). Control gated to matching `serverKey` only.
- **Playback facade (load-bearing refactor):** `activePlaybackProvider` (state the UI renders) + `playbackCommandsProvider` (a `PlaybackCommands` interface the UI drives). `PlaybackController` (unchanged local engine) and a new `RemotePlaybackProxy` both satisfy the seam. Local mode delegates to the controller; remote mode renders the last WS snapshot and turns commands into outgoing messages (optimistic, reconciled on next snapshot).
- **New modules:** `lib/remote/messages.dart` (DTOs + codec + auth), `host_server.dart`, `discovery.dart`, `remote_session.dart`, `remote_playback.dart`, `lib/state/remote_providers.dart`, plus a "Devices" picker sheet + a "Playing on <device>" status chip.
**Build order**
1. **Protocol + codec + auth** — pure Dart, fully unit-testable (`Command` / `StateSnapshot` / `Hello` DTOs, versioning, HMAC challenge-response).
2. **Facade indirection** — add `activePlaybackProvider` + `playbackCommandsProvider`, migrate the ~11 UI files off `playbackProvider`. Ships as a no-op local-only refactor; de-risks everything downstream. **← land first.**
3. **Host server** — WS server broadcasts snapshots on `PlaybackController` change (reuse the throttle pattern), applies inbound commands.
4. **Discovery** — advertise when playable / browse for peers; platform config.
5. **Remote session + routing** — connect, mirror state, send commands, position interpolation, disconnect → fall back to local.
6. **UI** — device sheet, status chip, route browse/enqueue actions to the host.
7. **Auth + polish + tests** — loopback (host + client in one process) integration test.
**Risks / gotchas**
- **iOS 14+:** `NSLocalNetworkUsageDescription` + `NSBonjourServices=_timbre._tcp` in Info.plist or discovery silently fails. Android NSD needs a multicast lock (nsd handles).
- **iOS backgrounding:** host stays alive while *playing* (audio background mode via `just_audio_background`); a paused+backgrounded host may suspend its socket — remote reconnects on resume.
- **Position sync:** interpolate on the remote between periodic snapshots; reconcile on seek.
- **Offline host:** host resolves its own URIs; a track it can't stream reuses the existing `PlaybackError.offline` path.
- Multi-remote works free: host broadcasts to all clients.
**Estimate:** ~2–3× any other item on this list; step 2 (facade) is the reversible foundation to land first.