feat(flutter): #426B client — offline play capture via mutation queue
Flutter half of offline-replay capture. Play events no longer
fire-and-forget: the reporter now tracks each play as a completed
unit (track, original start time, source, duration reached)
independently of connectivity.
- EventsApi.playOffline: single timestamp-preserving call → the new
/api/events play_offline (47aa178).
- MutationQueue: new play.offline kind + handler (EventsApi).
- PlayEventsReporter rework:
- _beginTrack captures start context + fires live play_started;
the server id is adopted only if it lands while still on-track.
- position progress gated on the tracked track id so a track
change can't clobber the finishing track's last values.
- _closeCurrent: if a server id registered, attempt the live
ended/skipped and fall back to the offline queue on failure; if
no id (offline start) enqueue the completed play directly. The
server applies the canonical skip rule, so the offline payload
only carries duration.
- app paused/detached closes durably via the queue (survives a
process kill; a teardown POST would not).
Result: listening to cached tracks fully offline now records
history / recs / scrobble / #415 rotation once back online, with
the original timestamps. Web stays best-effort by standing
occasional-use scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
@@ -30,6 +30,7 @@ import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../api/endpoints/discover.dart';
|
||||
import '../api/endpoints/events.dart';
|
||||
import '../api/endpoints/likes.dart';
|
||||
import '../library/library_providers.dart' show dioProvider;
|
||||
import '../likes/likes_provider.dart' show likesApiProvider;
|
||||
@@ -52,6 +53,7 @@ class MutationKinds {
|
||||
static const playlistAppend = 'playlist.append';
|
||||
static const requestCreate = 'request.create';
|
||||
static const requestCancel = 'request.cancel';
|
||||
static const playOffline = 'play.offline';
|
||||
}
|
||||
|
||||
class MutationQueue {
|
||||
@@ -237,6 +239,8 @@ typedef _Handler = Future<void> Function(Ref, Map<String, dynamic>);
|
||||
/// * playlist.append: {'playlistId': uuid, 'trackIds': [uuid, …]}
|
||||
/// * request.create: {full createRequest args; see DiscoverApi}
|
||||
/// * request.cancel: {'id': uuid}
|
||||
/// * play.offline: {'trackId': uuid, 'clientId': str, 'at': iso8601,
|
||||
/// 'durationPlayedMs': int, 'source'?: 'for_you'|'discover'}
|
||||
final Map<String, _Handler> _handlers = {
|
||||
MutationKinds.likeAdd: (ref, p) async {
|
||||
final api = await ref.read(likesApiProvider.future);
|
||||
@@ -281,6 +285,17 @@ final Map<String, _Handler> _handlers = {
|
||||
final api = await ref.read(requestsApiProvider.future);
|
||||
await api.cancel(p['id'] as String);
|
||||
},
|
||||
MutationKinds.playOffline: (ref, p) async {
|
||||
final dio = await ref.read(dioProvider.future);
|
||||
final api = EventsApi(dio);
|
||||
await api.playOffline(
|
||||
trackId: p['trackId'] as String,
|
||||
clientId: p['clientId'] as String,
|
||||
atIso: p['at'] as String,
|
||||
durationPlayedMs: (p['durationPlayedMs'] as num).toInt(),
|
||||
source: p['source'] as String?,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
LikeKind _likeKindFromString(String s) => switch (s) {
|
||||
|
||||
Reference in New Issue
Block a user