import 'dart:async'; import '../../../core/contracts/trakt/trakt_response.dart'; import '../../../core/contracts/trakt/trakt_scrobble_target.dart'; import '../../../core/contracts/trakt/trakt_sync_job.dart'; import '../../../core/domain/ports/authed_trakt_gateway.dart'; import '../../../core/infra/trakt_api/trakt_pending_sync_queue.dart'; import '../../../shared/utils/app_logger.dart'; import 'trakt_scrobble_state_machine.dart'; const _tag = 'TraktScrobble'; class TraktScrobbleReporter { TraktScrobbleReporter({ required this._gateway, required this._queue, required this._target, required this._readProgress, this._appVersion, this._appDate, this._onSuccess, this._onError, }); final AuthedTraktGateway _gateway; final TraktPendingSyncQueue _queue; final TraktScrobbleTarget? _target; final double Function() _readProgress; final String? _appVersion; final String? _appDate; final void Function()? _onSuccess; final void Function(String error)? _onError; final _sm = TraktScrobbleStateMachine(); bool get _enabled => _target != null; Future onPlay() => _dispatch(_sm.onPlay(), _readProgress()); Future onPause() => _dispatch(_sm.onPause(), _readProgress()); Future onCompleted() => _dispatch(_sm.onCompleted(), 100); Future onStop() => _dispatch(_sm.onStop(progress: _readProgress()), _readProgress()); Future _dispatch(TraktScrobbleAction action, double progress) async { if (!_enabled) return; final type = _typeFor(action); if (type == null) return; final clamped = progress.clamp(0.0, 100.0).toDouble(); final body = _target!.toBody( progress: clamped, appVersion: _appVersion, appDate: _appDate, ); AppLogger.debug( _tag, 'scrobble ${type.wire} progress=${clamped.toStringAsFixed(1)} ' '(${_target.kind.name})', ); TraktScrobbleOutcome outcome; try { outcome = await _gateway.scrobble(type, body); } catch (e) { AppLogger.debug(_tag, 'scrobble threw, enqueue', e); await _enqueue(type, body); return; } switch (outcome.cls) { case TraktResponseClass.success: case TraktResponseClass.duplicate: AppLogger.debug(_tag, 'scrobble ${type.wire} ok (${outcome.cls.name})'); await _queue.discardSupersededBy(type: type, body: body); _onSuccess?.call(); case TraktResponseClass.invalid: AppLogger.debug(_tag, 'scrobble ${type.wire} invalid → drop'); case TraktResponseClass.rateLimited: case TraktResponseClass.unauthorized: case TraktResponseClass.transient: await _enqueue(type, body); _onError?.call(outcome.cls.name); } } Future _enqueue( TraktSyncJobType type, Map body, ) async { try { await _queue.enqueue(type: type, body: body); } catch (e) { AppLogger.warn(_tag, 'enqueue failed', e); } } static TraktSyncJobType? _typeFor(TraktScrobbleAction action) { switch (action) { case TraktScrobbleAction.none: return null; case TraktScrobbleAction.start: return TraktSyncJobType.scrobbleStart; case TraktScrobbleAction.pause: return TraktSyncJobType.scrobblePause; case TraktScrobbleAction.stop: return TraktSyncJobType.scrobbleStop; } } }