fix(playback): harden offline source reporting

This commit is contained in:
edde746
2026-05-29 19:55:01 +02:00
parent 7501f461b9
commit d93ea9813f
34 changed files with 739 additions and 273 deletions
+2 -3
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/playback_report_metadata.dart';
import 'package:plezy/services/jellyfin_client.dart';
import 'package:plezy/services/live_session_tracker.dart';
@@ -45,9 +46,7 @@ class _FakeJellyfinClient implements JellyfinClient {
Duration? duration,
String? playSessionId,
String? mediaSourceId,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) async {
calls.add('stopped:$itemId:$playSessionId');
}
@@ -10,6 +10,7 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/media/playback_report_metadata.dart';
import 'package:plezy/services/jellyfin_api_cache.dart';
import 'package:plezy/services/jellyfin_client.dart';
import 'package:plezy/services/multi_server_manager.dart';
@@ -79,8 +80,7 @@ class _RecordingMediaClient implements MediaServerClient {
void close() {}
final started = <({String itemId, int positionMs, int? durationMs})>[];
final stopped =
<({String itemId, int positionMs, int? durationMs, bool offline, DateTime? updatedAt, bool? continuing})>[];
final stopped = <({String itemId, int positionMs, int? durationMs, PlaybackReportMetadata report})>[];
final watched = <String>[];
@override
@@ -108,17 +108,13 @@ class _RecordingMediaClient implements MediaServerClient {
Duration? duration,
String? playSessionId,
String? mediaSourceId,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) async {
stopped.add((
itemId: itemId,
positionMs: position.inMilliseconds,
durationMs: duration?.inMilliseconds,
offline: offline,
updatedAt: updatedAt,
continuing: continuing,
report: report,
));
}
@@ -355,8 +351,8 @@ void main() {
expect(client.started.single.positionMs, 50000);
expect(client.stopped, hasLength(1));
expect(client.stopped.single.positionMs, 50000);
expect(client.stopped.single.offline, isTrue);
expect(client.stopped.single.updatedAt?.millisecondsSinceEpoch, queued!.updatedAt);
expect(client.stopped.single.report.isOfflineReplay, isTrue);
expect(client.stopped.single.report.recordedAt?.millisecondsSinceEpoch, queued!.updatedAt);
expect(client.watched, isEmpty);
expect(await svc.getPendingSyncCount(), 0);
});
@@ -380,9 +376,9 @@ void main() {
expect(client.stopped, hasLength(1));
expect(client.stopped.single.positionMs, 100000);
expect(client.stopped.single.durationMs, 100000);
expect(client.stopped.single.offline, isTrue);
expect(client.stopped.single.continuing, isFalse);
expect(client.stopped.single.updatedAt?.millisecondsSinceEpoch, queued!.updatedAt);
expect(client.stopped.single.report.isOfflineReplay, isTrue);
expect(client.stopped.single.report.willContinue, isFalse);
expect(client.stopped.single.report.recordedAt?.millisecondsSinceEpoch, queued!.updatedAt);
expect(client.watched, ['42']);
expect(await svc.getPendingSyncCount(), 0);
});
@@ -482,7 +478,7 @@ void main() {
expect(await svc.getLocalWatchStatus('srv:1'), isFalse);
});
test('returns shouldMarkWatched for a "progress" action', () async {
test('returns true only for progress that crossed the watched threshold', () async {
final (svc: svc, db: db, mgr: mgr) = _makeService();
addTearDown(() async {
svc.dispose();
@@ -490,9 +486,9 @@ void main() {
await db.close();
});
// Below threshold → shouldMarkWatched=false → status=false.
// Below threshold is resume-only, not an explicit unwatched override.
await svc.queueProgressUpdate(serverId: 'srv', itemId: '1', viewOffset: 50, duration: 100);
expect(await svc.getLocalWatchStatus('srv:1'), isFalse);
expect(await svc.getLocalWatchStatus('srv:1'), isNull);
// Above threshold → shouldMarkWatched=true → status=true.
await svc.queueProgressUpdate(serverId: 'srv', itemId: '2', viewOffset: 99, duration: 100);
@@ -771,7 +767,7 @@ void main() {
expect(await svc.getLocalWatchStatus('jf-machine:item-1'), isTrue);
expect(await svc.getLocalViewOffset('jf-machine:item-1'), isNull);
expect(await svc.getLocalWatchStatus('jf-machine:item-1', clientScopeId: 'jf-machine/user-a'), isFalse);
expect(await svc.getLocalWatchStatus('jf-machine:item-1', clientScopeId: 'jf-machine/user-a'), isNull);
expect(await svc.getLocalViewOffset('jf-machine:item-1', clientScopeId: 'jf-machine/user-a'), 5000);
});
@@ -7,6 +7,7 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_source_info.dart';
import 'package:plezy/media/playback_report_metadata.dart';
import 'package:plezy/mpv/mpv.dart';
import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/services/offline_watch_sync_service.dart';
@@ -134,9 +135,7 @@ class _FakePlexClient implements PlexClient {
required int time,
required String state,
int? duration,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) async {
if (throwOnNextCall != null) {
final err = throwOnNextCall!;
@@ -201,9 +200,7 @@ class _FakePlexClient implements PlexClient {
Duration? duration,
String? playSessionId,
String? mediaSourceId,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) {
playbackSessionIds.add(playSessionId);
playbackStreamSelections.add((mediaSourceId: mediaSourceId, audioStreamIndex: null, subtitleStreamIndex: null));
@@ -974,9 +971,7 @@ class _ScrobblePreciseClient implements PlexClient {
required int time,
required String state,
int? duration,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) async {}
@override
@@ -1011,9 +1006,7 @@ class _ScrobblePreciseClient implements PlexClient {
Duration? duration,
String? playSessionId,
String? mediaSourceId,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) async {}
@override
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/media/playback_report_metadata.dart';
import 'package:plezy/services/playback_report_session.dart';
class _RecordingClient implements MediaServerClient {
@@ -47,9 +48,7 @@ class _RecordingClient implements MediaServerClient {
Duration? duration,
String? playSessionId,
String? mediaSourceId,
bool offline = false,
DateTime? updatedAt,
bool? continuing,
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
}) async {
calls.add('stopped-attempt:${position.inMilliseconds}:$mediaSourceId');
if (failNextStop) {