refactor(watch): single watched-progress predicate
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
/// Single source of truth for the "progress crossed the watched threshold"
|
||||||
|
/// decision. All comparison sites (online progress events, external-player
|
||||||
|
/// returns, offline queueing) must route through this so edge-case handling
|
||||||
|
/// (zero/unknown duration, exact-threshold) can't drift between paths.
|
||||||
|
bool isWatchedProgress({required int positionMs, required int durationMs, required double threshold}) {
|
||||||
|
if (durationMs <= 0) return false;
|
||||||
|
return positionMs / durationMs >= threshold;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
import '../media/media_item.dart';
|
import '../media/media_item.dart';
|
||||||
import '../media/media_server_client.dart';
|
import '../media/media_server_client.dart';
|
||||||
|
import '../media/watch_progress.dart';
|
||||||
import '../models/external_player_models.dart';
|
import '../models/external_player_models.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../utils/snackbar_helper.dart';
|
import '../utils/snackbar_helper.dart';
|
||||||
@@ -207,7 +208,11 @@ class ExternalPlayerService {
|
|||||||
watchedThreshold: client.watchedThreshold,
|
watchedThreshold: client.watchedThreshold,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (position.inMilliseconds / duration.inMilliseconds >= client.watchedThreshold) {
|
if (isWatchedProgress(
|
||||||
|
positionMs: position.inMilliseconds,
|
||||||
|
durationMs: duration.inMilliseconds,
|
||||||
|
threshold: client.watchedThreshold,
|
||||||
|
)) {
|
||||||
try {
|
try {
|
||||||
// reportPlaybackStopped above marks the item played on backends that
|
// reportPlaybackStopped above marks the item played on backends that
|
||||||
// support it (Jellyfin); markWatchedFromPlaybackStop then only emits the
|
// support it (Jellyfin); markWatchedFromPlaybackStop then only emits the
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../media/media_item.dart';
|
|||||||
import '../media/media_kind.dart';
|
import '../media/media_kind.dart';
|
||||||
import '../media/media_server_client.dart';
|
import '../media/media_server_client.dart';
|
||||||
import '../media/playback_report_metadata.dart';
|
import '../media/playback_report_metadata.dart';
|
||||||
|
import '../media/watch_progress.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../utils/global_key_utils.dart';
|
import '../utils/global_key_utils.dart';
|
||||||
import 'offline_mode_source.dart';
|
import 'offline_mode_source.dart';
|
||||||
@@ -275,9 +276,8 @@ class OfflineWatchSyncService extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Check if an item should be considered watched based on progress percentage.
|
/// Check if an item should be considered watched based on progress percentage.
|
||||||
bool isWatchedByProgress(int viewOffset, int duration, {ServerId? serverId}) {
|
bool isWatchedByProgress(int viewOffset, int duration, {ServerId? serverId}) {
|
||||||
if (duration == 0) return false;
|
|
||||||
final threshold = serverId != null ? getWatchedThreshold(serverId) : 0.9;
|
final threshold = serverId != null ? getWatchedThreshold(serverId) : 0.9;
|
||||||
return (viewOffset / duration) >= threshold;
|
return isWatchedProgress(positionMs: viewOffset, durationMs: duration, threshold: threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the local watch status for a media item.
|
/// Get the local watch status for a media item.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import '../media/media_item.dart';
|
import '../media/media_item.dart';
|
||||||
import '../media/ids.dart';
|
import '../media/ids.dart';
|
||||||
|
import '../media/watch_progress.dart';
|
||||||
import 'app_logger.dart';
|
import 'app_logger.dart';
|
||||||
import 'base_notifier.dart';
|
import 'base_notifier.dart';
|
||||||
import 'global_key_utils.dart';
|
import 'global_key_utils.dart';
|
||||||
@@ -130,7 +131,7 @@ class WatchStateNotifier extends BaseNotifier<WatchStateEvent> {
|
|||||||
appLogger.w('WatchStateNotifier: missing serverId for ${item.id}, skipping progress event');
|
appLogger.w('WatchStateNotifier: missing serverId for ${item.id}, skipping progress event');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final isNowWatched = duration > 0 && (viewOffset / duration) >= watchedThreshold;
|
final isNowWatched = isWatchedProgress(positionMs: viewOffset, durationMs: duration, threshold: watchedThreshold);
|
||||||
|
|
||||||
notify(
|
notify(
|
||||||
WatchStateEvent(
|
WatchStateEvent(
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:plezy/media/watch_progress.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('isWatchedProgress', () {
|
||||||
|
test('false for zero or negative duration', () {
|
||||||
|
expect(isWatchedProgress(positionMs: 1000, durationMs: 0, threshold: 0.9), isFalse);
|
||||||
|
expect(isWatchedProgress(positionMs: 1000, durationMs: -1, threshold: 0.9), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exact threshold counts as watched', () {
|
||||||
|
expect(isWatchedProgress(positionMs: 90, durationMs: 100, threshold: 0.9), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('just below threshold is not watched', () {
|
||||||
|
expect(isWatchedProgress(positionMs: 89, durationMs: 100, threshold: 0.9), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('position past duration is watched', () {
|
||||||
|
expect(isWatchedProgress(positionMs: 110, durationMs: 100, threshold: 0.9), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zero position is not watched', () {
|
||||||
|
expect(isWatchedProgress(positionMs: 0, durationMs: 100, threshold: 0.9), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user