When ServerHealthController reports Offline or ServerDown, SearchRepository
runs Room LIKE queries against cached_artists / cached_albums / cached_tracks
instead of hitting /api/search. The screen draws a one-line hint above the
results so the user can tell server matches from on-device-only matches.
Adds searchByName / searchByTitle DAO methods; LOCAL_SEARCH_LIMIT=20 matches
the server's default page size.
Phase 3 of #618. Wraps the OkHttpDataSource upstream of CacheDataSource with OfflineGatedDataSource. CacheDataSource only consults the upstream factory on a cache miss, so playback of cached audio is unaffected. Offline tap on a non-cached track now throws OfflineException immediately (subclass of IOException for ExoPlayer's PlaybackException to wrap) instead of waiting on a multi-second OkHttp timeout. AudioPrefetcher keeps its own ungated upstream -- writes fail silently when offline, no user-visible impact.
Closes Scribe #611. The 2026-06-04 logcat showed 33 consecutive AddURIToQueue failures clustered at ~10ms intervals once the burst hit offset 39 -- characteristic of Sonos's burst-add rate-limit. 50ms between successful adds adds ~5s to the 100-track background extension but eliminates the burst rejection. Next reproduction with the SOAP fault detail logging (audit commit c5b326c6) will confirm the fault code if any tracks still fail.
Closes Scribe #606. Two pieces: MediaMetadata gets durationMs (lock-screen scrubber gets a known total even when wrapped ExoPlayer is paused under UPnP); MinstrelForwardingPlayer keeps an externalListeners registry that mirrors super.addListener so we can directly invoke onIsPlayingChanged / onPlaybackStateChanged / onMediaItemTransition when remoteState mutates. Fires from pollOnce + play()/pause() onSuccess. dedup via lastNotified guards so we don't spam events at 1Hz when nothing changed.
Part A: split loadQueueOnSonos into an initial phase (tracks[0..currentIndex]
only, then SetAV+Seek+Play) plus a background extendQueueOnSonos coroutine
that appends the remainder after activation. Reduces the UPnP activation
block from ~17s (100 tracks serial) to ~200ms (1 track at currentIndex=0).
Background extension cancels cleanly when activeUpnpHolder.active changes.
Part B: add PlayerUiState.isUpnpLoading (target set, active null). Projected
inline in onEvents so it stays consistent with the rest of the snapshot, plus
a separate combine(target, active) collector that updates uiState between
player-event fires. NowPlayingScreen.TransportRow and MiniPlayer.MiniRow
replace the play/pause icon with a CircularProgressIndicator while loading
and disable the button tap to prevent premature commands to the Sonos queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Polling alone cannot distinguish Sonos auto-advancing via SetNextAVTransportURI
from URI changes we made ourselves via syncCurrentItemToRemote. This produced
two races: (1) activation race -- first poll returns stale URI from prior
session, second returns new URI, false-positive fires and double-advances the
cursor; (2) user-skip race -- skip's syncCurrentItemToRemote changes the URI,
next poll sees the change and fires again. Remove the detection block and
previousTrackUri capture from pollOnce entirely. pollLoop is now a pure
state-tracker (position + transport state) plus the one-shot initial pre-queue
gate. GENA event subscriptions to AVTransport LastChange are the correct fix;
deferred to its own slice (see parity-map).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional onRawResponse callback to SoapClient; loggingSoapClient
factory emits the first 6 GetPositionInfo/GetTransportInfo bodies
(3 poll cycles) at WARN so release logs capture them. Wire into
transportFor so every AVTransportClient for a new UPnP session logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move local ExoPlayer pause from OutputPickerController.selectUpnp
into MinstrelForwardingPlayer.onActiveChanged (handler.post { delegate.pause() }).
This guarantees the pause hits ExoPlayer before the holder is live, eliminating
the async race that caused SOAP fault 701 on Sonos when pause() was dispatched
via playerController after holder.active was already set.
Also adds per-poll Timber.w before initialPreQueueDone for diagnostics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OutputPickerController.selectUpnp: pause ExoPlayer BEFORE setting
activeUpnpHolder so ForwardingPlayer.pause() routes to ExoPlayer,
not SOAP; remove now-redundant playerController.pause() from inside
runCatching; bump activation Timber.i -> Timber.w for release logcat
- MinstrelForwardingPlayer: remove Player.Listener onMediaItemTransition
that raced with seekToNext/Prev override's syncCurrentItemToRemote;
seekToNext/Prev now launch sync -> preQueueNext sequentially in one
coroutine; remove early preQueueNext from onActiveChanged (raced with
selectUpnp SOAP); move initial pre-queue to pollLoop, fires once
trackUri lands confirming Sonos accepted SetAV+Play
- Extract pollOnce from pollLoop to stay within detekt LongMethod=60;
natural-advance branch now calls preQueueNext explicitly (no listener)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>