import 'package:drift/drift.dart'; import 'app_database.dart'; import '../models/download_models.dart'; import '../profiles/profile.dart'; extension DownloadDatabaseOperations on AppDatabase { Future addDownloadOwner({required String profileId, required String globalKey}) async { if (profileId.isEmpty) return; await into(downloadOwners).insert( DownloadOwnersCompanion.insert( profileId: profileId, globalKey: globalKey, createdAt: DateTime.now().millisecondsSinceEpoch, ), mode: InsertMode.insertOrIgnore, ); } Future removeDownloadOwner({required String profileId, required String globalKey}) async { await (delete(downloadOwners)..where((t) => t.profileId.equals(profileId) & t.globalKey.equals(globalKey))).go(); } Future removeDownloadOwnersForProfile(String profileId) async { if (profileId.isEmpty) return; await (delete(downloadOwners)..where((t) => t.profileId.equals(profileId))).go(); } Future> getDownloadOwnerKeysForProfile(String profileId) async { if (profileId.isEmpty) return const {}; final rows = await (select(downloadOwners)..where((t) => t.profileId.equals(profileId))).get(); return rows.map((row) => row.globalKey).toSet(); } Future getDownloadOwnerCount(String globalKey) async { return (await _validDownloadOwnerRows(globalKey)).length; } Future hasDownloadOwner(String globalKey, {String? excludingProfileId}) async { final rows = await _validDownloadOwnerRows(globalKey, excludingProfileId: excludingProfileId); return rows.isNotEmpty; } Future> _validDownloadOwnerRows(String globalKey, {String? excludingProfileId}) async { final rows = await (select(downloadOwners)..where((t) => t.globalKey.equals(globalKey))).get(); if (rows.isEmpty) return const []; final candidates = rows .where((row) => excludingProfileId == null || excludingProfileId.isEmpty || row.profileId != excludingProfileId) .toList(growable: false); if (candidates.isEmpty) return const []; final localProfileRows = await select(profiles).get(); final localProfileIds = localProfileRows.map((row) => row.id).toSet(); final connectionRows = await select(connections).get(); final connectionIds = connectionRows.map((row) => row.id).toSet(); return candidates .where((row) { if (localProfileIds.contains(row.profileId)) return true; final plexHome = parsePlexHomeProfileId(row.profileId); if (plexHome != null) return connectionIds.contains(plexHome.accountConnectionId); return localProfileIds.isEmpty; }) .toList(growable: false); } /// Claim pre-v17 shared download rows for [profileId]. Rows that already /// have any owner are left untouched so later profiles do not inherit them. Future adoptLegacyDownloadsForProfile(String profileId) async { if (profileId.isEmpty) return; final rows = await select(downloadedMedia).get(); for (final row in rows) { if (await getDownloadOwnerCount(row.globalKey) == 0) { await addDownloadOwner(profileId: profileId, globalKey: row.globalKey); } } } Future insertDownload({ required String serverId, String? clientScopeId, required String ratingKey, required String globalKey, required String type, String? parentRatingKey, String? grandparentRatingKey, required int status, int mediaIndex = 0, }) async { await into(downloadedMedia).insert( DownloadedMediaCompanion.insert( serverId: serverId, clientScopeId: Value(clientScopeId), ratingKey: ratingKey, globalKey: globalKey, type: type, parentRatingKey: Value(parentRatingKey), grandparentRatingKey: Value(grandparentRatingKey), status: status, mediaIndex: Value(mediaIndex), ), mode: InsertMode.insertOrReplace, ); } Future addToQueue({ required String mediaGlobalKey, int priority = 0, bool downloadSubtitles = true, bool downloadArtwork = true, }) async { await into(downloadQueue).insert( DownloadQueueCompanion.insert( mediaGlobalKey: mediaGlobalKey, priority: Value(priority), addedAt: DateTime.now().millisecondsSinceEpoch, downloadSubtitles: Value(downloadSubtitles), downloadArtwork: Value(downloadArtwork), ), mode: InsertMode.insertOrReplace, ); } /// Get next item from queue (highest priority, oldest first) /// Only returns items that are not paused Future getNextQueueItem() async { final query = select( downloadQueue, ).join([innerJoin(downloadedMedia, downloadedMedia.globalKey.equalsExp(downloadQueue.mediaGlobalKey))]); query ..where(downloadedMedia.status.equals(DownloadStatus.queued.index)) ..orderBy([ OrderingTerm(expression: downloadQueue.priority, mode: OrderingMode.desc), OrderingTerm(expression: downloadQueue.addedAt), ]) ..limit(1); final result = await query.getSingleOrNull(); return result?.readTable(downloadQueue); } Future updateDownloadStatus(String globalKey, int status) async { await (update( downloadedMedia, )..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(status: Value(status))); } Future updateDownloadProgress(String globalKey, int progress, int downloadedBytes, int totalBytes) async { await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( DownloadedMediaCompanion( progress: Value(progress), downloadedBytes: Value(downloadedBytes), totalBytes: Value(totalBytes), ), ); } Future updateVideoFilePath(String globalKey, String filePath) async { await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( DownloadedMediaCompanion( videoFilePath: Value(filePath), downloadedAt: Value(DateTime.now().millisecondsSinceEpoch), ), ); } Future updateArtworkPaths({required String globalKey, String? thumbPath}) async { await (update( downloadedMedia, )..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(thumbPath: Value(thumbPath))); } Future updateDownloadError(String globalKey, String errorMessage) async { final existing = await getDownloadedMedia(globalKey); final currentCount = existing?.retryCount ?? 0; await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( DownloadedMediaCompanion(errorMessage: Value(errorMessage), retryCount: Value(currentCount + 1)), ); } Future clearDownloadError(String globalKey) async { await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( const DownloadedMediaCompanion(errorMessage: Value(null), retryCount: Value(0)), ); } Future removeFromQueue(String mediaGlobalKey) async { await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(mediaGlobalKey))).go(); } Future getDownloadedMedia(String globalKey) { return (select(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull(); } Future deleteDownload(String globalKey) async { 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(); } Future> getEpisodesBySeason( String seasonKey, { String? serverId, String? clientScopeId, bool filterClientScope = false, }) { return (select(downloadedMedia)..where( (t) => t.parentRatingKey.equals(seasonKey) & _optionalServerPredicate(t.serverId, serverId) & _optionalClientScopePredicate(t.clientScopeId, clientScopeId, filterClientScope: filterClientScope), )) .get(); } Future> getEpisodesByShow( String showKey, { String? serverId, String? clientScopeId, bool filterClientScope = false, }) { return (select(downloadedMedia)..where( (t) => t.grandparentRatingKey.equals(showKey) & _optionalServerPredicate(t.serverId, serverId) & _optionalClientScopePredicate(t.clientScopeId, clientScopeId, filterClientScope: filterClientScope), )) .get(); } Future> getDownloadsByServerId(String serverId) { return (select(downloadedMedia)..where((t) => t.serverId.equals(serverId))).get(); } Expression _optionalServerPredicate(GeneratedColumn column, String? serverId) { return serverId == null ? const Constant(true) : column.equals(serverId); } Expression _optionalClientScopePredicate( GeneratedColumn column, String? clientScopeId, { required bool filterClientScope, }) { if (!filterClientScope && (clientScopeId == null || clientScopeId.isEmpty)) { return const Constant(true); } if (clientScopeId == null || clientScopeId.isEmpty) { return column.isNull() | column.equals(''); } return column.equals(clientScopeId); } Future updateBgTaskId(String globalKey, String? taskId) async { await (update( downloadedMedia, )..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(bgTaskId: Value(taskId))); } Future getBgTaskId(String globalKey) async { final item = await getDownloadedMedia(globalKey); return item?.bgTaskId; } }