feat(downloads): remove playlist sync downloads together

close #1656
This commit is contained in:
edde746
2026-07-26 14:58:38 +02:00
parent 1fea9ef6e3
commit 60cc983471
32 changed files with 2025 additions and 136 deletions
+101 -1
View File
@@ -62,6 +62,7 @@ final class AppDatabaseBootstrap {
ApiCache,
OfflineWatchProgress,
SyncRules,
SyncRuleDownloads,
Connections,
Profiles,
ProfileConnections,
@@ -495,7 +496,7 @@ class AppDatabase extends _$AppDatabase {
}
@override
int get schemaVersion => 19;
int get schemaVersion => 20;
@override
MigrationStrategy get migration {
@@ -911,6 +912,18 @@ class AppDatabase extends _$AppDatabase {
WHERE backend IS NULL
''');
}
if (from < 20) {
appLogger.i('Adding sync rule download associations (v20 migration)');
await _ignoreAlreadyExists(
'SyncRules.downloadLinksInitialized column',
() => m.addColumn(syncRules, syncRules.downloadLinksInitialized),
);
await _ignoreAlreadyExists('SyncRuleDownloads table', () => m.createTable(syncRuleDownloads));
await _ignoreAlreadyExists(
'Index idx_sync_rule_downloads_profile_key',
() => m.create(idxSyncRuleDownloadsProfileKey),
);
}
},
);
}
@@ -1230,6 +1243,81 @@ class AppDatabase extends _$AppDatabase {
return (select(syncRules)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull();
}
Future<void> associateSyncRuleDownload(SyncRuleItem rule, String downloadGlobalKey) {
return into(syncRuleDownloads).insertOnConflictUpdate(
SyncRuleDownloadsCompanion.insert(
syncRuleId: rule.id,
profileId: rule.profileId,
downloadGlobalKey: downloadGlobalKey,
),
);
}
Future<void> markSyncRuleDownloadLinksInitialized(String globalKey) {
return (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(
const SyncRulesCompanion(downloadLinksInitialized: Value(true)),
);
}
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),
))
.get();
}
Future<List<SyncRuleDownloadItem>> getSyncRuleDownloadLinks(int syncRuleId) {
return (select(syncRuleDownloads)..where((t) => t.syncRuleId.equals(syncRuleId))).get();
}
Future<List<String>> getOwnedDownloadKeysForAncestorRule({
required String profileId,
required ServerId serverId,
required String ratingKey,
required bool matchGrandparent,
}) async {
final query = select(
downloadedMedia,
).join([innerJoin(downloadOwners, downloadOwners.globalKey.equalsExp(downloadedMedia.globalKey))]);
final ancestorMatches = matchGrandparent
? downloadedMedia.grandparentRatingKey.equals(ratingKey) | downloadedMedia.parentRatingKey.equals(ratingKey)
: downloadedMedia.parentRatingKey.equals(ratingKey);
query.where(
downloadOwners.profileId.equals(profileId) &
downloadedMedia.serverId.equals(serverId) &
downloadedMedia.status.isIn([
DownloadStatus.queued.index,
DownloadStatus.downloading.index,
DownloadStatus.completed.index,
DownloadStatus.paused.index,
]) &
ancestorMatches,
);
final rows = await query.get();
return rows.map((row) => row.readTable(downloadedMedia).globalKey).toList(growable: false);
}
Future<List<String>> getExclusiveSyncRuleDownloadKeys(SyncRuleItem rule) async {
final links = await getSyncRuleDownloadLinks(rule.id);
if (links.isEmpty) return const [];
final keys = links.map((link) => link.downloadGlobalKey).toSet();
final allLinks = await (select(
syncRuleDownloads,
)..where((t) => t.profileId.equals(rule.profileId) & t.downloadGlobalKey.isIn(keys))).get();
final linkedRuleCounts = <String, int>{};
for (final link in allLinks) {
linkedRuleCounts.update(link.downloadGlobalKey, (count) => count + 1, ifAbsent: () => 1);
}
return [
for (final key in keys)
if (linkedRuleCounts[key] == 1) key,
];
}
Future<void> insertSyncRule({
String profileId = '',
required ServerId serverId,
@@ -1290,6 +1378,9 @@ class AppDatabase extends _$AppDatabase {
await (update(syncRules)..where((t) => t.id.equals(rule.id))).write(
SyncRulesCompanion(profileId: Value(profileId), globalKey: Value(scopedKey)),
);
await (update(
syncRuleDownloads,
)..where((t) => t.syncRuleId.equals(rule.id))).write(SyncRuleDownloadsCompanion(profileId: Value(profileId)));
}
}
@@ -1317,6 +1408,15 @@ class AppDatabase extends _$AppDatabase {
);
}
Future<void> completeSyncRuleExecution(String globalKey) {
return (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(
SyncRulesCompanion(
lastExecutedAt: Value(DateTime.now().millisecondsSinceEpoch),
downloadLinksInitialized: const Value(true),
),
);
}
Future<void> deleteSyncRule(String globalKey) async {
await (delete(syncRules)..where((t) => t.globalKey.equals(globalKey))).go();
}
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -51,8 +51,13 @@ extension DownloadDatabaseOperations on AppDatabase {
);
}
Future<void> removeDownloadOwner({required String profileId, required String globalKey}) async {
await (delete(downloadOwners)..where((t) => t.profileId.equals(profileId) & t.globalKey.equals(globalKey))).go();
Future<void> removeDownloadOwner({required String profileId, required String globalKey}) {
return transaction(() async {
await (delete(
syncRuleDownloads,
)..where((t) => t.profileId.equals(profileId) & t.downloadGlobalKey.equals(globalKey))).go();
await (delete(downloadOwners)..where((t) => t.profileId.equals(profileId) & t.globalKey.equals(globalKey))).go();
});
}
/// Removes one owner from a shared download while keeping an incomplete
@@ -102,8 +107,12 @@ extension DownloadDatabaseOperations on AppDatabase {
)..where((t) => t.profileId.equals(profileId) & t.globalKey.equals(globalKey))).getSingleOrNull();
}
Future<void> clearAllDownloadOwners() async {
await delete(downloadOwners).go();
Future<void> clearAllDownloadOwners() {
return transaction(() async {
await delete(syncRuleDownloads).go();
await update(syncRules).write(const SyncRulesCompanion(downloadLinksInitialized: Value(false)));
await delete(downloadOwners).go();
});
}
Future<Set<String>> getDownloadOwnerKeysForProfile(String profileId) async {
@@ -620,6 +629,7 @@ extension DownloadDatabaseOperations on AppDatabase {
late String? safRootUri;
await transaction(() async {
safRootUri = (await getDownloadedMedia(globalKey))?.safRootUri;
await (delete(syncRuleDownloads)..where((t) => t.downloadGlobalKey.equals(globalKey))).go();
await (delete(downloadOwners)..where((t) => t.globalKey.equals(globalKey))).go();
await (delete(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).go();
await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(globalKey))).go();
+21
View File
@@ -105,6 +105,27 @@ class SyncRules extends Table {
IntColumn get mediaIndex => integer().withDefault(const Constant(0))();
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.
BoolColumn get downloadLinksInitialized => boolean().withDefault(const Constant(false))();
}
/// Downloads covered by a sync rule for one profile.
///
/// Links are retained when list membership changes so removing a rule can
/// clean up items it previously synced without re-fetching the list. A
/// download may be linked to multiple rules.
@DataClassName('SyncRuleDownloadItem')
@TableIndex(name: 'idx_sync_rule_downloads_profile_key', columns: {#profileId, #downloadGlobalKey})
class SyncRuleDownloads extends Table {
IntColumn get syncRuleId => integer().references(SyncRules, #id, onDelete: KeyAction.cascade)();
TextColumn get profileId => text()();
TextColumn get downloadGlobalKey => text()();
@override
Set<Column> get primaryKey => {syncRuleId, downloadGlobalKey};
}
/// Persisted media-server connections.