116 lines
3.3 KiB
Dart
116 lines
3.3 KiB
Dart
|
|
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<void> onPlay() => _dispatch(_sm.onPlay(), _readProgress());
|
||
|
|
|
||
|
|
Future<void> onPause() => _dispatch(_sm.onPause(), _readProgress());
|
||
|
|
|
||
|
|
Future<void> onCompleted() => _dispatch(_sm.onCompleted(), 100);
|
||
|
|
|
||
|
|
|
||
|
|
Future<void> onStop() =>
|
||
|
|
_dispatch(_sm.onStop(progress: _readProgress()), _readProgress());
|
||
|
|
|
||
|
|
Future<void> _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<void> _enqueue(
|
||
|
|
TraktSyncJobType type,
|
||
|
|
Map<String, dynamic> 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|