Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -0,0 +1,115 @@
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;
}
}
}