fix(downloads): rederive show rule download links before cleanup
Return initialized show and season rules from the backfill query so their coverage is recomputed from download ancestry. Rule execution links only the unwatched episodes it inspected, so the cached flag let a sibling list cleanup delete episodes the show rule covers.
This commit is contained in:
@@ -18,6 +18,7 @@ import '../services/credential_vault.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/serial_future_queue.dart';
|
||||
import '../utils/global_key_utils.dart';
|
||||
import '../utils/content_utils.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
@@ -1057,12 +1058,18 @@ class AppDatabase extends _$AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns uninitialized collection/playlist rules and every show/season
|
||||
/// rule whose local ancestry-derived links must be refreshed before cleanup.
|
||||
Future<List<SyncRuleItem>> getUninitializedSyncRulesForServer({
|
||||
required String profileId,
|
||||
required ServerId serverId,
|
||||
}) {
|
||||
return (select(syncRules)..where(
|
||||
(t) => t.profileId.equals(profileId) & t.serverId.equals(serverId) & t.downloadLinksInitialized.equals(false),
|
||||
(t) =>
|
||||
t.profileId.equals(profileId) &
|
||||
t.serverId.equals(serverId) &
|
||||
(t.downloadLinksInitialized.equals(false) |
|
||||
t.targetType.isIn(const [ContentTypes.show, ContentTypes.season])),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
@@ -3568,9 +3568,9 @@ class SyncRuleItem extends DataClass implements Insertable<SyncRuleItem> {
|
||||
final String downloadFilter;
|
||||
final bool includeSpecials;
|
||||
|
||||
/// Whether every currently-owned candidate has been associated in
|
||||
/// [SyncRuleDownloads]. Existing rules start false and are backfilled before
|
||||
/// destructive cleanup.
|
||||
/// Gates collection/playlist backfill into [SyncRuleDownloads] before
|
||||
/// destructive cleanup. Show/season coverage is re-derived from
|
||||
/// [DownloadedMedia] ancestry at cleanup time regardless of this value.
|
||||
final bool downloadLinksInitialized;
|
||||
const SyncRuleItem({
|
||||
required this.id,
|
||||
|
||||
@@ -106,9 +106,9 @@ class SyncRules extends Table {
|
||||
TextColumn get downloadFilter => text().withDefault(const Constant('unwatched'))();
|
||||
BoolColumn get includeSpecials => boolean().withDefault(const Constant(true))();
|
||||
|
||||
/// Whether every currently-owned candidate has been associated in
|
||||
/// [SyncRuleDownloads]. Existing rules start false and are backfilled before
|
||||
/// destructive cleanup.
|
||||
/// Gates collection/playlist backfill into [SyncRuleDownloads] before
|
||||
/// destructive cleanup. Show/season coverage is re-derived from
|
||||
/// [DownloadedMedia] ancestry at cleanup time regardless of this value.
|
||||
BoolColumn get downloadLinksInitialized => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
|
||||
|
||||
@@ -755,6 +755,53 @@ void main() {
|
||||
expect(await db.getDownloadOwner(profileId: 'profile-b', globalKey: 'srv:profile-shared'), isNotNull);
|
||||
});
|
||||
|
||||
test('deleteSyncRuleAndDownloads keeps episodes an initialized show rule covers', () async {
|
||||
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
|
||||
addTearDown(p.dispose);
|
||||
await p.ensureInitialized();
|
||||
final serverManager = MultiServerManager();
|
||||
addTearDown(serverManager.dispose);
|
||||
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: 'playlist', targetType: 'playlist', episodeCount: 0);
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: 'show-1', targetType: 'show', episodeCount: 1);
|
||||
final targetKey = p.syncRuleKeyFor(ServerId('srv'), 'playlist');
|
||||
final showKey = p.syncRuleKeyFor(ServerId('srv'), 'show-1');
|
||||
final targetRule = (await db.getSyncRule(targetKey))!;
|
||||
final showRule = (await db.getSyncRule(showKey))!;
|
||||
await db.markSyncRuleDownloadLinksInitialized(targetKey);
|
||||
await db.markSyncRuleDownloadLinksInitialized(showKey);
|
||||
|
||||
const downloadKey = 'srv:ep-watched';
|
||||
final episode = testMediaItem(
|
||||
id: 'ep-watched',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.episode,
|
||||
title: 'Watched episode',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'ep-watched',
|
||||
globalKey: downloadKey,
|
||||
type: 'episode',
|
||||
grandparentRatingKey: 'show-1',
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.addDownloadOwner(profileId: 'test-profile', globalKey: downloadKey);
|
||||
await db.associateSyncRuleDownload(targetRule, downloadKey);
|
||||
|
||||
p.debugSeedState(
|
||||
downloads: {downloadKey: const DownloadProgress(globalKey: downloadKey, status: DownloadStatus.completed)},
|
||||
metadata: {downloadKey: episode},
|
||||
ownedDownloadKeys: {downloadKey},
|
||||
);
|
||||
|
||||
await p.deleteSyncRuleAndDownloads(targetKey, serverManager);
|
||||
|
||||
expect(await db.getDownloadedMedia(downloadKey), isNotNull);
|
||||
expect(await db.getSyncRuleDownloadLinks(showRule.id), hasLength(1));
|
||||
});
|
||||
|
||||
test('deleteSyncRuleAndDownloads keeps an unresolvable legacy list rule intact', () async {
|
||||
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
|
||||
addTearDown(p.dispose);
|
||||
|
||||
Reference in New Issue
Block a user