diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart index 7416927..81c6265 100644 --- a/lib/features/playback_reporting/core/playback_reporter.dart +++ b/lib/features/playback_reporting/core/playback_reporter.dart @@ -39,7 +39,15 @@ class PlaybackReporter { /// the minimum duration to report final Duration reportingDurationThreshold; + /// the duration to wait before starting the reporting + /// this is to ignore the initial duration in case user is browsing + final Duration? minimumPositionForReporting; + + /// the duration to mark the book as complete when the time left is less than this + final Duration markCompleteWhenTimeLeft; + /// timer to report every 10 seconds + /// tracking the time since the last report Timer? _reportTimer; /// metadata to report @@ -61,6 +69,8 @@ class PlaybackReporter { this.deviceManufacturer, this.reportingDurationThreshold = const Duration(seconds: 1), Duration reportingInterval = const Duration(seconds: 10), + this.minimumPositionForReporting, + this.markCompleteWhenTimeLeft = const Duration(seconds: 5), }) : _reportingInterval = reportingInterval { // initial conditions if (player.playing) { @@ -97,23 +107,35 @@ class PlaybackReporter { _logger.fine( 'player state observed, stopping stopwatch at ${_stopwatch.elapsed}', ); - await syncCurrentPosition(); + await tryReportPlayback(null); } }), ); _logger.fine( - 'initialized with interval: $reportingInterval, threshold: $reportingDurationThreshold', + 'initialized with reportingInterval: $reportingInterval, reportingDurationThreshold: $reportingDurationThreshold', + ); + _logger.fine( + 'initialized with minimumPositionForReporting: $minimumPositionForReporting, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft', ); _logger.fine( 'initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer', ); } - void tryReportPlayback(_) async { + Future tryReportPlayback(_) async { _logger.fine( 'callback called when elapsed ${_stopwatch.elapsed}', ); + if (player.book != null && + player.positionInBook >= + player.book!.duration - markCompleteWhenTimeLeft) { + _logger.info( + 'marking complete as time left is less than $markCompleteWhenTimeLeft', + ); + await markComplete(); + return; + } if (_stopwatch.elapsed > reportingDurationThreshold) { _logger.fine( 'reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold', @@ -159,6 +181,22 @@ class PlaybackReporter { return _session; } + Future markComplete() async { + if (player.book == null) { + throw NoAudiobookPlayingError(); + } + await authenticatedApi.me.createUpdateMediaProgress( + libraryItemId: player.book!.libraryItemId, + parameters: CreateUpdateProgressReqParams( + isFinished: true, + currentTime: player.positionInBook, + duration: player.book!.duration, + ), + responseErrorHandler: _responseErrorHandler, + ); + _logger.info('Marked complete for book: ${player.book!.libraryItemId}'); + } + Future syncCurrentPosition() async { final data = _getSyncData(); if (data == null) { @@ -229,6 +267,23 @@ class PlaybackReporter { ); return null; } + + // if in the ignore duration, don't sync + if (minimumPositionForReporting != null && + player.positionInBook < minimumPositionForReporting!) { + // but if elapsed time is more than the minimumPositionForReporting, sync + if (_stopwatch.elapsed > minimumPositionForReporting!) { + _logger.info( + 'Syncing position despite being less than minimumPositionForReporting as elapsed time is more: ${_stopwatch.elapsed}', + ); + } else { + _logger.info( + 'Ignoring sync for position: ${player.positionInBook} < $minimumPositionForReporting', + ); + return null; + } + } + return SyncSessionReqParams( currentTime: player.positionInBook, timeListened: _stopwatch.elapsed, diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.dart index e76f7ff..a7ca1d5 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.dart @@ -13,7 +13,7 @@ part 'playback_reporter_provider.g.dart'; class PlaybackReporter extends _$PlaybackReporter { @override Future build() async { - final appSettings = ref.watch(appSettingsProvider); + final playerSettings = ref.watch(appSettingsProvider).playerSettings; final player = ref.watch(simpleAudiobookPlayerProvider); final packageInfo = await PackageInfo.fromPlatform(); final api = ref.watch(authenticatedApiProvider); @@ -26,7 +26,9 @@ class PlaybackReporter extends _$PlaybackReporter { final reporter = core.PlaybackReporter( player, api, - reportingInterval: appSettings.playerSettings.playbackReportInterval, + reportingInterval: playerSettings.playbackReportInterval, + markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, + minimumPositionForReporting: playerSettings.minimumPositionForReporting, deviceName: deviceName, deviceModel: deviceModel, deviceSdkVersion: deviceSdkVersion, diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart index 2a76328..8b8936d 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart @@ -6,7 +6,7 @@ part of 'playback_reporter_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$playbackReporterHash() => r'c210b7286d9c151fd59a9ead9eb4a28d1cffdc7c'; +String _$playbackReporterHash() => r'f5436d652e51c37bcc684acdaec94e17a97e68e5'; /// See also [PlaybackReporter]. @ProviderFor(PlaybackReporter) diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index cc4506b..8ccb320 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -45,7 +45,9 @@ class PlayerSettings with _$PlayerSettings { @Default(1) double preferredDefaultSpeed, @Default([0.75, 1, 1.25, 1.5, 1.75, 2]) List speedOptions, @Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings, + @Default(Duration(seconds: 10)) Duration minimumPositionForReporting, @Default(Duration(seconds: 10)) Duration playbackReportInterval, + @Default(Duration(seconds: 15)) Duration markCompleteWhenTimeLeft, @Default(true) bool configurePlayerForEveryBook, }) = _PlayerSettings; diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 204d314..b07b684 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -514,7 +514,10 @@ mixin _$PlayerSettings { List get speedOptions => throw _privateConstructorUsedError; SleepTimerSettings get sleepTimerSettings => throw _privateConstructorUsedError; + Duration get minimumPositionForReporting => + throw _privateConstructorUsedError; Duration get playbackReportInterval => throw _privateConstructorUsedError; + Duration get markCompleteWhenTimeLeft => throw _privateConstructorUsedError; bool get configurePlayerForEveryBook => throw _privateConstructorUsedError; /// Serializes this PlayerSettings to a JSON map. @@ -540,7 +543,9 @@ abstract class $PlayerSettingsCopyWith<$Res> { double preferredDefaultSpeed, List speedOptions, SleepTimerSettings sleepTimerSettings, + Duration minimumPositionForReporting, Duration playbackReportInterval, + Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook}); $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; @@ -569,7 +574,9 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings> Object? preferredDefaultSpeed = null, Object? speedOptions = null, Object? sleepTimerSettings = null, + Object? minimumPositionForReporting = null, Object? playbackReportInterval = null, + Object? markCompleteWhenTimeLeft = null, Object? configurePlayerForEveryBook = null, }) { return _then(_value.copyWith( @@ -597,10 +604,18 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings> ? _value.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable as SleepTimerSettings, + minimumPositionForReporting: null == minimumPositionForReporting + ? _value.minimumPositionForReporting + : minimumPositionForReporting // ignore: cast_nullable_to_non_nullable + as Duration, playbackReportInterval: null == playbackReportInterval ? _value.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable as Duration, + markCompleteWhenTimeLeft: null == markCompleteWhenTimeLeft + ? _value.markCompleteWhenTimeLeft + : markCompleteWhenTimeLeft // ignore: cast_nullable_to_non_nullable + as Duration, configurePlayerForEveryBook: null == configurePlayerForEveryBook ? _value.configurePlayerForEveryBook : configurePlayerForEveryBook // ignore: cast_nullable_to_non_nullable @@ -657,7 +672,9 @@ abstract class _$$PlayerSettingsImplCopyWith<$Res> double preferredDefaultSpeed, List speedOptions, SleepTimerSettings sleepTimerSettings, + Duration minimumPositionForReporting, Duration playbackReportInterval, + Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook}); @override @@ -687,7 +704,9 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res> Object? preferredDefaultSpeed = null, Object? speedOptions = null, Object? sleepTimerSettings = null, + Object? minimumPositionForReporting = null, Object? playbackReportInterval = null, + Object? markCompleteWhenTimeLeft = null, Object? configurePlayerForEveryBook = null, }) { return _then(_$PlayerSettingsImpl( @@ -715,10 +734,18 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res> ? _value.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable as SleepTimerSettings, + minimumPositionForReporting: null == minimumPositionForReporting + ? _value.minimumPositionForReporting + : minimumPositionForReporting // ignore: cast_nullable_to_non_nullable + as Duration, playbackReportInterval: null == playbackReportInterval ? _value.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable as Duration, + markCompleteWhenTimeLeft: null == markCompleteWhenTimeLeft + ? _value.markCompleteWhenTimeLeft + : markCompleteWhenTimeLeft // ignore: cast_nullable_to_non_nullable + as Duration, configurePlayerForEveryBook: null == configurePlayerForEveryBook ? _value.configurePlayerForEveryBook : configurePlayerForEveryBook // ignore: cast_nullable_to_non_nullable @@ -737,7 +764,9 @@ class _$PlayerSettingsImpl implements _PlayerSettings { this.preferredDefaultSpeed = 1, final List speedOptions = const [0.75, 1, 1.25, 1.5, 1.75, 2], this.sleepTimerSettings = const SleepTimerSettings(), + this.minimumPositionForReporting = const Duration(seconds: 10), this.playbackReportInterval = const Duration(seconds: 10), + this.markCompleteWhenTimeLeft = const Duration(seconds: 15), this.configurePlayerForEveryBook = true}) : _speedOptions = speedOptions; @@ -770,14 +799,20 @@ class _$PlayerSettingsImpl implements _PlayerSettings { final SleepTimerSettings sleepTimerSettings; @override @JsonKey() + final Duration minimumPositionForReporting; + @override + @JsonKey() final Duration playbackReportInterval; @override @JsonKey() + final Duration markCompleteWhenTimeLeft; + @override + @JsonKey() final bool configurePlayerForEveryBook; @override String toString() { - return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval, configurePlayerForEveryBook: $configurePlayerForEveryBook)'; + return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, minimumPositionForReporting: $minimumPositionForReporting, playbackReportInterval: $playbackReportInterval, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft, configurePlayerForEveryBook: $configurePlayerForEveryBook)'; } @override @@ -797,8 +832,15 @@ class _$PlayerSettingsImpl implements _PlayerSettings { .equals(other._speedOptions, _speedOptions) && (identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings) && + (identical(other.minimumPositionForReporting, + minimumPositionForReporting) || + other.minimumPositionForReporting == + minimumPositionForReporting) && (identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval) && + (identical( + other.markCompleteWhenTimeLeft, markCompleteWhenTimeLeft) || + other.markCompleteWhenTimeLeft == markCompleteWhenTimeLeft) && (identical(other.configurePlayerForEveryBook, configurePlayerForEveryBook) || other.configurePlayerForEveryBook == @@ -815,7 +857,9 @@ class _$PlayerSettingsImpl implements _PlayerSettings { preferredDefaultSpeed, const DeepCollectionEquality().hash(_speedOptions), sleepTimerSettings, + minimumPositionForReporting, playbackReportInterval, + markCompleteWhenTimeLeft, configurePlayerForEveryBook); /// Create a copy of PlayerSettings @@ -843,7 +887,9 @@ abstract class _PlayerSettings implements PlayerSettings { final double preferredDefaultSpeed, final List speedOptions, final SleepTimerSettings sleepTimerSettings, + final Duration minimumPositionForReporting, final Duration playbackReportInterval, + final Duration markCompleteWhenTimeLeft, final bool configurePlayerForEveryBook}) = _$PlayerSettingsImpl; factory _PlayerSettings.fromJson(Map json) = @@ -862,8 +908,12 @@ abstract class _PlayerSettings implements PlayerSettings { @override SleepTimerSettings get sleepTimerSettings; @override + Duration get minimumPositionForReporting; + @override Duration get playbackReportInterval; @override + Duration get markCompleteWhenTimeLeft; + @override bool get configurePlayerForEveryBook; /// Create a copy of PlayerSettings diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 58aaad4..c1554c7 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -73,10 +73,19 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => ? const SleepTimerSettings() : SleepTimerSettings.fromJson( json['sleepTimerSettings'] as Map), + minimumPositionForReporting: json['minimumPositionForReporting'] == null + ? const Duration(seconds: 10) + : Duration( + microseconds: + (json['minimumPositionForReporting'] as num).toInt()), playbackReportInterval: json['playbackReportInterval'] == null ? const Duration(seconds: 10) : Duration( microseconds: (json['playbackReportInterval'] as num).toInt()), + markCompleteWhenTimeLeft: json['markCompleteWhenTimeLeft'] == null + ? const Duration(seconds: 15) + : Duration( + microseconds: (json['markCompleteWhenTimeLeft'] as num).toInt()), configurePlayerForEveryBook: json['configurePlayerForEveryBook'] as bool? ?? true, ); @@ -90,7 +99,11 @@ Map _$$PlayerSettingsImplToJson( 'preferredDefaultSpeed': instance.preferredDefaultSpeed, 'speedOptions': instance.speedOptions, 'sleepTimerSettings': instance.sleepTimerSettings, + 'minimumPositionForReporting': + instance.minimumPositionForReporting.inMicroseconds, 'playbackReportInterval': instance.playbackReportInterval.inMicroseconds, + 'markCompleteWhenTimeLeft': + instance.markCompleteWhenTimeLeft.inMicroseconds, 'configurePlayerForEveryBook': instance.configurePlayerForEveryBook, };