feat(flutter): #415 stage 3 — play-events reporter + rotation wiring
The Flutter client previously reported NO plays — mobile listening never reached play_events, so history, recommendation scoring, ListenBrainz scrobbles, and #415 rotation all missed mobile entirely. Operator chose to close that gap properly as part of Stage 3. New: - EventsApi (api/endpoints/events.dart): play_started/ended/skipped. - PlayEventsReporter (player/play_events_reporter.dart): state machine over (track id, playing) mirroring the web dispatcher. Persists an opaque client_id in secure storage. Deliberate divergence from web: a track change inside a queue is classified ended-vs-skipped by whether the prior track reached ~its duration (3s tolerance), instead of web's blanket "track change = skip" which would mark every naturally-finished in-queue track a skip and dilute recommendation skip-ratios — the exact failure mode that motivated doing this properly. Fail-safe: no-ops when there's no audio handler (tests / no-audio env). App-lifecycle paused/ detached closes an open row as a best-effort skip (web pagehide parity). Wired in app.dart postFrame. - PlaylistsApi.systemShuffle(variant): GET the rotation-aware order. Wiring: - audio_handler: _queueSource carried through setQueueFromTracks (source param); preserved across internal skipToQueueItem rebuild. - player_provider.playTracks: source param → setQueueFromTracks. - PlaylistCard: system playlists fetch systemShuffle and play as-is tagged with source (no client shuffle — server already ordered). - playlist_detail_screen: header Play + per-track tap tag source for system playlists so rotation advances from any entry point. Known/flagged separately: the web dispatcher likely has the same false-skip-on-advance issue; not fixed here to keep #415 scoped and clients' wire behavior comparable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,15 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
/// past the fill front needs to rebuild from the stored tracks.
|
||||
List<TrackRef> _lastTracks = const [];
|
||||
|
||||
/// #415: which system playlist this queue was seeded from
|
||||
/// ('for_you' | 'discover'), or null for library / user-playlist /
|
||||
/// radio. The play-events reporter reads this so play_started
|
||||
/// carries `source` and the server advances that rotation. A fresh
|
||||
/// setQueueFromTracks from a non-system surface clears it; internal
|
||||
/// rebuilds (skipToQueueItem) preserve it.
|
||||
String? _queueSource;
|
||||
String? get queueSource => _queueSource;
|
||||
|
||||
/// Volume stream for UI subscribers. Mirrors the just_audio player's
|
||||
/// volume directly; set via setVolume(double).
|
||||
Stream<double> get volumeStream => _player.volumeStream;
|
||||
@@ -109,8 +118,13 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
if (audioCacheManager != null) _audioCacheManager = audioCacheManager;
|
||||
}
|
||||
|
||||
Future<void> setQueueFromTracks(List<TrackRef> tracks, {int initialIndex = 0}) async {
|
||||
Future<void> setQueueFromTracks(
|
||||
List<TrackRef> tracks, {
|
||||
int initialIndex = 0,
|
||||
String? source,
|
||||
}) async {
|
||||
if (tracks.isEmpty) return;
|
||||
_queueSource = source;
|
||||
final clampedInitial = initialIndex.clamp(0, tracks.length - 1);
|
||||
|
||||
// Bump the generation FIRST. Any in-flight _fillRemainingSources
|
||||
@@ -190,7 +204,9 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
@override
|
||||
Future<void> skipToQueueItem(int index) async {
|
||||
if (index < 0 || index >= _lastTracks.length) return;
|
||||
await setQueueFromTracks(_lastTracks, initialIndex: index);
|
||||
// Preserve the system-playlist source across an internal rebuild
|
||||
// — a queue-item skip is still playing from the same playlist.
|
||||
await setQueueFromTracks(_lastTracks, initialIndex: index, source: _queueSource);
|
||||
await play();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user