import 'package:dio/dio.dart'; /// Thin client for POST /api/events — the play-event lifecycle the /// server uses for history, recommendation scoring, ListenBrainz /// scrobbles, and (since #415) system-playlist rotation. /// /// Mirrors the web events dispatcher's three calls. Best-effort by /// contract: callers swallow errors — a missed event is acceptable /// per the server spec's v1 stance, and the server's /// auto-close-prior-open keeps history sane even if an ended/skipped /// is lost. class EventsApi { EventsApi(this._dio); final Dio _dio; /// POST play_started. Returns the server's play_event_id (used to /// close the row later), or null if the call failed / response was /// malformed. `source` tags the originating system playlist /// ('for_you' | 'discover') so the server advances that rotation; /// omit for library / user-playlist / radio plays. Future playStarted({ required String trackId, required String clientId, String? source, }) async { final r = await _dio.post>( '/api/events', data: { 'type': 'play_started', 'track_id': trackId, 'client_id': clientId, if (source != null && source.isNotEmpty) 'source': source, }, ); return (r.data ?? const {})['play_event_id'] as String?; } Future playEnded({ required String playEventId, required int durationPlayedMs, }) async { await _dio.post( '/api/events', data: { 'type': 'play_ended', 'play_event_id': playEventId, 'duration_played_ms': durationPlayedMs, }, ); } Future playSkipped({ required String playEventId, required int positionMs, }) async { await _dio.post( '/api/events', data: { 'type': 'play_skipped', 'play_event_id': playEventId, 'position_ms': positionMs, }, ); } /// Replays a complete play that happened offline / on a flaky /// connection (#426 part B). One call: the server records start+end /// from `atIso` (the original play-start time) + durationPlayedMs, /// applies the canonical skip rule, and advances #415 rotation when /// source is a system playlist. Driven by the offline mutation /// queue, never the live path. Future playOffline({ required String trackId, required String clientId, required String atIso, required int durationPlayedMs, String? source, }) async { await _dio.post( '/api/events', data: { 'type': 'play_offline', 'track_id': trackId, 'client_id': clientId, 'at': atIso, 'duration_played_ms': durationPlayedMs, if (source != null && source.isNotEmpty) 'source': source, }, ); } }