fix(playback): harden offline source reporting
This commit is contained in:
+104
-44
@@ -60,7 +60,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase.forTesting(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 14;
|
||||
int get schemaVersion => 15;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@@ -206,6 +206,13 @@ class AppDatabase extends _$AppDatabase {
|
||||
() => m.create(idxOfflineWatchProgressProfile),
|
||||
);
|
||||
}
|
||||
if (from < 15) {
|
||||
appLogger.i('Adding mediaSourceId column to DownloadedMedia (v15 migration)');
|
||||
await _ignoreAlreadyExists(
|
||||
'DownloadedMedia.mediaSourceId column',
|
||||
() => m.addColumn(downloadedMedia, downloadedMedia.mediaSourceId),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -282,6 +289,52 @@ class AppDatabase extends _$AppDatabase {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<OfflineWatchProgressItem>> getWatchActionsForKey(
|
||||
String globalKey, {
|
||||
String? profileId,
|
||||
bool filterProfile = false,
|
||||
String? clientScopeId,
|
||||
bool filterClientScope = false,
|
||||
}) {
|
||||
return (select(offlineWatchProgress)
|
||||
..where(
|
||||
(t) =>
|
||||
t.globalKey.equals(globalKey) &
|
||||
(filterProfile ? _nullableTextPredicate(t.profileId, profileId) : const Constant(true)) &
|
||||
(filterClientScope ? _clientScopePredicate(t.clientScopeId, clientScopeId) : const Constant(true)),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.updatedAt)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<Map<String, List<OfflineWatchProgressItem>>> getWatchActionsForKeys(
|
||||
Set<String> globalKeys, {
|
||||
String? profileId,
|
||||
bool filterProfile = false,
|
||||
Map<String, String?>? clientScopeIdsByGlobalKey,
|
||||
}) async {
|
||||
if (globalKeys.isEmpty) return const {};
|
||||
final rows =
|
||||
await (select(offlineWatchProgress)
|
||||
..where(
|
||||
(t) =>
|
||||
t.globalKey.isIn(globalKeys) &
|
||||
(filterProfile ? _nullableTextPredicate(t.profileId, profileId) : const Constant(true)),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.updatedAt)]))
|
||||
.get();
|
||||
|
||||
final result = <String, List<OfflineWatchProgressItem>>{};
|
||||
for (final action in rows) {
|
||||
if (clientScopeIdsByGlobalKey != null && clientScopeIdsByGlobalKey.containsKey(action.globalKey)) {
|
||||
final expectedScope = clientScopeIdsByGlobalKey[action.globalKey];
|
||||
if (!_clientScopeValuesMatch(action.clientScopeId, expectedScope)) continue;
|
||||
}
|
||||
result.putIfAbsent(action.globalKey, () => <OfflineWatchProgressItem>[]).add(action);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get the latest actions for multiple items in a single query
|
||||
///
|
||||
/// Returns a map of globalKey -> latest action for each key.
|
||||
@@ -338,49 +391,53 @@ class AppDatabase extends _$AppDatabase {
|
||||
final globalKey = buildGlobalKey(serverId, ratingKey);
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Check for existing progress entry
|
||||
final existing =
|
||||
await (select(offlineWatchProgress)
|
||||
..where(
|
||||
(t) =>
|
||||
t.globalKey.equals(globalKey) &
|
||||
_nullableTextPredicate(t.profileId, profileId) &
|
||||
_clientScopePredicate(t.clientScopeId, clientScopeId) &
|
||||
t.actionType.equals(OfflineActionType.progress.id),
|
||||
)
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
await transaction(() async {
|
||||
final existing =
|
||||
await (select(offlineWatchProgress)
|
||||
..where(
|
||||
(t) =>
|
||||
t.globalKey.equals(globalKey) &
|
||||
_nullableTextPredicate(t.profileId, profileId) &
|
||||
_clientScopePredicate(t.clientScopeId, clientScopeId) &
|
||||
t.actionType.equals(OfflineActionType.progress.id),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.asc(t.id)]))
|
||||
.get();
|
||||
|
||||
if (existing != null) {
|
||||
// Update existing progress entry
|
||||
await (update(offlineWatchProgress)..where((t) => t.id.equals(existing.id))).write(
|
||||
OfflineWatchProgressCompanion(
|
||||
viewOffset: Value(viewOffset),
|
||||
duration: Value(duration),
|
||||
shouldMarkWatched: Value(shouldMarkWatched),
|
||||
profileId: Value(profileId),
|
||||
clientScopeId: Value(clientScopeId),
|
||||
updatedAt: Value(now),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Insert new progress entry
|
||||
await into(offlineWatchProgress).insert(
|
||||
OfflineWatchProgressCompanion.insert(
|
||||
serverId: serverId,
|
||||
profileId: Value(profileId),
|
||||
clientScopeId: Value(clientScopeId),
|
||||
ratingKey: ratingKey,
|
||||
globalKey: globalKey,
|
||||
actionType: OfflineActionType.progress.id,
|
||||
viewOffset: Value(viewOffset),
|
||||
duration: Value(duration),
|
||||
shouldMarkWatched: Value(shouldMarkWatched),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
final keep = existing.isEmpty ? null : existing.first;
|
||||
if (keep != null) {
|
||||
await (update(offlineWatchProgress)..where((t) => t.id.equals(keep.id))).write(
|
||||
OfflineWatchProgressCompanion(
|
||||
viewOffset: Value(viewOffset),
|
||||
duration: Value(duration),
|
||||
shouldMarkWatched: Value(shouldMarkWatched),
|
||||
profileId: Value(profileId),
|
||||
clientScopeId: Value(clientScopeId),
|
||||
updatedAt: Value(now),
|
||||
),
|
||||
);
|
||||
final duplicateIds = existing.skip(1).map((row) => row.id).toList(growable: false);
|
||||
if (duplicateIds.isNotEmpty) {
|
||||
await (delete(offlineWatchProgress)..where((t) => t.id.isIn(duplicateIds))).go();
|
||||
}
|
||||
} else {
|
||||
await into(offlineWatchProgress).insert(
|
||||
OfflineWatchProgressCompanion.insert(
|
||||
serverId: serverId,
|
||||
profileId: Value(profileId),
|
||||
clientScopeId: Value(clientScopeId),
|
||||
ratingKey: ratingKey,
|
||||
globalKey: globalKey,
|
||||
actionType: OfflineActionType.progress.id,
|
||||
viewOffset: Value(viewOffset),
|
||||
duration: Value(duration),
|
||||
shouldMarkWatched: Value(shouldMarkWatched),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a manual watch action (watched or unwatched).
|
||||
@@ -436,11 +493,14 @@ class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
|
||||
/// Get count of pending sync items
|
||||
Future<int> getPendingSyncCount({String? profileId}) async {
|
||||
Future<int> getPendingSyncCount({String? profileId, int? maxSyncAttempts}) async {
|
||||
final query = selectOnly(offlineWatchProgress)..addColumns([offlineWatchProgress.id.count()]);
|
||||
if (profileId != null) {
|
||||
query.where(offlineWatchProgress.profileId.equals(profileId));
|
||||
}
|
||||
if (maxSyncAttempts != null) {
|
||||
query.where(offlineWatchProgress.syncAttempts.isSmallerThanValue(maxSyncAttempts));
|
||||
}
|
||||
final count = await query.map((row) => row.read(offlineWatchProgress.id.count())).getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user