From d986f0d2ea95a0174b9417319db56db9d2159372 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:00:39 +0100 Subject: [PATCH] feat: migrate downloads to background_downloader close #454 --- android/app/src/main/AndroidManifest.xml | 4 + ios/Podfile.lock | 6 + ios/Runner/Info.plist | 5 + lib/database/app_database.dart | 7 +- lib/database/app_database.g.dart | 1596 +++++++++++++++----- lib/database/tables.dart | 1 + lib/services/download_manager_service.dart | 999 ++++++------ lib/widgets/download_tree_view.dart | 15 +- pubspec.lock | 8 + pubspec.yaml | 1 + 10 files changed, 1762 insertions(+), 880 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b731f830..4cda54be 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,10 @@ + + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4804c94b..15e44fbf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - background_downloader (0.0.1): + - Flutter - connectivity_plus (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -105,6 +107,7 @@ PODS: - Flutter DEPENDENCIES: + - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -138,6 +141,8 @@ SPEC REPOS: - sqlite3 EXTERNAL SOURCES: + background_downloader: + :path: ".symlinks/plugins/background_downloader/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: @@ -172,6 +177,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/workmanager_apple/ios" SPEC CHECKSUMS: + background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5278ba84..a3b398c7 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -49,6 +49,11 @@ LaunchScreen UIMainStoryboardFile Main + UIBackgroundModes + + fetch + processing + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index 9bcbe0d6..ed635ff1 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -16,7 +16,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 7; // Added OfflineWatchProgress table + int get schemaVersion => 8; // Added bgTaskId column to DownloadedMedia @override MigrationStrategy get migration { @@ -25,11 +25,14 @@ class AppDatabase extends _$AppDatabase { await m.createAll(); }, onUpgrade: (Migrator m, int from, int to) async { - // Additive migration for schema version 7 if (from < 7) { appLogger.i('Adding OfflineWatchProgress table (v7 migration)'); await m.createTable(offlineWatchProgress); } + if (from < 8) { + appLogger.i('Adding bgTaskId column to DownloadedMedia (v8 migration)'); + await m.addColumn(downloadedMedia, downloadedMedia.bgTaskId); + } }, ); } diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 6844c0e6..28e81134 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -3,7 +3,8 @@ part of 'app_database.dart'; // ignore_for_file: type=lint -class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { +class $DownloadedMediaTable extends DownloadedMedia + with TableInfo<$DownloadedMediaTable, DownloadedMediaItem> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -17,9 +18,13 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', ); - static const VerificationMeta _serverIdMeta = const VerificationMeta('serverId'); @override late final GeneratedColumn serverId = GeneratedColumn( 'server_id', @@ -28,7 +33,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _ratingKeyMeta = const VerificationMeta('ratingKey'); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta( + 'ratingKey', + ); @override late final GeneratedColumn ratingKey = GeneratedColumn( 'rating_key', @@ -37,7 +44,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _globalKeyMeta = const VerificationMeta('globalKey'); + static const VerificationMeta _globalKeyMeta = const VerificationMeta( + 'globalKey', + ); @override late final GeneratedColumn globalKey = GeneratedColumn( 'global_key', @@ -56,7 +65,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta('parentRatingKey'); + static const VerificationMeta _parentRatingKeyMeta = const VerificationMeta( + 'parentRatingKey', + ); @override late final GeneratedColumn parentRatingKey = GeneratedColumn( 'parent_rating_key', @@ -65,15 +76,17 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _grandparentRatingKeyMeta = const VerificationMeta('grandparentRatingKey'); + static const VerificationMeta _grandparentRatingKeyMeta = + const VerificationMeta('grandparentRatingKey'); @override - late final GeneratedColumn grandparentRatingKey = GeneratedColumn( - 'grandparent_rating_key', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); + late final GeneratedColumn grandparentRatingKey = + GeneratedColumn( + 'grandparent_rating_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); static const VerificationMeta _statusMeta = const VerificationMeta('status'); @override late final GeneratedColumn status = GeneratedColumn( @@ -83,7 +96,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _progressMeta = const VerificationMeta('progress'); + static const VerificationMeta _progressMeta = const VerificationMeta( + 'progress', + ); @override late final GeneratedColumn progress = GeneratedColumn( 'progress', @@ -93,7 +108,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _totalBytesMeta = const VerificationMeta('totalBytes'); + static const VerificationMeta _totalBytesMeta = const VerificationMeta( + 'totalBytes', + ); @override late final GeneratedColumn totalBytes = GeneratedColumn( 'total_bytes', @@ -102,7 +119,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _downloadedBytesMeta = const VerificationMeta('downloadedBytes'); + static const VerificationMeta _downloadedBytesMeta = const VerificationMeta( + 'downloadedBytes', + ); @override late final GeneratedColumn downloadedBytes = GeneratedColumn( 'downloaded_bytes', @@ -112,7 +131,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _videoFilePathMeta = const VerificationMeta('videoFilePath'); + static const VerificationMeta _videoFilePathMeta = const VerificationMeta( + 'videoFilePath', + ); @override late final GeneratedColumn videoFilePath = GeneratedColumn( 'video_file_path', @@ -121,7 +142,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _thumbPathMeta = const VerificationMeta('thumbPath'); + static const VerificationMeta _thumbPathMeta = const VerificationMeta( + 'thumbPath', + ); @override late final GeneratedColumn thumbPath = GeneratedColumn( 'thumb_path', @@ -130,7 +153,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _downloadedAtMeta = const VerificationMeta('downloadedAt'); + static const VerificationMeta _downloadedAtMeta = const VerificationMeta( + 'downloadedAt', + ); @override late final GeneratedColumn downloadedAt = GeneratedColumn( 'downloaded_at', @@ -139,7 +164,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _errorMessageMeta = const VerificationMeta('errorMessage'); + static const VerificationMeta _errorMessageMeta = const VerificationMeta( + 'errorMessage', + ); @override late final GeneratedColumn errorMessage = GeneratedColumn( 'error_message', @@ -148,7 +175,9 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _retryCountMeta = const VerificationMeta('retryCount'); + static const VerificationMeta _retryCountMeta = const VerificationMeta( + 'retryCount', + ); @override late final GeneratedColumn retryCount = GeneratedColumn( 'retry_count', @@ -158,6 +187,17 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe requiredDuringInsert: false, defaultValue: const Constant(0), ); + static const VerificationMeta _bgTaskIdMeta = const VerificationMeta( + 'bgTaskId', + ); + @override + late final GeneratedColumn bgTaskId = GeneratedColumn( + 'bg_task_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); @override List get $columns => [ id, @@ -176,6 +216,7 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe downloadedAt, errorMessage, retryCount, + bgTaskId, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -183,78 +224,138 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe String get actualTableName => $name; static const String $name = 'downloaded_media'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } if (data.containsKey('server_id')) { - context.handle(_serverIdMeta, serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta)); + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); } else if (isInserting) { context.missing(_serverIdMeta); } if (data.containsKey('rating_key')) { - context.handle(_ratingKeyMeta, ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta)); + context.handle( + _ratingKeyMeta, + ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), + ); } else if (isInserting) { context.missing(_ratingKeyMeta); } if (data.containsKey('global_key')) { - context.handle(_globalKeyMeta, globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta)); + context.handle( + _globalKeyMeta, + globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), + ); } else if (isInserting) { context.missing(_globalKeyMeta); } if (data.containsKey('type')) { - context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); } else if (isInserting) { context.missing(_typeMeta); } if (data.containsKey('parent_rating_key')) { context.handle( _parentRatingKeyMeta, - parentRatingKey.isAcceptableOrUnknown(data['parent_rating_key']!, _parentRatingKeyMeta), + parentRatingKey.isAcceptableOrUnknown( + data['parent_rating_key']!, + _parentRatingKeyMeta, + ), ); } if (data.containsKey('grandparent_rating_key')) { context.handle( _grandparentRatingKeyMeta, - grandparentRatingKey.isAcceptableOrUnknown(data['grandparent_rating_key']!, _grandparentRatingKeyMeta), + grandparentRatingKey.isAcceptableOrUnknown( + data['grandparent_rating_key']!, + _grandparentRatingKeyMeta, + ), ); } if (data.containsKey('status')) { - context.handle(_statusMeta, status.isAcceptableOrUnknown(data['status']!, _statusMeta)); + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); } else if (isInserting) { context.missing(_statusMeta); } if (data.containsKey('progress')) { - context.handle(_progressMeta, progress.isAcceptableOrUnknown(data['progress']!, _progressMeta)); + context.handle( + _progressMeta, + progress.isAcceptableOrUnknown(data['progress']!, _progressMeta), + ); } if (data.containsKey('total_bytes')) { - context.handle(_totalBytesMeta, totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta)); + context.handle( + _totalBytesMeta, + totalBytes.isAcceptableOrUnknown(data['total_bytes']!, _totalBytesMeta), + ); } if (data.containsKey('downloaded_bytes')) { context.handle( _downloadedBytesMeta, - downloadedBytes.isAcceptableOrUnknown(data['downloaded_bytes']!, _downloadedBytesMeta), + downloadedBytes.isAcceptableOrUnknown( + data['downloaded_bytes']!, + _downloadedBytesMeta, + ), ); } if (data.containsKey('video_file_path')) { context.handle( _videoFilePathMeta, - videoFilePath.isAcceptableOrUnknown(data['video_file_path']!, _videoFilePathMeta), + videoFilePath.isAcceptableOrUnknown( + data['video_file_path']!, + _videoFilePathMeta, + ), ); } if (data.containsKey('thumb_path')) { - context.handle(_thumbPathMeta, thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta)); + context.handle( + _thumbPathMeta, + thumbPath.isAcceptableOrUnknown(data['thumb_path']!, _thumbPathMeta), + ); } if (data.containsKey('downloaded_at')) { - context.handle(_downloadedAtMeta, downloadedAt.isAcceptableOrUnknown(data['downloaded_at']!, _downloadedAtMeta)); + context.handle( + _downloadedAtMeta, + downloadedAt.isAcceptableOrUnknown( + data['downloaded_at']!, + _downloadedAtMeta, + ), + ); } if (data.containsKey('error_message')) { - context.handle(_errorMessageMeta, errorMessage.isAcceptableOrUnknown(data['error_message']!, _errorMessageMeta)); + context.handle( + _errorMessageMeta, + errorMessage.isAcceptableOrUnknown( + data['error_message']!, + _errorMessageMeta, + ), + ); } if (data.containsKey('retry_count')) { - context.handle(_retryCountMeta, retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta)); + context.handle( + _retryCountMeta, + retryCount.isAcceptableOrUnknown(data['retry_count']!, _retryCountMeta), + ); + } + if (data.containsKey('bg_task_id')) { + context.handle( + _bgTaskIdMeta, + bgTaskId.isAcceptableOrUnknown(data['bg_task_id']!, _bgTaskIdMeta), + ); } return context; } @@ -265,11 +366,26 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe DownloadedMediaItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DownloadedMediaItem( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - serverId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}server_id'])!, - ratingKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}rating_key'])!, - globalKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}global_key'])!, - type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ratingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}rating_key'], + )!, + globalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}global_key'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, parentRatingKey: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}parent_rating_key'], @@ -278,15 +394,46 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe DriftSqlType.string, data['${effectivePrefix}grandparent_rating_key'], ), - status: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}status'])!, - progress: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}progress'])!, - totalBytes: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}total_bytes']), - downloadedBytes: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}downloaded_bytes'])!, - videoFilePath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}video_file_path']), - thumbPath: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}thumb_path']), - downloadedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}downloaded_at']), - errorMessage: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}error_message']), - retryCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, + status: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}status'], + )!, + progress: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}progress'], + )!, + totalBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_bytes'], + ), + downloadedBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}downloaded_bytes'], + )!, + videoFilePath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}video_file_path'], + ), + thumbPath: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_path'], + ), + downloadedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}downloaded_at'], + ), + errorMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}error_message'], + ), + retryCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}retry_count'], + )!, + bgTaskId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}bg_task_id'], + ), ); } @@ -296,7 +443,8 @@ class $DownloadedMediaTable extends DownloadedMedia with TableInfo<$DownloadedMe } } -class DownloadedMediaItem extends DataClass implements Insertable { +class DownloadedMediaItem extends DataClass + implements Insertable { final int id; final String serverId; final String ratingKey; @@ -313,6 +461,7 @@ class DownloadedMediaItem extends DataClass implements Insertable toColumns(bool nullToAbsent) { @@ -364,6 +514,9 @@ class DownloadedMediaItem extends DataClass implements Insertable(errorMessage); } map['retry_count'] = Variable(retryCount); + if (!nullToAbsent || bgTaskId != null) { + map['bg_task_id'] = Variable(bgTaskId); + } return map; } @@ -374,23 +527,41 @@ class DownloadedMediaItem extends DataClass implements Insertable json, {ValueSerializer? serializer}) { + factory DownloadedMediaItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return DownloadedMediaItem( id: serializer.fromJson(json['id']), @@ -399,7 +570,9 @@ class DownloadedMediaItem extends DataClass implements Insertable(json['globalKey']), type: serializer.fromJson(json['type']), parentRatingKey: serializer.fromJson(json['parentRatingKey']), - grandparentRatingKey: serializer.fromJson(json['grandparentRatingKey']), + grandparentRatingKey: serializer.fromJson( + json['grandparentRatingKey'], + ), status: serializer.fromJson(json['status']), progress: serializer.fromJson(json['progress']), totalBytes: serializer.fromJson(json['totalBytes']), @@ -409,6 +582,7 @@ class DownloadedMediaItem extends DataClass implements Insertable(json['downloadedAt']), errorMessage: serializer.fromJson(json['errorMessage']), retryCount: serializer.fromJson(json['retryCount']), + bgTaskId: serializer.fromJson(json['bgTaskId']), ); } @override @@ -431,6 +605,7 @@ class DownloadedMediaItem extends DataClass implements Insertable(downloadedAt), 'errorMessage': serializer.toJson(errorMessage), 'retryCount': serializer.toJson(retryCount), + 'bgTaskId': serializer.toJson(bgTaskId), }; } @@ -451,23 +626,31 @@ class DownloadedMediaItem extends DataClass implements Insertable downloadedAt = const Value.absent(), Value errorMessage = const Value.absent(), int? retryCount, + Value bgTaskId = const Value.absent(), }) => DownloadedMediaItem( id: id ?? this.id, serverId: serverId ?? this.serverId, ratingKey: ratingKey ?? this.ratingKey, globalKey: globalKey ?? this.globalKey, type: type ?? this.type, - parentRatingKey: parentRatingKey.present ? parentRatingKey.value : this.parentRatingKey, - grandparentRatingKey: grandparentRatingKey.present ? grandparentRatingKey.value : this.grandparentRatingKey, + parentRatingKey: parentRatingKey.present + ? parentRatingKey.value + : this.parentRatingKey, + grandparentRatingKey: grandparentRatingKey.present + ? grandparentRatingKey.value + : this.grandparentRatingKey, status: status ?? this.status, progress: progress ?? this.progress, totalBytes: totalBytes.present ? totalBytes.value : this.totalBytes, downloadedBytes: downloadedBytes ?? this.downloadedBytes, - videoFilePath: videoFilePath.present ? videoFilePath.value : this.videoFilePath, + videoFilePath: videoFilePath.present + ? videoFilePath.value + : this.videoFilePath, thumbPath: thumbPath.present ? thumbPath.value : this.thumbPath, downloadedAt: downloadedAt.present ? downloadedAt.value : this.downloadedAt, errorMessage: errorMessage.present ? errorMessage.value : this.errorMessage, retryCount: retryCount ?? this.retryCount, + bgTaskId: bgTaskId.present ? bgTaskId.value : this.bgTaskId, ); DownloadedMediaItem copyWithCompanion(DownloadedMediaCompanion data) { return DownloadedMediaItem( @@ -476,19 +659,34 @@ class DownloadedMediaItem extends DataClass implements Insertable @@ -553,7 +753,8 @@ class DownloadedMediaItem extends DataClass implements Insertable { @@ -573,6 +774,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { final Value downloadedAt; final Value errorMessage; final Value retryCount; + final Value bgTaskId; const DownloadedMediaCompanion({ this.id = const Value.absent(), this.serverId = const Value.absent(), @@ -590,6 +792,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { this.downloadedAt = const Value.absent(), this.errorMessage = const Value.absent(), this.retryCount = const Value.absent(), + this.bgTaskId = const Value.absent(), }); DownloadedMediaCompanion.insert({ this.id = const Value.absent(), @@ -608,6 +811,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { this.downloadedAt = const Value.absent(), this.errorMessage = const Value.absent(), this.retryCount = const Value.absent(), + this.bgTaskId = const Value.absent(), }) : serverId = Value(serverId), ratingKey = Value(ratingKey), globalKey = Value(globalKey), @@ -630,6 +834,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { Expression? downloadedAt, Expression? errorMessage, Expression? retryCount, + Expression? bgTaskId, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -638,7 +843,8 @@ class DownloadedMediaCompanion extends UpdateCompanion { if (globalKey != null) 'global_key': globalKey, if (type != null) 'type': type, if (parentRatingKey != null) 'parent_rating_key': parentRatingKey, - if (grandparentRatingKey != null) 'grandparent_rating_key': grandparentRatingKey, + if (grandparentRatingKey != null) + 'grandparent_rating_key': grandparentRatingKey, if (status != null) 'status': status, if (progress != null) 'progress': progress, if (totalBytes != null) 'total_bytes': totalBytes, @@ -648,6 +854,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { if (downloadedAt != null) 'downloaded_at': downloadedAt, if (errorMessage != null) 'error_message': errorMessage, if (retryCount != null) 'retry_count': retryCount, + if (bgTaskId != null) 'bg_task_id': bgTaskId, }); } @@ -668,6 +875,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { Value? downloadedAt, Value? errorMessage, Value? retryCount, + Value? bgTaskId, }) { return DownloadedMediaCompanion( id: id ?? this.id, @@ -686,6 +894,7 @@ class DownloadedMediaCompanion extends UpdateCompanion { downloadedAt: downloadedAt ?? this.downloadedAt, errorMessage: errorMessage ?? this.errorMessage, retryCount: retryCount ?? this.retryCount, + bgTaskId: bgTaskId ?? this.bgTaskId, ); } @@ -711,7 +920,9 @@ class DownloadedMediaCompanion extends UpdateCompanion { map['parent_rating_key'] = Variable(parentRatingKey.value); } if (grandparentRatingKey.present) { - map['grandparent_rating_key'] = Variable(grandparentRatingKey.value); + map['grandparent_rating_key'] = Variable( + grandparentRatingKey.value, + ); } if (status.present) { map['status'] = Variable(status.value); @@ -740,6 +951,9 @@ class DownloadedMediaCompanion extends UpdateCompanion { if (retryCount.present) { map['retry_count'] = Variable(retryCount.value); } + if (bgTaskId.present) { + map['bg_task_id'] = Variable(bgTaskId.value); + } return map; } @@ -761,13 +975,15 @@ class DownloadedMediaCompanion extends UpdateCompanion { ..write('thumbPath: $thumbPath, ') ..write('downloadedAt: $downloadedAt, ') ..write('errorMessage: $errorMessage, ') - ..write('retryCount: $retryCount') + ..write('retryCount: $retryCount, ') + ..write('bgTaskId: $bgTaskId') ..write(')')) .toString(); } } -class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTable, DownloadQueueItem> { +class $DownloadQueueTable extends DownloadQueue + with TableInfo<$DownloadQueueTable, DownloadQueueItem> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -781,9 +997,13 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta( + 'mediaGlobalKey', ); - static const VerificationMeta _mediaGlobalKeyMeta = const VerificationMeta('mediaGlobalKey'); @override late final GeneratedColumn mediaGlobalKey = GeneratedColumn( 'media_global_key', @@ -793,7 +1013,9 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), ); - static const VerificationMeta _priorityMeta = const VerificationMeta('priority'); + static const VerificationMeta _priorityMeta = const VerificationMeta( + 'priority', + ); @override late final GeneratedColumn priority = GeneratedColumn( 'priority', @@ -803,7 +1025,9 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _addedAtMeta = const VerificationMeta('addedAt'); + static const VerificationMeta _addedAtMeta = const VerificationMeta( + 'addedAt', + ); @override late final GeneratedColumn addedAt = GeneratedColumn( 'added_at', @@ -812,7 +1036,9 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta('downloadSubtitles'); + static const VerificationMeta _downloadSubtitlesMeta = const VerificationMeta( + 'downloadSubtitles', + ); @override late final GeneratedColumn downloadSubtitles = GeneratedColumn( 'download_subtitles', @@ -820,10 +1046,14 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("download_subtitles" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("download_subtitles" IN (0, 1))', + ), defaultValue: const Constant(true), ); - static const VerificationMeta _downloadArtworkMeta = const VerificationMeta('downloadArtwork'); + static const VerificationMeta _downloadArtworkMeta = const VerificationMeta( + 'downloadArtwork', + ); @override late final GeneratedColumn downloadArtwork = GeneratedColumn( 'download_artwork', @@ -831,18 +1061,30 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("download_artwork" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("download_artwork" IN (0, 1))', + ), defaultValue: const Constant(true), ); @override - List get $columns => [id, mediaGlobalKey, priority, addedAt, downloadSubtitles, downloadArtwork]; + List get $columns => [ + id, + mediaGlobalKey, + priority, + addedAt, + downloadSubtitles, + downloadArtwork, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'download_queue'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -851,29 +1093,44 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab if (data.containsKey('media_global_key')) { context.handle( _mediaGlobalKeyMeta, - mediaGlobalKey.isAcceptableOrUnknown(data['media_global_key']!, _mediaGlobalKeyMeta), + mediaGlobalKey.isAcceptableOrUnknown( + data['media_global_key']!, + _mediaGlobalKeyMeta, + ), ); } else if (isInserting) { context.missing(_mediaGlobalKeyMeta); } if (data.containsKey('priority')) { - context.handle(_priorityMeta, priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta)); + context.handle( + _priorityMeta, + priority.isAcceptableOrUnknown(data['priority']!, _priorityMeta), + ); } if (data.containsKey('added_at')) { - context.handle(_addedAtMeta, addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta)); + context.handle( + _addedAtMeta, + addedAt.isAcceptableOrUnknown(data['added_at']!, _addedAtMeta), + ); } else if (isInserting) { context.missing(_addedAtMeta); } if (data.containsKey('download_subtitles')) { context.handle( _downloadSubtitlesMeta, - downloadSubtitles.isAcceptableOrUnknown(data['download_subtitles']!, _downloadSubtitlesMeta), + downloadSubtitles.isAcceptableOrUnknown( + data['download_subtitles']!, + _downloadSubtitlesMeta, + ), ); } if (data.containsKey('download_artwork')) { context.handle( _downloadArtworkMeta, - downloadArtwork.isAcceptableOrUnknown(data['download_artwork']!, _downloadArtworkMeta), + downloadArtwork.isAcceptableOrUnknown( + data['download_artwork']!, + _downloadArtworkMeta, + ), ); } return context; @@ -885,13 +1142,22 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab DownloadQueueItem map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DownloadQueueItem( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, mediaGlobalKey: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}media_global_key'], )!, - priority: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}priority'])!, - addedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}added_at'])!, + priority: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}priority'], + )!, + addedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}added_at'], + )!, downloadSubtitles: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}download_subtitles'], @@ -909,7 +1175,8 @@ class $DownloadQueueTable extends DownloadQueue with TableInfo<$DownloadQueueTab } } -class DownloadQueueItem extends DataClass implements Insertable { +class DownloadQueueItem extends DataClass + implements Insertable { final int id; final String mediaGlobalKey; final int priority; @@ -947,7 +1214,10 @@ class DownloadQueueItem extends DataClass implements Insertable json, {ValueSerializer? serializer}) { + factory DownloadQueueItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return DownloadQueueItem( id: serializer.fromJson(json['id']), @@ -989,11 +1259,17 @@ class DownloadQueueItem extends DataClass implements Insertable Object.hash(id, mediaGlobalKey, priority, addedAt, downloadSubtitles, downloadArtwork); + int get hashCode => Object.hash( + id, + mediaGlobalKey, + priority, + addedAt, + downloadSubtitles, + downloadArtwork, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -1122,12 +1405,15 @@ class DownloadQueueCompanion extends UpdateCompanion { } } -class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheData> { +class $ApiCacheTable extends ApiCache + with TableInfo<$ApiCacheTable, ApiCacheData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; $ApiCacheTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _cacheKeyMeta = const VerificationMeta('cacheKey'); + static const VerificationMeta _cacheKeyMeta = const VerificationMeta( + 'cacheKey', + ); @override late final GeneratedColumn cacheKey = GeneratedColumn( 'cache_key', @@ -1153,10 +1439,14 @@ class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheDat false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pinned" IN (0, 1))', + ), defaultValue: const Constant(false), ); - static const VerificationMeta _cachedAtMeta = const VerificationMeta('cachedAt'); + static const VerificationMeta _cachedAtMeta = const VerificationMeta( + 'cachedAt', + ); @override late final GeneratedColumn cachedAt = GeneratedColumn( 'cached_at', @@ -1174,24 +1464,39 @@ class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheDat String get actualTableName => $name; static const String $name = 'api_cache'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('cache_key')) { - context.handle(_cacheKeyMeta, cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta)); + context.handle( + _cacheKeyMeta, + cacheKey.isAcceptableOrUnknown(data['cache_key']!, _cacheKeyMeta), + ); } else if (isInserting) { context.missing(_cacheKeyMeta); } if (data.containsKey('data')) { - context.handle(_dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + context.handle( + _dataMeta, + this.data.isAcceptableOrUnknown(data['data']!, _dataMeta), + ); } else if (isInserting) { context.missing(_dataMeta); } if (data.containsKey('pinned')) { - context.handle(_pinnedMeta, pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + context.handle( + _pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta), + ); } if (data.containsKey('cached_at')) { - context.handle(_cachedAtMeta, cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta)); + context.handle( + _cachedAtMeta, + cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta), + ); } return context; } @@ -1202,10 +1507,22 @@ class $ApiCacheTable extends ApiCache with TableInfo<$ApiCacheTable, ApiCacheDat ApiCacheData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ApiCacheData( - cacheKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}cache_key'])!, - data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!, - pinned: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, - cachedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}cached_at'])!, + cacheKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cache_key'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + pinned: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pinned'], + )!, + cachedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}cached_at'], + )!, ); } @@ -1227,7 +1544,12 @@ class ApiCacheData extends DataClass implements Insertable { /// Timestamp for cache invalidation (optional future use) final DateTime cachedAt; - const ApiCacheData({required this.cacheKey, required this.data, required this.pinned, required this.cachedAt}); + const ApiCacheData({ + required this.cacheKey, + required this.data, + required this.pinned, + required this.cachedAt, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -1247,7 +1569,10 @@ class ApiCacheData extends DataClass implements Insertable { ); } - factory ApiCacheData.fromJson(Map json, {ValueSerializer? serializer}) { + factory ApiCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return ApiCacheData( cacheKey: serializer.fromJson(json['cacheKey']), @@ -1267,7 +1592,12 @@ class ApiCacheData extends DataClass implements Insertable { }; } - ApiCacheData copyWith({String? cacheKey, String? data, bool? pinned, DateTime? cachedAt}) => ApiCacheData( + ApiCacheData copyWith({ + String? cacheKey, + String? data, + bool? pinned, + DateTime? cachedAt, + }) => ApiCacheData( cacheKey: cacheKey ?? this.cacheKey, data: data ?? this.data, pinned: pinned ?? this.pinned, @@ -1407,9 +1737,13 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _serverIdMeta = const VerificationMeta( + 'serverId', ); - static const VerificationMeta _serverIdMeta = const VerificationMeta('serverId'); @override late final GeneratedColumn serverId = GeneratedColumn( 'server_id', @@ -1418,7 +1752,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _ratingKeyMeta = const VerificationMeta('ratingKey'); + static const VerificationMeta _ratingKeyMeta = const VerificationMeta( + 'ratingKey', + ); @override late final GeneratedColumn ratingKey = GeneratedColumn( 'rating_key', @@ -1427,7 +1763,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _globalKeyMeta = const VerificationMeta('globalKey'); + static const VerificationMeta _globalKeyMeta = const VerificationMeta( + 'globalKey', + ); @override late final GeneratedColumn globalKey = GeneratedColumn( 'global_key', @@ -1436,7 +1774,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _actionTypeMeta = const VerificationMeta('actionType'); + static const VerificationMeta _actionTypeMeta = const VerificationMeta( + 'actionType', + ); @override late final GeneratedColumn actionType = GeneratedColumn( 'action_type', @@ -1445,7 +1785,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _viewOffsetMeta = const VerificationMeta('viewOffset'); + static const VerificationMeta _viewOffsetMeta = const VerificationMeta( + 'viewOffset', + ); @override late final GeneratedColumn viewOffset = GeneratedColumn( 'view_offset', @@ -1454,7 +1796,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _durationMeta = const VerificationMeta('duration'); + static const VerificationMeta _durationMeta = const VerificationMeta( + 'duration', + ); @override late final GeneratedColumn duration = GeneratedColumn( 'duration', @@ -1463,7 +1807,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta('shouldMarkWatched'); + static const VerificationMeta _shouldMarkWatchedMeta = const VerificationMeta( + 'shouldMarkWatched', + ); @override late final GeneratedColumn shouldMarkWatched = GeneratedColumn( 'should_mark_watched', @@ -1471,10 +1817,14 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("should_mark_watched" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("should_mark_watched" IN (0, 1))', + ), defaultValue: const Constant(false), ); - static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); @override late final GeneratedColumn createdAt = GeneratedColumn( 'created_at', @@ -1483,7 +1833,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); @override late final GeneratedColumn updatedAt = GeneratedColumn( 'updated_at', @@ -1492,7 +1844,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress type: DriftSqlType.int, requiredDuringInsert: true, ); - static const VerificationMeta _syncAttemptsMeta = const VerificationMeta('syncAttempts'); + static const VerificationMeta _syncAttemptsMeta = const VerificationMeta( + 'syncAttempts', + ); @override late final GeneratedColumn syncAttempts = GeneratedColumn( 'sync_attempts', @@ -1502,7 +1856,9 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress requiredDuringInsert: false, defaultValue: const Constant(0), ); - static const VerificationMeta _lastErrorMeta = const VerificationMeta('lastError'); + static const VerificationMeta _lastErrorMeta = const VerificationMeta( + 'lastError', + ); @override late final GeneratedColumn lastError = GeneratedColumn( 'last_error', @@ -1532,59 +1888,98 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress String get actualTableName => $name; static const String $name = 'offline_watch_progress'; @override - VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } if (data.containsKey('server_id')) { - context.handle(_serverIdMeta, serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta)); + context.handle( + _serverIdMeta, + serverId.isAcceptableOrUnknown(data['server_id']!, _serverIdMeta), + ); } else if (isInserting) { context.missing(_serverIdMeta); } if (data.containsKey('rating_key')) { - context.handle(_ratingKeyMeta, ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta)); + context.handle( + _ratingKeyMeta, + ratingKey.isAcceptableOrUnknown(data['rating_key']!, _ratingKeyMeta), + ); } else if (isInserting) { context.missing(_ratingKeyMeta); } if (data.containsKey('global_key')) { - context.handle(_globalKeyMeta, globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta)); + context.handle( + _globalKeyMeta, + globalKey.isAcceptableOrUnknown(data['global_key']!, _globalKeyMeta), + ); } else if (isInserting) { context.missing(_globalKeyMeta); } if (data.containsKey('action_type')) { - context.handle(_actionTypeMeta, actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta)); + context.handle( + _actionTypeMeta, + actionType.isAcceptableOrUnknown(data['action_type']!, _actionTypeMeta), + ); } else if (isInserting) { context.missing(_actionTypeMeta); } if (data.containsKey('view_offset')) { - context.handle(_viewOffsetMeta, viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta)); + context.handle( + _viewOffsetMeta, + viewOffset.isAcceptableOrUnknown(data['view_offset']!, _viewOffsetMeta), + ); } if (data.containsKey('duration')) { - context.handle(_durationMeta, duration.isAcceptableOrUnknown(data['duration']!, _durationMeta)); + context.handle( + _durationMeta, + duration.isAcceptableOrUnknown(data['duration']!, _durationMeta), + ); } if (data.containsKey('should_mark_watched')) { context.handle( _shouldMarkWatchedMeta, - shouldMarkWatched.isAcceptableOrUnknown(data['should_mark_watched']!, _shouldMarkWatchedMeta), + shouldMarkWatched.isAcceptableOrUnknown( + data['should_mark_watched']!, + _shouldMarkWatchedMeta, + ), ); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); } else if (isInserting) { context.missing(_createdAtMeta); } if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); } else if (isInserting) { context.missing(_updatedAtMeta); } if (data.containsKey('sync_attempts')) { - context.handle(_syncAttemptsMeta, syncAttempts.isAcceptableOrUnknown(data['sync_attempts']!, _syncAttemptsMeta)); + context.handle( + _syncAttemptsMeta, + syncAttempts.isAcceptableOrUnknown( + data['sync_attempts']!, + _syncAttemptsMeta, + ), + ); } if (data.containsKey('last_error')) { - context.handle(_lastErrorMeta, lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta)); + context.handle( + _lastErrorMeta, + lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + ); } return context; } @@ -1592,24 +1987,60 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress @override Set get $primaryKey => {id}; @override - OfflineWatchProgressItem map(Map data, {String? tablePrefix}) { + OfflineWatchProgressItem map( + Map data, { + String? tablePrefix, + }) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return OfflineWatchProgressItem( - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, - serverId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}server_id'])!, - ratingKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}rating_key'])!, - globalKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}global_key'])!, - actionType: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}action_type'])!, - viewOffset: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}view_offset']), - duration: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}duration']), + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + serverId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}server_id'], + )!, + ratingKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}rating_key'], + )!, + globalKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}global_key'], + )!, + actionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}action_type'], + )!, + viewOffset: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}view_offset'], + ), + duration: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration'], + ), shouldMarkWatched: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}should_mark_watched'], )!, - createdAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}created_at'])!, - updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}updated_at'])!, - syncAttempts: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}sync_attempts'])!, - lastError: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}last_error']), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at'], + )!, + syncAttempts: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sync_attempts'], + )!, + lastError: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error'], + ), ); } @@ -1619,7 +2050,8 @@ class $OfflineWatchProgressTable extends OfflineWatchProgress } } -class OfflineWatchProgressItem extends DataClass implements Insertable { +class OfflineWatchProgressItem extends DataClass + implements Insertable { /// Auto-incrementing primary key final int id; @@ -1701,17 +2133,26 @@ class OfflineWatchProgressItem extends DataClass implements Insertable json, {ValueSerializer? serializer}) { + factory OfflineWatchProgressItem.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return OfflineWatchProgressItem( id: serializer.fromJson(json['id']), @@ -1774,19 +2215,29 @@ class OfflineWatchProgressItem extends DataClass implements Insertable { +class OfflineWatchProgressCompanion + extends UpdateCompanion { final Value id; final Value serverId; final Value ratingKey; @@ -2014,14 +2466,23 @@ class OfflineWatchProgressCompanion extends UpdateCompanion $AppDatabaseManager(this); - late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable(this); + late final $DownloadedMediaTable downloadedMedia = $DownloadedMediaTable( + this, + ); late final $DownloadQueueTable downloadQueue = $DownloadQueueTable(this); late final $ApiCacheTable apiCache = $ApiCacheTable(this); - late final $OfflineWatchProgressTable offlineWatchProgress = $OfflineWatchProgressTable(this); + late final $OfflineWatchProgressTable offlineWatchProgress = + $OfflineWatchProgressTable(this); @override - Iterable> get allTables => allSchemaEntities.whereType>(); + Iterable> get allTables => + allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [downloadedMedia, downloadQueue, apiCache, offlineWatchProgress]; + List get allSchemaEntities => [ + downloadedMedia, + downloadQueue, + apiCache, + offlineWatchProgress, + ]; } typedef $$DownloadedMediaTableCreateCompanionBuilder = @@ -2042,6 +2503,7 @@ typedef $$DownloadedMediaTableCreateCompanionBuilder = Value downloadedAt, Value errorMessage, Value retryCount, + Value bgTaskId, }); typedef $$DownloadedMediaTableUpdateCompanionBuilder = DownloadedMediaCompanion Function({ @@ -2061,9 +2523,11 @@ typedef $$DownloadedMediaTableUpdateCompanionBuilder = Value downloadedAt, Value errorMessage, Value retryCount, + Value bgTaskId, }); -class $$DownloadedMediaTableFilterComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableFilterComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableFilterComposer({ required super.$db, required super.$table, @@ -2071,54 +2535,94 @@ class $$DownloadedMediaTableFilterComposer extends Composer<_$AppDatabase, $Down super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnFilters(column)); + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get parentRatingKey => - $composableBuilder(column: $table.parentRatingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get grandparentRatingKey => - $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get status => - $composableBuilder(column: $table.status, builder: (column) => ColumnFilters(column)); + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get progress => - $composableBuilder(column: $table.progress, builder: (column) => ColumnFilters(column)); + ColumnFilters get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get totalBytes => - $composableBuilder(column: $table.totalBytes, builder: (column) => ColumnFilters(column)); + ColumnFilters get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadedBytes => - $composableBuilder(column: $table.downloadedBytes, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get videoFilePath => - $composableBuilder(column: $table.videoFilePath, builder: (column) => ColumnFilters(column)); + ColumnFilters get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get thumbPath => - $composableBuilder(column: $table.thumbPath, builder: (column) => ColumnFilters(column)); + ColumnFilters get thumbPath => $composableBuilder( + column: $table.thumbPath, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadedAt => - $composableBuilder(column: $table.downloadedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get errorMessage => - $composableBuilder(column: $table.errorMessage, builder: (column) => ColumnFilters(column)); + ColumnFilters get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get retryCount => - $composableBuilder(column: $table.retryCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get bgTaskId => $composableBuilder( + column: $table.bgTaskId, + builder: (column) => ColumnFilters(column), + ); } -class $$DownloadedMediaTableOrderingComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableOrderingComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableOrderingComposer({ required super.$db, required super.$table, @@ -2126,55 +2630,94 @@ class $$DownloadedMediaTableOrderingComposer extends Composer<_$AppDatabase, $Do super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get type => - $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get parentRatingKey => - $composableBuilder(column: $table.parentRatingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get grandparentRatingKey => - $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get status => - $composableBuilder(column: $table.status, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get progress => - $composableBuilder(column: $table.progress, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get totalBytes => - $composableBuilder(column: $table.totalBytes, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadedBytes => - $composableBuilder(column: $table.downloadedBytes, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get videoFilePath => - $composableBuilder(column: $table.videoFilePath, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get thumbPath => - $composableBuilder(column: $table.thumbPath, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get thumbPath => $composableBuilder( + column: $table.thumbPath, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadedAt => - $composableBuilder(column: $table.downloadedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get errorMessage => - $composableBuilder(column: $table.errorMessage, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get retryCount => - $composableBuilder(column: $table.retryCount, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get bgTaskId => $composableBuilder( + column: $table.bgTaskId, + builder: (column) => ColumnOrderings(column), + ); } -class $$DownloadedMediaTableAnnotationComposer extends Composer<_$AppDatabase, $DownloadedMediaTable> { +class $$DownloadedMediaTableAnnotationComposer + extends Composer<_$AppDatabase, $DownloadedMediaTable> { $$DownloadedMediaTableAnnotationComposer({ required super.$db, required super.$table, @@ -2182,42 +2725,72 @@ class $$DownloadedMediaTableAnnotationComposer extends Composer<_$AppDatabase, $ super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get serverId => $composableBuilder(column: $table.serverId, builder: (column) => column); + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); - GeneratedColumn get ratingKey => $composableBuilder(column: $table.ratingKey, builder: (column) => column); + GeneratedColumn get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => column); - GeneratedColumn get globalKey => $composableBuilder(column: $table.globalKey, builder: (column) => column); + GeneratedColumn get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => column); - GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get parentRatingKey => - $composableBuilder(column: $table.parentRatingKey, builder: (column) => column); + GeneratedColumn get parentRatingKey => $composableBuilder( + column: $table.parentRatingKey, + builder: (column) => column, + ); - GeneratedColumn get grandparentRatingKey => - $composableBuilder(column: $table.grandparentRatingKey, builder: (column) => column); + GeneratedColumn get grandparentRatingKey => $composableBuilder( + column: $table.grandparentRatingKey, + builder: (column) => column, + ); - GeneratedColumn get status => $composableBuilder(column: $table.status, builder: (column) => column); + GeneratedColumn get status => + $composableBuilder(column: $table.status, builder: (column) => column); - GeneratedColumn get progress => $composableBuilder(column: $table.progress, builder: (column) => column); + GeneratedColumn get progress => + $composableBuilder(column: $table.progress, builder: (column) => column); - GeneratedColumn get totalBytes => $composableBuilder(column: $table.totalBytes, builder: (column) => column); + GeneratedColumn get totalBytes => $composableBuilder( + column: $table.totalBytes, + builder: (column) => column, + ); - GeneratedColumn get downloadedBytes => - $composableBuilder(column: $table.downloadedBytes, builder: (column) => column); + GeneratedColumn get downloadedBytes => $composableBuilder( + column: $table.downloadedBytes, + builder: (column) => column, + ); - GeneratedColumn get videoFilePath => - $composableBuilder(column: $table.videoFilePath, builder: (column) => column); + GeneratedColumn get videoFilePath => $composableBuilder( + column: $table.videoFilePath, + builder: (column) => column, + ); - GeneratedColumn get thumbPath => $composableBuilder(column: $table.thumbPath, builder: (column) => column); + GeneratedColumn get thumbPath => + $composableBuilder(column: $table.thumbPath, builder: (column) => column); - GeneratedColumn get downloadedAt => $composableBuilder(column: $table.downloadedAt, builder: (column) => column); + GeneratedColumn get downloadedAt => $composableBuilder( + column: $table.downloadedAt, + builder: (column) => column, + ); - GeneratedColumn get errorMessage => - $composableBuilder(column: $table.errorMessage, builder: (column) => column); + GeneratedColumn get errorMessage => $composableBuilder( + column: $table.errorMessage, + builder: (column) => column, + ); - GeneratedColumn get retryCount => $composableBuilder(column: $table.retryCount, builder: (column) => column); + GeneratedColumn get retryCount => $composableBuilder( + column: $table.retryCount, + builder: (column) => column, + ); + + GeneratedColumn get bgTaskId => + $composableBuilder(column: $table.bgTaskId, builder: (column) => column); } class $$DownloadedMediaTableTableManager @@ -2231,18 +2804,30 @@ class $$DownloadedMediaTableTableManager $$DownloadedMediaTableAnnotationComposer, $$DownloadedMediaTableCreateCompanionBuilder, $$DownloadedMediaTableUpdateCompanionBuilder, - (DownloadedMediaItem, BaseReferences<_$AppDatabase, $DownloadedMediaTable, DownloadedMediaItem>), + ( + DownloadedMediaItem, + BaseReferences< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem + >, + ), DownloadedMediaItem, PrefetchHooks Function() > { - $$DownloadedMediaTableTableManager(_$AppDatabase db, $DownloadedMediaTable table) - : super( + $$DownloadedMediaTableTableManager( + _$AppDatabase db, + $DownloadedMediaTable table, + ) : super( TableManagerState( db: db, table: table, - createFilteringComposer: () => $$DownloadedMediaTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$DownloadedMediaTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$DownloadedMediaTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DownloadedMediaTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DownloadedMediaTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2261,6 +2846,7 @@ class $$DownloadedMediaTableTableManager Value downloadedAt = const Value.absent(), Value errorMessage = const Value.absent(), Value retryCount = const Value.absent(), + Value bgTaskId = const Value.absent(), }) => DownloadedMediaCompanion( id: id, serverId: serverId, @@ -2278,6 +2864,7 @@ class $$DownloadedMediaTableTableManager downloadedAt: downloadedAt, errorMessage: errorMessage, retryCount: retryCount, + bgTaskId: bgTaskId, ), createCompanionCallback: ({ @@ -2297,6 +2884,7 @@ class $$DownloadedMediaTableTableManager Value downloadedAt = const Value.absent(), Value errorMessage = const Value.absent(), Value retryCount = const Value.absent(), + Value bgTaskId = const Value.absent(), }) => DownloadedMediaCompanion.insert( id: id, serverId: serverId, @@ -2314,8 +2902,11 @@ class $$DownloadedMediaTableTableManager downloadedAt: downloadedAt, errorMessage: errorMessage, retryCount: retryCount, + bgTaskId: bgTaskId, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2331,7 +2922,14 @@ typedef $$DownloadedMediaTableProcessedTableManager = $$DownloadedMediaTableAnnotationComposer, $$DownloadedMediaTableCreateCompanionBuilder, $$DownloadedMediaTableUpdateCompanionBuilder, - (DownloadedMediaItem, BaseReferences<_$AppDatabase, $DownloadedMediaTable, DownloadedMediaItem>), + ( + DownloadedMediaItem, + BaseReferences< + _$AppDatabase, + $DownloadedMediaTable, + DownloadedMediaItem + >, + ), DownloadedMediaItem, PrefetchHooks Function() >; @@ -2354,7 +2952,8 @@ typedef $$DownloadQueueTableUpdateCompanionBuilder = Value downloadArtwork, }); -class $$DownloadQueueTableFilterComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableFilterComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableFilterComposer({ required super.$db, required super.$table, @@ -2362,25 +2961,39 @@ class $$DownloadQueueTableFilterComposer extends Composer<_$AppDatabase, $Downlo super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get mediaGlobalKey => - $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get priority => - $composableBuilder(column: $table.priority, builder: (column) => ColumnFilters(column)); + ColumnFilters get priority => $composableBuilder( + column: $table.priority, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get addedAt => - $composableBuilder(column: $table.addedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get addedAt => $composableBuilder( + column: $table.addedAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadSubtitles => - $composableBuilder(column: $table.downloadSubtitles, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get downloadArtwork => - $composableBuilder(column: $table.downloadArtwork, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => ColumnFilters(column), + ); } -class $$DownloadQueueTableOrderingComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableOrderingComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableOrderingComposer({ required super.$db, required super.$table, @@ -2388,25 +3001,39 @@ class $$DownloadQueueTableOrderingComposer extends Composer<_$AppDatabase, $Down super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get mediaGlobalKey => - $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get priority => - $composableBuilder(column: $table.priority, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get priority => $composableBuilder( + column: $table.priority, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get addedAt => - $composableBuilder(column: $table.addedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get addedAt => $composableBuilder( + column: $table.addedAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadSubtitles => - $composableBuilder(column: $table.downloadSubtitles, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get downloadArtwork => - $composableBuilder(column: $table.downloadArtwork, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => ColumnOrderings(column), + ); } -class $$DownloadQueueTableAnnotationComposer extends Composer<_$AppDatabase, $DownloadQueueTable> { +class $$DownloadQueueTableAnnotationComposer + extends Composer<_$AppDatabase, $DownloadQueueTable> { $$DownloadQueueTableAnnotationComposer({ required super.$db, required super.$table, @@ -2414,20 +3041,29 @@ class $$DownloadQueueTableAnnotationComposer extends Composer<_$AppDatabase, $Do super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get mediaGlobalKey => - $composableBuilder(column: $table.mediaGlobalKey, builder: (column) => column); + GeneratedColumn get mediaGlobalKey => $composableBuilder( + column: $table.mediaGlobalKey, + builder: (column) => column, + ); - GeneratedColumn get priority => $composableBuilder(column: $table.priority, builder: (column) => column); + GeneratedColumn get priority => + $composableBuilder(column: $table.priority, builder: (column) => column); - GeneratedColumn get addedAt => $composableBuilder(column: $table.addedAt, builder: (column) => column); + GeneratedColumn get addedAt => + $composableBuilder(column: $table.addedAt, builder: (column) => column); - GeneratedColumn get downloadSubtitles => - $composableBuilder(column: $table.downloadSubtitles, builder: (column) => column); + GeneratedColumn get downloadSubtitles => $composableBuilder( + column: $table.downloadSubtitles, + builder: (column) => column, + ); - GeneratedColumn get downloadArtwork => - $composableBuilder(column: $table.downloadArtwork, builder: (column) => column); + GeneratedColumn get downloadArtwork => $composableBuilder( + column: $table.downloadArtwork, + builder: (column) => column, + ); } class $$DownloadQueueTableTableManager @@ -2441,7 +3077,14 @@ class $$DownloadQueueTableTableManager $$DownloadQueueTableAnnotationComposer, $$DownloadQueueTableCreateCompanionBuilder, $$DownloadQueueTableUpdateCompanionBuilder, - (DownloadQueueItem, BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>), + ( + DownloadQueueItem, + BaseReferences< + _$AppDatabase, + $DownloadQueueTable, + DownloadQueueItem + >, + ), DownloadQueueItem, PrefetchHooks Function() > { @@ -2450,9 +3093,12 @@ class $$DownloadQueueTableTableManager TableManagerState( db: db, table: table, - createFilteringComposer: () => $$DownloadQueueTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$DownloadQueueTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$DownloadQueueTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$DownloadQueueTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DownloadQueueTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DownloadQueueTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2485,7 +3131,9 @@ class $$DownloadQueueTableTableManager downloadSubtitles: downloadSubtitles, downloadArtwork: downloadArtwork, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2501,7 +3149,10 @@ typedef $$DownloadQueueTableProcessedTableManager = $$DownloadQueueTableAnnotationComposer, $$DownloadQueueTableCreateCompanionBuilder, $$DownloadQueueTableUpdateCompanionBuilder, - (DownloadQueueItem, BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>), + ( + DownloadQueueItem, + BaseReferences<_$AppDatabase, $DownloadQueueTable, DownloadQueueItem>, + ), DownloadQueueItem, PrefetchHooks Function() >; @@ -2522,7 +3173,8 @@ typedef $$ApiCacheTableUpdateCompanionBuilder = Value rowid, }); -class $$ApiCacheTableFilterComposer extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableFilterComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableFilterComposer({ required super.$db, required super.$table, @@ -2530,19 +3182,29 @@ class $$ApiCacheTableFilterComposer extends Composer<_$AppDatabase, $ApiCacheTab super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get cacheKey => - $composableBuilder(column: $table.cacheKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get cacheKey => $composableBuilder( + column: $table.cacheKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get data => $composableBuilder(column: $table.data, builder: (column) => ColumnFilters(column)); + ColumnFilters get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get cachedAt => - $composableBuilder(column: $table.cachedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get cachedAt => $composableBuilder( + column: $table.cachedAt, + builder: (column) => ColumnFilters(column), + ); } -class $$ApiCacheTableOrderingComposer extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableOrderingComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableOrderingComposer({ required super.$db, required super.$table, @@ -2550,20 +3212,29 @@ class $$ApiCacheTableOrderingComposer extends Composer<_$AppDatabase, $ApiCacheT super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get cacheKey => - $composableBuilder(column: $table.cacheKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get cacheKey => $composableBuilder( + column: $table.cacheKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get data => - $composableBuilder(column: $table.data, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get cachedAt => - $composableBuilder(column: $table.cachedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get cachedAt => $composableBuilder( + column: $table.cachedAt, + builder: (column) => ColumnOrderings(column), + ); } -class $$ApiCacheTableAnnotationComposer extends Composer<_$AppDatabase, $ApiCacheTable> { +class $$ApiCacheTableAnnotationComposer + extends Composer<_$AppDatabase, $ApiCacheTable> { $$ApiCacheTableAnnotationComposer({ required super.$db, required super.$table, @@ -2571,13 +3242,17 @@ class $$ApiCacheTableAnnotationComposer extends Composer<_$AppDatabase, $ApiCach super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get cacheKey => $composableBuilder(column: $table.cacheKey, builder: (column) => column); + GeneratedColumn get cacheKey => + $composableBuilder(column: $table.cacheKey, builder: (column) => column); - GeneratedColumn get data => $composableBuilder(column: $table.data, builder: (column) => column); + GeneratedColumn get data => + $composableBuilder(column: $table.data, builder: (column) => column); - GeneratedColumn get pinned => $composableBuilder(column: $table.pinned, builder: (column) => column); + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); - GeneratedColumn get cachedAt => $composableBuilder(column: $table.cachedAt, builder: (column) => column); + GeneratedColumn get cachedAt => + $composableBuilder(column: $table.cachedAt, builder: (column) => column); } class $$ApiCacheTableTableManager @@ -2591,7 +3266,10 @@ class $$ApiCacheTableTableManager $$ApiCacheTableAnnotationComposer, $$ApiCacheTableCreateCompanionBuilder, $$ApiCacheTableUpdateCompanionBuilder, - (ApiCacheData, BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>), + ( + ApiCacheData, + BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, + ), ApiCacheData, PrefetchHooks Function() > { @@ -2600,9 +3278,12 @@ class $$ApiCacheTableTableManager TableManagerState( db: db, table: table, - createFilteringComposer: () => $$ApiCacheTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$ApiCacheTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$ApiCacheTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$ApiCacheTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ApiCacheTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ApiCacheTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value cacheKey = const Value.absent(), @@ -2610,7 +3291,13 @@ class $$ApiCacheTableTableManager Value pinned = const Value.absent(), Value cachedAt = const Value.absent(), Value rowid = const Value.absent(), - }) => ApiCacheCompanion(cacheKey: cacheKey, data: data, pinned: pinned, cachedAt: cachedAt, rowid: rowid), + }) => ApiCacheCompanion( + cacheKey: cacheKey, + data: data, + pinned: pinned, + cachedAt: cachedAt, + rowid: rowid, + ), createCompanionCallback: ({ required String cacheKey, @@ -2625,7 +3312,9 @@ class $$ApiCacheTableTableManager cachedAt: cachedAt, rowid: rowid, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2641,7 +3330,10 @@ typedef $$ApiCacheTableProcessedTableManager = $$ApiCacheTableAnnotationComposer, $$ApiCacheTableCreateCompanionBuilder, $$ApiCacheTableUpdateCompanionBuilder, - (ApiCacheData, BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>), + ( + ApiCacheData, + BaseReferences<_$AppDatabase, $ApiCacheTable, ApiCacheData>, + ), ApiCacheData, PrefetchHooks Function() >; @@ -2676,7 +3368,8 @@ typedef $$OfflineWatchProgressTableUpdateCompanionBuilder = Value lastError, }); -class $$OfflineWatchProgressTableFilterComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableFilterComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableFilterComposer({ required super.$db, required super.$table, @@ -2684,43 +3377,69 @@ class $$OfflineWatchProgressTableFilterComposer extends Composer<_$AppDatabase, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnFilters(column)); + ColumnFilters get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get actionType => - $composableBuilder(column: $table.actionType, builder: (column) => ColumnFilters(column)); + ColumnFilters get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get viewOffset => - $composableBuilder(column: $table.viewOffset, builder: (column) => ColumnFilters(column)); + ColumnFilters get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get duration => - $composableBuilder(column: $table.duration, builder: (column) => ColumnFilters(column)); + ColumnFilters get duration => $composableBuilder( + column: $table.duration, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get shouldMarkWatched => - $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => ColumnFilters(column)); + ColumnFilters get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get syncAttempts => - $composableBuilder(column: $table.syncAttempts, builder: (column) => ColumnFilters(column)); + ColumnFilters get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => ColumnFilters(column), + ); - ColumnFilters get lastError => - $composableBuilder(column: $table.lastError, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnFilters(column), + ); } -class $$OfflineWatchProgressTableOrderingComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableOrderingComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableOrderingComposer({ required super.$db, required super.$table, @@ -2728,43 +3447,69 @@ class $$OfflineWatchProgressTableOrderingComposer extends Composer<_$AppDatabase super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get serverId => - $composableBuilder(column: $table.serverId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get serverId => $composableBuilder( + column: $table.serverId, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get ratingKey => - $composableBuilder(column: $table.ratingKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ratingKey => $composableBuilder( + column: $table.ratingKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get globalKey => - $composableBuilder(column: $table.globalKey, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get globalKey => $composableBuilder( + column: $table.globalKey, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get actionType => - $composableBuilder(column: $table.actionType, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get viewOffset => - $composableBuilder(column: $table.viewOffset, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get duration => - $composableBuilder(column: $table.duration, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get duration => $composableBuilder( + column: $table.duration, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get shouldMarkWatched => - $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get syncAttempts => - $composableBuilder(column: $table.syncAttempts, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => ColumnOrderings(column), + ); - ColumnOrderings get lastError => - $composableBuilder(column: $table.lastError, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastError => $composableBuilder( + column: $table.lastError, + builder: (column) => ColumnOrderings(column), + ); } -class $$OfflineWatchProgressTableAnnotationComposer extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { +class $$OfflineWatchProgressTableAnnotationComposer + extends Composer<_$AppDatabase, $OfflineWatchProgressTable> { $$OfflineWatchProgressTableAnnotationComposer({ required super.$db, required super.$table, @@ -2772,30 +3517,49 @@ class $$OfflineWatchProgressTableAnnotationComposer extends Composer<_$AppDataba super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get serverId => $composableBuilder(column: $table.serverId, builder: (column) => column); + GeneratedColumn get serverId => + $composableBuilder(column: $table.serverId, builder: (column) => column); - GeneratedColumn get ratingKey => $composableBuilder(column: $table.ratingKey, builder: (column) => column); + GeneratedColumn get ratingKey => + $composableBuilder(column: $table.ratingKey, builder: (column) => column); - GeneratedColumn get globalKey => $composableBuilder(column: $table.globalKey, builder: (column) => column); + GeneratedColumn get globalKey => + $composableBuilder(column: $table.globalKey, builder: (column) => column); - GeneratedColumn get actionType => $composableBuilder(column: $table.actionType, builder: (column) => column); + GeneratedColumn get actionType => $composableBuilder( + column: $table.actionType, + builder: (column) => column, + ); - GeneratedColumn get viewOffset => $composableBuilder(column: $table.viewOffset, builder: (column) => column); + GeneratedColumn get viewOffset => $composableBuilder( + column: $table.viewOffset, + builder: (column) => column, + ); - GeneratedColumn get duration => $composableBuilder(column: $table.duration, builder: (column) => column); + GeneratedColumn get duration => + $composableBuilder(column: $table.duration, builder: (column) => column); - GeneratedColumn get shouldMarkWatched => - $composableBuilder(column: $table.shouldMarkWatched, builder: (column) => column); + GeneratedColumn get shouldMarkWatched => $composableBuilder( + column: $table.shouldMarkWatched, + builder: (column) => column, + ); - GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumn get syncAttempts => $composableBuilder(column: $table.syncAttempts, builder: (column) => column); + GeneratedColumn get syncAttempts => $composableBuilder( + column: $table.syncAttempts, + builder: (column) => column, + ); - GeneratedColumn get lastError => $composableBuilder(column: $table.lastError, builder: (column) => column); + GeneratedColumn get lastError => + $composableBuilder(column: $table.lastError, builder: (column) => column); } class $$OfflineWatchProgressTableTableManager @@ -2811,19 +3575,34 @@ class $$OfflineWatchProgressTableTableManager $$OfflineWatchProgressTableUpdateCompanionBuilder, ( OfflineWatchProgressItem, - BaseReferences<_$AppDatabase, $OfflineWatchProgressTable, OfflineWatchProgressItem>, + BaseReferences< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem + >, ), OfflineWatchProgressItem, PrefetchHooks Function() > { - $$OfflineWatchProgressTableTableManager(_$AppDatabase db, $OfflineWatchProgressTable table) - : super( + $$OfflineWatchProgressTableTableManager( + _$AppDatabase db, + $OfflineWatchProgressTable table, + ) : super( TableManagerState( db: db, table: table, - createFilteringComposer: () => $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => $$OfflineWatchProgressTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => $$OfflineWatchProgressTableAnnotationComposer($db: db, $table: table), + createFilteringComposer: () => + $$OfflineWatchProgressTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$OfflineWatchProgressTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$OfflineWatchProgressTableAnnotationComposer( + $db: db, + $table: table, + ), updateCompanionCallback: ({ Value id = const Value.absent(), @@ -2880,7 +3659,9 @@ class $$OfflineWatchProgressTableTableManager syncAttempts: syncAttempts, lastError: lastError, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, ), ); @@ -2896,7 +3677,14 @@ typedef $$OfflineWatchProgressTableProcessedTableManager = $$OfflineWatchProgressTableAnnotationComposer, $$OfflineWatchProgressTableCreateCompanionBuilder, $$OfflineWatchProgressTableUpdateCompanionBuilder, - (OfflineWatchProgressItem, BaseReferences<_$AppDatabase, $OfflineWatchProgressTable, OfflineWatchProgressItem>), + ( + OfflineWatchProgressItem, + BaseReferences< + _$AppDatabase, + $OfflineWatchProgressTable, + OfflineWatchProgressItem + >, + ), OfflineWatchProgressItem, PrefetchHooks Function() >; @@ -2906,8 +3694,10 @@ class $AppDatabaseManager { $AppDatabaseManager(this._db); $$DownloadedMediaTableTableManager get downloadedMedia => $$DownloadedMediaTableTableManager(_db, _db.downloadedMedia); - $$DownloadQueueTableTableManager get downloadQueue => $$DownloadQueueTableTableManager(_db, _db.downloadQueue); - $$ApiCacheTableTableManager get apiCache => $$ApiCacheTableTableManager(_db, _db.apiCache); + $$DownloadQueueTableTableManager get downloadQueue => + $$DownloadQueueTableTableManager(_db, _db.downloadQueue); + $$ApiCacheTableTableManager get apiCache => + $$ApiCacheTableTableManager(_db, _db.apiCache); $$OfflineWatchProgressTableTableManager get offlineWatchProgress => $$OfflineWatchProgressTableTableManager(_db, _db.offlineWatchProgress); } diff --git a/lib/database/tables.dart b/lib/database/tables.dart index 809c183f..17b0ca3b 100644 --- a/lib/database/tables.dart +++ b/lib/database/tables.dart @@ -47,6 +47,7 @@ class DownloadedMedia extends Table { IntColumn get downloadedAt => integer().nullable()(); TextColumn get errorMessage => text().nullable()(); IntColumn get retryCount => integer().withDefault(const Constant(0))(); + TextColumn get bgTaskId => text().nullable()(); } /// Queue for offline watch progress and manual watch actions. diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 3934a6fa..e5b89fde 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; @@ -7,6 +8,7 @@ import 'package:path/path.dart' as path; import 'package:plezy/utils/content_utils.dart'; import '../database/app_database.dart'; import 'settings_service.dart'; +import 'saf_storage_service.dart'; import '../models/download_models.dart'; import '../models/plex_metadata.dart'; import '../models/plex_media_info.dart'; @@ -19,8 +21,6 @@ import '../utils/global_key_utils.dart'; import '../utils/plex_cache_parser.dart'; /// Extension methods on AppDatabase for download operations -/// Result of a download attempt for queue processing decisions -enum _DownloadResult { success, networkError, permanentFailure } extension DownloadDatabaseOperations on AppDatabase { /// Insert a new download into the database @@ -75,7 +75,7 @@ extension DownloadDatabaseOperations on AppDatabase { ).join([innerJoin(downloadedMedia, downloadedMedia.globalKey.equalsExp(downloadQueue.mediaGlobalKey))]); query - ..where(downloadedMedia.status.equals(DownloadStatus.paused.index).not()) + ..where(downloadedMedia.status.equals(DownloadStatus.queued.index)) ..orderBy([ OrderingTerm(expression: downloadQueue.priority, mode: OrderingMode.desc), OrderingTerm(expression: downloadQueue.addedAt), @@ -164,6 +164,43 @@ extension DownloadDatabaseOperations on AppDatabase { Future> getEpisodesByShow(String showKey) { return (select(downloadedMedia)..where((t) => t.grandparentRatingKey.equals(showKey))).get(); } + + /// Update the background_downloader task ID for a download + Future updateBgTaskId(String globalKey, String? taskId) async { + await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( + DownloadedMediaCompanion(bgTaskId: Value(taskId)), + ); + } + + /// Get the background_downloader task ID for a download + Future getBgTaskId(String globalKey) async { + final item = await getDownloadedMedia(globalKey); + return item?.bgTaskId; + } +} + +/// Context for a download that's been enqueued with background_downloader. +/// Carries metadata needed between enqueue and completion callback. +class _DownloadContext { + final PlexMetadata metadata; + final DownloadQueueItem queueItem; + final String filePath; // Absolute path (normal) or SAF dir URI (SAF mode) + final String extension; + final PlexClient client; + final int? showYear; + final bool isSafMode; + final PlexMediaInfo? mediaInfo; + + _DownloadContext({ + required this.metadata, + required this.queueItem, + required this.filePath, + required this.extension, + required this.client, + this.showYear, + this.isSafMode = false, + this.mediaInfo, + }); } class DownloadManagerService { @@ -180,56 +217,24 @@ class DownloadManagerService { final _deletionProgressController = StreamController.broadcast(); Stream get deletionProgressStream => _deletionProgressController.stream; - // Active downloads with cancel tokens - final Map _activeDownloads = {}; + // Context for downloads enqueued in this session + final Map _pendingDownloadContext = {}; - // Flag to prevent multiple queue processing - bool _isProcessingQueue = false; + // Items recovered with video complete but supplementary downloads missing + final Set _pendingSupplementaryDownloads = {}; - // Connectivity listener for auto-resume - StreamSubscription>? _connectivitySubscription; - - // Cached client for auto-resume + // Cached client for recovery and queue processing PlexClient? _lastClient; - /// Check if downloads should be blocked due to cellular-only setting - Future _shouldBlockDownload() async { - return shouldBlockDownloadOnCellular(); - } + // background_downloader state + bool _fileDownloaderInitialized = false; + static const _downloadGroup = 'video_downloads'; - /// Determine if an error is a retriable network error vs a permanent failure - bool _isRetriableNetworkError(Object e) { - if (e is DioException) { - switch (e.type) { - case DioExceptionType.connectionTimeout: - case DioExceptionType.receiveTimeout: - case DioExceptionType.sendTimeout: - case DioExceptionType.connectionError: - return true; - case DioExceptionType.unknown: - // Mid-stream disconnects surface as DioExceptionType.unknown - // wrapping SocketException, HttpException, or similar - final inner = e.error; - if (inner is SocketException) return true; - if (inner is HttpException) return true; - final msg = inner?.toString() ?? ''; - if (msg.contains('Connection closed') || msg.contains('Connection reset')) { - return true; - } - return false; - case DioExceptionType.badResponse: - // 5xx server errors are transient and worth retrying - final statusCode = e.response?.statusCode; - if (statusCode != null && statusCode >= 500) return true; - return false; - default: - return false; - } - } - if (e is SocketException) return true; - if (e is HttpException) return true; - return false; - } + // Keys currently being paused — prevents holding queue from promoting them + final Set _pausingKeys = {}; + + // Prevents concurrent _processQueue calls + bool _isProcessingQueue = false; /// Public method to check if downloads should be blocked due to cellular-only setting /// Can be used by DownloadProvider to show user-friendly error @@ -253,22 +258,77 @@ class DownloadManagerService { _storageService = storageService, _dio = dio ?? Dio(); - /// Recover downloads that were in "downloading" state when the app was killed. - /// Transitions them to "queued" so _processQueue picks them up with resume - /// support via the existing .part file on disk. + /// Initialize background_downloader with callbacks, notifications, and concurrency config. + Future _initializeFileDownloader() async { + if (_fileDownloaderInitialized) return; + + FileDownloader() + .registerCallbacks( + group: _downloadGroup, + taskStatusCallback: _onTaskStatusChanged, + taskProgressCallback: _onTaskProgress, + ) + .configureNotificationForGroup( + _downloadGroup, + running: const TaskNotification('{displayName}', 'Downloading...'), + complete: const TaskNotification('{displayName}', 'Download complete'), + error: const TaskNotification('{displayName}', 'Download failed'), + paused: const TaskNotification('{displayName}', 'Download paused'), + progressBar: true, + ); + + // Configure native holding queue: max 1 concurrent (Plex server limitation) + await FileDownloader().configure(globalConfig: (Config.holdingQueue, (1, 1, 1))); + + // Track tasks for persistence across app restarts + await FileDownloader().trackTasks(); + + _fileDownloaderInitialized = true; + } + + /// Recover downloads that were interrupted when the app was killed. + /// Uses background_downloader's rescheduleKilledTasks for native recovery, + /// then scans drift for orphaned items. Future recoverInterruptedDownloads() async { try { + await _initializeFileDownloader(); + + // Let background_downloader re-enqueue tasks killed by the OS + final (rescheduled, _) = await FileDownloader().rescheduleKilledTasks(); + if (rescheduled.isNotEmpty) { + appLogger.i('Rescheduled ${rescheduled.length} killed download task(s)'); + } + + // Scan drift for orphaned items stuck in 'downloading' final allDownloads = await _database.select(_database.downloadedMedia).get(); - final interrupted = allDownloads.where((item) => item.status == DownloadStatus.downloading.index).toList(); - if (interrupted.isEmpty) return; + for (final item in allDownloads) { + if (item.status == DownloadStatus.downloading.index) { + // Video already downloaded but post-processing didn't complete + if (item.videoFilePath != null) { + appLogger.i('Download ${item.globalKey} has video but incomplete post-processing, completing'); + await _database.updateDownloadStatus(item.globalKey, DownloadStatus.completed.index); + await _database.removeFromQueue(item.globalKey); + _emitProgress(item.globalKey, DownloadStatus.completed, 100); + _pendingSupplementaryDownloads.add(item.globalKey); + continue; + } - appLogger.i('Recovering ${interrupted.length} interrupted download(s)'); - for (final item in interrupted) { - await _database.updateDownloadStatus(item.globalKey, DownloadStatus.queued.index); - // Re-add to queue so _processQueue picks it up - await _database.addToQueue(mediaGlobalKey: item.globalKey); - appLogger.d('Re-queued interrupted download: ${item.globalKey}'); + // Check if background_downloader still has this task + Task? bgTask; + if (item.bgTaskId != null) { + bgTask = await FileDownloader().taskForId(item.bgTaskId!); + } + + if (bgTask == null) { + // No active bg task — orphan, re-queue it + appLogger.i('Re-queuing orphaned download: ${item.globalKey}'); + await _database.updateDownloadStatus(item.globalKey, DownloadStatus.queued.index); + await _database.updateBgTaskId(item.globalKey, null); + await _database.addToQueue(mediaGlobalKey: item.globalKey); + } + // If bgTask exists, background_downloader is still handling it + } } } catch (e) { appLogger.e('Failed to recover interrupted downloads', error: e); @@ -278,14 +338,66 @@ class DownloadManagerService { /// Resume queued downloads that have no active processing. /// Call after a PlexClient becomes available (e.g. after server connect on launch). void resumeQueuedDownloads(PlexClient client) { + _lastClient = client; + + // Attempt deferred supplementary downloads for recovered items + _processPendingSupplementaryDownloads(client); + _database.getNextQueueItem().then((item) { - if (item != null && !_isProcessingQueue) { + if (item != null) { appLogger.i('Resuming queued downloads after app restart'); _processQueue(client); } }); } + /// Attempt supplementary downloads (artwork, subtitles) for items that were + /// recovered with a completed video but missed post-processing. + Future _processPendingSupplementaryDownloads(PlexClient client) async { + if (_pendingSupplementaryDownloads.isEmpty) return; + + final keys = Set.from(_pendingSupplementaryDownloads); + _pendingSupplementaryDownloads.clear(); + + for (final globalKey in keys) { + try { + final metadata = await _resolveMetadata(globalKey); + if (metadata == null) { + appLogger.w('No metadata for deferred supplementary download: $globalKey'); + continue; + } + + // Look up show year for episodes + int? showYear; + if (metadata.type == 'episode' && metadata.grandparentRatingKey != null) { + final parsed = parseGlobalKey(globalKey); + if (parsed != null) { + final showCached = await _apiCache.get(parsed.serverId, '/library/metadata/${metadata.grandparentRatingKey}'); + final showJson = PlexCacheParser.extractFirstMetadata(showCached); + if (showJson != null) showYear = PlexMetadata.fromJson(showJson).year; + } + } + + await _downloadArtwork(globalKey, metadata, client, showYear: showYear); + await _downloadChapterThumbnails(metadata.serverId!, metadata.ratingKey, client); + + // Attempt subtitles + try { + final playbackData = await client.getVideoPlaybackData(metadata.ratingKey); + if (playbackData.mediaInfo != null) { + await _downloadSubtitles(globalKey, metadata, playbackData.mediaInfo!, client, showYear: showYear); + } + } catch (e) { + appLogger.w('Could not fetch playback data for deferred subtitles: $globalKey', error: e); + } + + appLogger.i('Deferred supplementary downloads completed for $globalKey'); + } catch (e) { + appLogger.w('Deferred supplementary downloads failed for $globalKey', error: e); + } + } + } + /// Delete a file if it exists and log the deletion /// Returns true if file was deleted, false otherwise Future _deleteFileIfExists(File file, String description) async { @@ -344,446 +456,384 @@ class DownloadManagerService { _processQueue(client); } - /// Start processing the download queue - processes one item at a time + /// Process the download queue — prepares and enqueues items with background_downloader. + /// Non-blocking: returns after all queued items are enqueued (downloads run natively). Future _processQueue(PlexClient client) async { - if (_isProcessingQueue) { - appLogger.d('Queue processing already in progress'); - return; - } - + if (_isProcessingQueue) return; _isProcessingQueue = true; - _lastClient = client; // Cache for auto-resume - _setupConnectivityListener(); // Setup listener for auto-resume + _lastClient = client; try { + await _initializeFileDownloader(); + while (true) { - // Check if we should pause due to cellular - if (await _shouldBlockDownload()) { - appLogger.i('Pausing downloads - on cellular data with WiFi-only enabled'); - break; - } - - // Get next item from queue final nextItem = await _database.getNextQueueItem(); - if (nextItem == null) { - appLogger.d('No more items in queue'); - break; - } + if (nextItem == null) break; - final result = await _startDownload(nextItem.mediaGlobalKey, client, nextItem); - - // If download failed due to network, wait before retrying to avoid rapid loops - if (result == _DownloadResult.networkError) { - appLogger.i('Network error - waiting 5 seconds before processing next item'); - await Future.delayed(const Duration(seconds: 5)); - } + await _prepareAndEnqueueDownload(nextItem.mediaGlobalKey, client, nextItem); } } finally { _isProcessingQueue = false; } } - /// Setup connectivity listener to auto-resume downloads when WiFi becomes available - void _setupConnectivityListener() { - _connectivitySubscription?.cancel(); - _connectivitySubscription = Connectivity().onConnectivityChanged.listen((results) async { - // If WiFi becomes available, try to resume queue - if (results.contains(ConnectivityResult.wifi) || results.contains(ConnectivityResult.ethernet)) { - final hasQueuedItems = await _database.getNextQueueItem() != null; - if (hasQueuedItems && !_isProcessingQueue && _lastClient != null) { - appLogger.i('WiFi available - resuming downloads'); - _processQueue(_lastClient!); - } - } - }); - } - - /// Start downloading a specific item - /// Returns the result of the download attempt for queue processing decisions - Future<_DownloadResult> _startDownload(String globalKey, PlexClient client, DownloadQueueItem queueItem) async { - // Hoisted so catch blocks can clean up .part files - String? downloadFilePath; + /// Resolve metadata, video URL, and file path, then enqueue a background download task. + Future _prepareAndEnqueueDownload(String globalKey, PlexClient client, DownloadQueueItem queueItem) async { try { - appLogger.i('Starting download for $globalKey'); - - // Update status to downloading + appLogger.i('Preparing download for $globalKey'); await _transitionStatus(globalKey, DownloadStatus.downloading); - appLogger.d('Status updated to downloading'); - // Parse globalKey to get serverId and ratingKey final parsed = parseGlobalKey(globalKey); - if (parsed == null) { - throw Exception('Invalid globalKey format: $globalKey'); - } + if (parsed == null) throw Exception('Invalid globalKey: $globalKey'); final serverId = parsed.serverId; final ratingKey = parsed.ratingKey; - // Get metadata from cache final metadata = await _apiCache.getMetadata(serverId, ratingKey); - if (metadata == null) { - throw Exception('Metadata not found in cache for $globalKey'); - } + if (metadata == null) throw Exception('Metadata not found in cache for $globalKey'); - // Get video playback data (includes URL, streams, markers, etc.) - // This also caches the metadata with chapters/markers for offline use final playbackData = await client.getVideoPlaybackData(metadata.ratingKey); - if (playbackData.videoUrl == null) { - throw Exception('Could not get video URL'); - } + if (playbackData.videoUrl == null) throw Exception('Could not get video URL'); - // Determine file extension from URL or default to mp4 - final extension = _getExtensionFromUrl(playbackData.videoUrl!) ?? 'mp4'; + final ext = _getExtensionFromUrl(playbackData.videoUrl!) ?? 'mp4'; - final metadataWithServer = metadata; - - // For episodes, look up the show's year from cached show metadata + // Look up show year for episodes int? showYear; - if (metadataWithServer.type == 'episode' && metadataWithServer.grandparentRatingKey != null) { - final showCached = await _apiCache.get( - serverId, - '/library/metadata/${metadataWithServer.grandparentRatingKey}', - ); + if (metadata.type == 'episode' && metadata.grandparentRatingKey != null) { + final showCached = await _apiCache.get(serverId, '/library/metadata/${metadata.grandparentRatingKey}'); final showJson = PlexCacheParser.extractFirstMetadata(showCached); - if (showJson != null) { - final showMetadata = PlexMetadata.fromJson(showJson); - showYear = showMetadata.year; - } + if (showJson != null) showYear = PlexMetadata.fromJson(showJson).year; } - // Create cancel token - final cancelToken = CancelToken(); - _activeDownloads[globalKey] = cancelToken; + // Build display name for notifications + final displayName = + metadata.type == 'episode' ? '${metadata.grandparentTitle ?? metadata.title} - ${metadata.title}' : metadata.title; - appLogger.d('Starting video download for $globalKey'); - - // Determine download path and handle SAF mode - final String storedPath; + // Get WiFi-only setting for native enforcement + final settings = await SettingsService.getInstance(); + final requiresWiFi = settings.getDownloadOnWifiOnly(); if (_storageService.isUsingSaf) { - // SAF mode: download to temp cache first, then copy to SAF - final tempFileName = '${globalKey.replaceAll(':', '_')}.$extension'; - downloadFilePath = await _storageService.getTempDownloadPath(tempFileName); - - // Download to temp path - await _downloadFile( - url: playbackData.videoUrl!, - filePath: downloadFilePath, - globalKey: globalKey, - cancelToken: cancelToken, - ); - - appLogger.d('Video downloaded to temp, copying to SAF for $globalKey'); - - // Copy to SAF + // SAF mode: use UriDownloadTask (writes directly to content:// URI, no pause/resume) final List pathComponents; final String safFileName; - if (metadataWithServer.type == 'movie') { - pathComponents = _storageService.getMovieSafPathComponents(metadataWithServer); - safFileName = _storageService.getMovieSafFileName(metadataWithServer, extension); - } else if (metadataWithServer.type == 'episode') { - pathComponents = _storageService.getEpisodeSafPathComponents(metadataWithServer, showYear: showYear); - safFileName = _storageService.getEpisodeSafFileName(metadataWithServer, extension); + if (metadata.type == 'movie') { + pathComponents = _storageService.getMovieSafPathComponents(metadata); + safFileName = _storageService.getMovieSafFileName(metadata, ext); + } else if (metadata.type == 'episode') { + pathComponents = _storageService.getEpisodeSafPathComponents(metadata, showYear: showYear); + safFileName = _storageService.getEpisodeSafFileName(metadata, ext); } else { - pathComponents = [serverId, metadataWithServer.ratingKey]; - safFileName = 'video.$extension'; + pathComponents = [serverId, metadata.ratingKey]; + safFileName = 'video.$ext'; } - final safUri = await _storageService.copyToSaf( - downloadFilePath, + final safDirUri = await SafStorageService.instance.createNestedDirectories( + _storageService.safBaseUri!, pathComponents, - safFileName, - _storageService.getMimeType(extension), ); + if (safDirUri == null) throw Exception('Failed to create SAF directory'); - if (safUri == null) { - throw Exception('Failed to copy video to SAF storage'); - } - - storedPath = safUri; - appLogger.d('Video copied to SAF: $safUri'); - } else { - // Normal mode: download directly to final path - if (metadataWithServer.type == 'movie') { - downloadFilePath = await _storageService.getMovieVideoPath(metadataWithServer, extension); - } else if (metadataWithServer.type == 'episode') { - downloadFilePath = await _storageService.getEpisodeVideoPath( - metadataWithServer, - extension, - showYear: showYear, - ); - } else { - downloadFilePath = await _storageService.getVideoFilePath(serverId, metadataWithServer.ratingKey, extension); - } - - await _downloadFile( + final task = UriDownloadTask( url: playbackData.videoUrl!, - filePath: downloadFilePath, - globalKey: globalKey, - cancelToken: cancelToken, + filename: safFileName, + directoryUri: Uri.parse(safDirUri), + group: _downloadGroup, + updates: Updates.statusAndProgress, + requiresWiFi: requiresWiFi, + retries: 3, + metaData: globalKey, + displayName: displayName, ); - // Store relative path (survives iOS container UUID changes) - storedPath = await _storageService.toRelativePath(downloadFilePath); - } + _pendingDownloadContext[globalKey] = _DownloadContext( + metadata: metadata, + queueItem: queueItem, + filePath: safDirUri, + extension: ext, + client: client, + showYear: showYear, + isSafMode: true, + mediaInfo: playbackData.mediaInfo, + ); - appLogger.d('Video download completed for $globalKey'); - - // Update database with stored path (SAF URI or relative path) - await _database.updateVideoFilePath(globalKey, storedPath); - - // Download artwork if enabled (only episode-specific artwork, not show/season) - // Use the passed queueItem's settings (not getNextQueueItem which would return the NEXT item) - if (queueItem.downloadArtwork) { - await _downloadArtwork(globalKey, metadataWithServer, client, showYear: showYear); - - // Download chapter thumbnails - await _downloadChapterThumbnails(metadataWithServer.serverId!, metadataWithServer.ratingKey, client); - } - - // Download subtitles if enabled - if (queueItem.downloadSubtitles && playbackData.mediaInfo != null) { - await _downloadSubtitles(globalKey, metadataWithServer, playbackData.mediaInfo!, client, showYear: showYear); - } - - // Mark as completed - await _transitionStatus(globalKey, DownloadStatus.completed); - await _database.removeFromQueue(globalKey); - - _activeDownloads.remove(globalKey); - - appLogger.i('Download completed for $globalKey'); - return _DownloadResult.success; - } catch (e) { - // Check if this was a user-initiated cancel/pause (not a real failure) - if (e is DioException && e.type == DioExceptionType.cancel) { - // Status was already set by pauseDownload() or cancelDownload() - appLogger.d('Download cancelled/paused for $globalKey: ${e.message}'); - _activeDownloads.remove(globalKey); - - // Clean up .part file for cancel/delete, but preserve for pause - final reason = e.message ?? ''; - if (reason.contains('Cancelled') || reason.contains('deleted')) { - if (downloadFilePath != null) { - await _cleanupPartFile(downloadFilePath); - } + await _database.updateBgTaskId(globalKey, task.taskId); + final success = await FileDownloader().enqueue(task); + if (!success) throw Exception('Failed to enqueue SAF download task'); + appLogger.i('Enqueued SAF download task ${task.taskId} for $globalKey'); + } else { + // Normal mode: use DownloadTask with pause/resume support + String downloadFilePath; + if (metadata.type == 'movie') { + downloadFilePath = await _storageService.getMovieVideoPath(metadata, ext); + } else if (metadata.type == 'episode') { + downloadFilePath = await _storageService.getEpisodeVideoPath(metadata, ext, showYear: showYear); + } else { + downloadFilePath = await _storageService.getVideoFilePath(serverId, metadata.ratingKey, ext); } - // Paused: .part file preserved for resume - return _DownloadResult.success; // User action, not a failure + await File(downloadFilePath).parent.create(recursive: true); + + final task = DownloadTask( + url: playbackData.videoUrl!, + filename: path.basename(downloadFilePath), + directory: path.dirname(downloadFilePath), + baseDirectory: BaseDirectory.root, + group: _downloadGroup, + updates: Updates.statusAndProgress, + requiresWiFi: requiresWiFi, + retries: 3, + allowPause: true, + metaData: globalKey, + displayName: displayName, + ); + + _pendingDownloadContext[globalKey] = _DownloadContext( + metadata: metadata, + queueItem: queueItem, + filePath: downloadFilePath, + extension: ext, + client: client, + showYear: showYear, + mediaInfo: playbackData.mediaInfo, + ); + + await _database.updateBgTaskId(globalKey, task.taskId); + final success = await FileDownloader().enqueue(task); + if (!success) throw Exception('Failed to enqueue download task'); + appLogger.i('Enqueued download task ${task.taskId} for $globalKey'); } - - // Check if this is a retriable network error - if (_isRetriableNetworkError(e)) { - // Network error - keep in queue for auto-retry when connectivity returns - // .part file is preserved for resume via Range header - appLogger.w('Download interrupted by network error for $globalKey, will retry on reconnect'); - await _transitionStatus(globalKey, DownloadStatus.queued); - _activeDownloads.remove(globalKey); - return _DownloadResult.networkError; // Stay in queue - will auto-retry - } - - // Permanent failure - remove from queue and clean up .part file - appLogger.e('Download failed for $globalKey', error: e); + } catch (e) { + appLogger.e('Failed to prepare download for $globalKey', error: e); await _transitionStatus(globalKey, DownloadStatus.failed, errorMessage: e.toString()); await _database.updateDownloadError(globalKey, e.toString()); await _database.removeFromQueue(globalKey); - _activeDownloads.remove(globalKey); - if (downloadFilePath != null) { - await _cleanupPartFile(downloadFilePath); - } - return _DownloadResult.permanentFailure; + _pendingDownloadContext.remove(globalKey); } } - /// Delete the .part file for a given download path - Future _cleanupPartFile(String filePath) async { - final partFile = File('$filePath.part'); - if (await partFile.exists()) { - await partFile.delete(); - appLogger.d('Cleaned up partial file: ${partFile.path}'); - } - } + /// Callback: background_downloader progress update + void _onTaskProgress(TaskProgressUpdate update) { + final globalKey = update.task.metaData; + if (globalKey.isEmpty || update.progress < 0) return; - Future _downloadFile({ - required String url, - required String filePath, - required String globalKey, - required CancelToken cancelToken, - }) async { - final partPath = '$filePath.part'; - final partFile = File(partPath); - await partFile.parent.create(recursive: true); - - // Determine resume offset from existing .part file - int offset = 0; - if (await partFile.exists()) { - final partSize = await partFile.length(); - // Cross-validate with DB to detect corruption - final dbRecord = await _database.getDownloadedMedia(globalKey); - final dbBytes = dbRecord?.downloadedBytes ?? 0; - if (dbBytes > 0 && (partSize - dbBytes).abs() > 1024 * 1024) { - // More than 1MB discrepancy — likely corrupt, restart fresh - appLogger.w('Part file size ($partSize) differs from DB ($dbBytes) by >1MB, restarting'); - await partFile.delete(); - } else if (partSize > 0) { - offset = partSize; - appLogger.i('Resuming download from byte $offset for $globalKey'); - } + // If this item is being paused, the holding queue promoted it — cancel it + if (_pausingKeys.contains(globalKey)) { + FileDownloader().cancelTaskWithId(update.task.taskId); + return; } - await _downloadFileWithRange( - url: url, - filePath: filePath, - partPath: partPath, - globalKey: globalKey, - cancelToken: cancelToken, - offset: offset, - ); - } + final progress = (update.progress * 100).round().clamp(0, 100); + final speedBytesPerSec = update.hasNetworkSpeed ? update.networkSpeed * 1024 * 1024 : 0.0; + final totalBytes = update.hasExpectedFileSize ? update.expectedFileSize : 0; + final downloadedBytes = totalBytes > 0 ? (update.progress * totalBytes).round() : 0; - Future _downloadFileWithRange({ - required String url, - required String filePath, - required String partPath, - required String globalKey, - required CancelToken cancelToken, - required int offset, - }) async { - final headers = {}; - if (offset > 0) { - headers['Range'] = 'bytes=$offset-'; - } - - final response = await _dio.get( - url, - options: Options( - responseType: ResponseType.stream, - headers: headers, - // Prevent Dio from throwing on 206/416 - validateStatus: (status) => status != null && (status >= 200 && status < 300 || status == 416), - ), - cancelToken: cancelToken, - ); - - final statusCode = response.statusCode; - - if (statusCode == 416) { - // Range not satisfiable — .part is stale/invalid, restart fresh - appLogger.w('416 Range Not Satisfiable — deleting .part and restarting for $globalKey'); - final partFile = File(partPath); - if (await partFile.exists()) await partFile.delete(); - return _downloadFileWithRange( - url: url, - filePath: filePath, - partPath: partPath, + _progressController.add( + DownloadProgress( globalKey: globalKey, - cancelToken: cancelToken, - offset: 0, - ); + status: DownloadStatus.downloading, + progress: progress, + downloadedBytes: downloadedBytes, + totalBytes: totalBytes, + speed: speedBytesPerSec, + currentFile: 'video', + ), + ); + + _database.updateDownloadProgress(globalKey, progress, downloadedBytes, totalBytes).catchError((e) { + appLogger.w('Failed to update download progress in DB', error: e); + }); + } + + /// Callback: background_downloader status change + void _onTaskStatusChanged(TaskStatusUpdate update) { + final globalKey = update.task.metaData; + if (globalKey.isEmpty) return; + + appLogger.d('Background task status: ${update.status} for $globalKey'); + + switch (update.status) { + case TaskStatus.complete: + _onDownloadComplete(globalKey, update.task); + case TaskStatus.failed: + _onDownloadFailed(globalKey, update.exception?.description ?? 'Download failed'); + case TaskStatus.notFound: + _onDownloadFailed(globalKey, 'File not found (404)'); + case TaskStatus.canceled: + if (_pausingKeys.contains(globalKey)) { + // Expected cancel from holding-queue promotion during pause — ignore + break; + } + final ctx = _pendingDownloadContext.remove(globalKey); + if (ctx != null) { + // Context still present → OS cancelled the task, not user code + // (user-initiated pause/cancel/delete removes context before cancellation completes) + appLogger.w('Download cancelled by system for $globalKey, re-queuing'); + _database.updateBgTaskId(globalKey, null); + _transitionStatus(globalKey, DownloadStatus.queued); + _database.addToQueue(mediaGlobalKey: globalKey); + if (_lastClient != null) _processQueue(_lastClient!); + } + case TaskStatus.paused: + appLogger.d('Download paused by system for $globalKey'); + case TaskStatus.waitingToRetry: + appLogger.d('Download waiting to retry for $globalKey'); + case TaskStatus.enqueued: + case TaskStatus.running: + // If this item is being paused, the holding queue promoted it — cancel it + if (_pausingKeys.contains(globalKey)) { + FileDownloader().cancelTaskWithId(update.task.taskId); + } + break; } + } - // Determine write mode and total size based on response - int totalBytes; - FileMode fileMode; - int resumeOffset; + /// Handle a permanently failed download + Future _onDownloadFailed(String globalKey, String errorMessage) async { + _pendingDownloadContext.remove(globalKey); + appLogger.e('Download failed for $globalKey: $errorMessage'); + await _transitionStatus(globalKey, DownloadStatus.failed, errorMessage: errorMessage); + await _database.updateDownloadError(globalKey, errorMessage); + await _database.removeFromQueue(globalKey); - if (statusCode == 206) { - // Server supports Range — append to .part file - fileMode = FileMode.append; - resumeOffset = offset; - // Parse total from Content-Range: bytes 1000-9999/10000 - final contentRange = response.headers.value('content-range'); - if (contentRange != null) { - final match = RegExp(r'/(\d+)').firstMatch(contentRange); - totalBytes = match != null ? int.parse(match.group(1)!) : -1; - } else { - final contentLength = response.headers.value('content-length'); - totalBytes = contentLength != null ? int.parse(contentLength) + offset : -1; - } - appLogger.i('Resuming download at $offset/$totalBytes for $globalKey'); - } else { - // 200 — fresh download (server may not support Range) - fileMode = FileMode.write; - resumeOffset = 0; - if (offset > 0) { - appLogger.w('Server returned 200 instead of 206 — restarting from scratch for $globalKey'); - } - final contentLength = response.headers.value('content-length'); - totalBytes = contentLength != null ? int.parse(contentLength) : -1; - } - - final partFile = File(partPath); - final sink = partFile.openWrite(mode: fileMode); - - int receivedBytes = resumeOffset; - int lastReportedBytes = receivedBytes; - DateTime lastUpdate = DateTime.now(); + // Try to enqueue more items from the queue + if (_lastClient != null) _processQueue(_lastClient!); + } + /// Handle a completed video download — store path, download supplementary content, mark done. + Future _onDownloadComplete(String globalKey, Task task) async { try { - await for (final chunk in response.data!.stream) { - sink.add(chunk); - receivedBytes += chunk.length; + final ctx = _pendingDownloadContext.remove(globalKey); - // Throttled progress reporting (every 500ms) - final now = DateTime.now(); - if (now.difference(lastUpdate).inMilliseconds >= 500) { - final elapsed = now.difference(lastUpdate).inMilliseconds / 1000.0; - final bytesPerSecond = elapsed > 0 ? (receivedBytes - lastReportedBytes) / elapsed : 0.0; - lastUpdate = now; - lastReportedBytes = receivedBytes; - - final progress = totalBytes > 0 ? ((receivedBytes / totalBytes) * 100).round() : 0; - - _progressController.add( - DownloadProgress( - globalKey: globalKey, - status: DownloadStatus.downloading, - progress: progress, - downloadedBytes: receivedBytes, - totalBytes: totalBytes, - speed: bytesPerSecond, - currentFile: 'video', - ), - ); - - _database.updateDownloadProgress(globalKey, progress, receivedBytes, totalBytes).catchError((e) { - appLogger.w('Failed to update download progress in DB', error: e); - }); + // ── Phase 1 (critical): resolve and store the video file path ── + final String storedPath; + if (ctx != null) { + // Happy path: context available from this session + if (ctx.isSafMode) { + // UriDownloadTask wrote directly to SAF — find the file URI + final child = await SafStorageService.instance.getChild(ctx.filePath, task.filename); + if (child != null) { + storedPath = child.uri; + } else { + storedPath = await _resolveSafStoredPath(ctx.metadata, ctx.extension, ctx.showYear) ?? ''; + if (storedPath.isEmpty) throw Exception('Cannot determine SAF file URI'); + } + } else { + storedPath = await _storageService.toRelativePath(ctx.filePath); + } + } else { + // Recovery path: context missing (app was restarted) + final existing = await _database.getDownloadedMedia(globalKey); + if (existing?.videoFilePath != null && existing?.status == DownloadStatus.completed.index) { + appLogger.d('Download already completed for $globalKey'); + return; + } + if (existing?.videoFilePath != null) { + // Video path set but status not completed — just finish up + storedPath = existing!.videoFilePath!; + } else if (task is UriDownloadTask) { + // SAF mode recovery: re-derive path from metadata + final parsed = parseGlobalKey(globalKey); + if (parsed == null) throw Exception('Invalid globalKey for recovery: $globalKey'); + final metadata = await _apiCache.getMetadata(parsed.serverId, parsed.ratingKey); + if (metadata == null) throw Exception('No metadata for SAF recovery of $globalKey'); + final ext = _getExtensionFromUrl(task.url) ?? 'mp4'; + storedPath = await _resolveSafStoredPath(metadata, ext, null) ?? ''; + if (storedPath.isEmpty) throw Exception('Cannot resolve SAF path on recovery'); + } else { + // Normal mode recovery: reconstruct from task + storedPath = await _storageService.toRelativePath('${task.directory}/${task.filename}'); } } - // Stream completed successfully — flush and rename - await sink.flush(); - await sink.close(); + // Store video path in DB + await _database.updateVideoFilePath(globalKey, storedPath); + appLogger.d('Video download completed for $globalKey'); - // Verify file size if we know the total - if (totalBytes > 0) { - final actualSize = await partFile.length(); - if (actualSize != totalBytes) { - throw Exception('Download size mismatch: expected $totalBytes bytes but got $actualSize'); - } - } - - // Rename .part to final path - await partFile.rename(filePath); - appLogger.i('Download complete: $filePath ($receivedBytes bytes)'); - } catch (e) { - // Flush what we have so far — preserves partial data for resume + // ── Phase 2 (best-effort): supplementary downloads ── try { - await sink.flush(); - await sink.close(); - } catch (_) { - // Ignore flush errors during error handling + final metadata = ctx?.metadata ?? await _resolveMetadata(globalKey); + final client = ctx?.client ?? _lastClient; + final showYear = ctx?.showYear; + + // Get queue item settings (still in drift at this point) + final queueItem = + ctx?.queueItem ?? + await (_database.select(_database.downloadQueue)..where((t) => t.mediaGlobalKey.equals(globalKey))) + .getSingleOrNull(); + final downloadArtwork = queueItem?.downloadArtwork ?? true; + final downloadSubtitles = queueItem?.downloadSubtitles ?? true; + + if (metadata != null && client != null) { + if (downloadArtwork) { + await _downloadArtwork(globalKey, metadata, client, showYear: showYear); + await _downloadChapterThumbnails(metadata.serverId!, metadata.ratingKey, client); + } + if (downloadSubtitles) { + PlexMediaInfo? mediaInfo = ctx?.mediaInfo; + if (mediaInfo == null) { + try { + final playbackData = await client.getVideoPlaybackData(metadata.ratingKey); + mediaInfo = playbackData.mediaInfo; + } catch (e) { + appLogger.w('Could not re-fetch playback data for subtitles', error: e); + } + } + if (mediaInfo != null) { + await _downloadSubtitles(globalKey, metadata, mediaInfo, client, showYear: showYear); + } + } + } + } catch (e) { + appLogger.w('Supplementary downloads failed for $globalKey (video is saved)', error: e); } - // Persist current progress to DB for resume - final progress = totalBytes > 0 ? ((receivedBytes / totalBytes) * 100).round() : 0; - await _database.updateDownloadProgress(globalKey, progress, receivedBytes, totalBytes).catchError((dbErr) { - appLogger.w('Failed to persist progress on error', error: dbErr); - }); - rethrow; + + // Mark as completed — video is saved regardless of supplementary outcome + await _transitionStatus(globalKey, DownloadStatus.completed); + await _database.removeFromQueue(globalKey); + appLogger.i('Download completed for $globalKey'); + } catch (e) { + appLogger.e('Post-download processing failed for $globalKey', error: e); + await _transitionStatus(globalKey, DownloadStatus.failed, errorMessage: 'Post-processing failed: $e'); + await _database.updateDownloadError(globalKey, 'Post-processing failed: $e'); + await _database.removeFromQueue(globalKey); + } finally { + // Always advance the queue, even after errors + if (_lastClient != null) _processQueue(_lastClient!); } } + /// Resolve metadata from cache using a globalKey + Future _resolveMetadata(String globalKey) async { + final parsed = parseGlobalKey(globalKey); + if (parsed == null) return null; + return _apiCache.getMetadata(parsed.serverId, parsed.ratingKey); + } + + /// Re-derive the SAF file URI from metadata (for recovery when context is lost) + Future _resolveSafStoredPath(PlexMetadata metadata, String ext, int? showYear) async { + final safBaseUri = _storageService.safBaseUri; + if (safBaseUri == null) return null; + + final List pathComponents; + final String safFileName; + if (metadata.type == 'movie') { + pathComponents = _storageService.getMovieSafPathComponents(metadata); + safFileName = _storageService.getMovieSafFileName(metadata, ext); + } else if (metadata.type == 'episode') { + pathComponents = _storageService.getEpisodeSafPathComponents(metadata, showYear: showYear); + safFileName = _storageService.getEpisodeSafFileName(metadata, ext); + } else { + pathComponents = [metadata.serverId!, metadata.ratingKey]; + safFileName = 'video.$ext'; + } + + final dirUri = await SafStorageService.instance.createNestedDirectories(safBaseUri, pathComponents); + if (dirUri == null) return null; + + final child = await SafStorageService.instance.getChild(dirUri, safFileName); + return child?.uri; + } + /// Download artwork for a media item using hash-based storage /// Downloads all artwork types: thumb/poster, clearLogo, and background art Future _downloadArtwork(String globalKey, PlexMetadata metadata, PlexClient client, {int? showYear}) async { @@ -1013,55 +1063,83 @@ class DownloadManagerService { /// Pause a download (works for both downloading and queued items) Future pauseDownload(String globalKey) async { - // Cancel active download if exists - final cancelToken = _activeDownloads[globalKey]; - if (cancelToken != null) { - cancelToken.cancel('Paused by user'); - _activeDownloads.remove(globalKey); + // Mark as pausing synchronously so callbacks from holding-queue promotions + // can detect and cancel promoted tasks before any await yields. + _pausingKeys.add(globalKey); + + try { + final bgTaskId = await _database.getBgTaskId(globalKey); + if (bgTaskId != null) { + final task = await FileDownloader().taskForId(bgTaskId); + if (task != null && task is DownloadTask) { + // Normal mode: native pause support + await FileDownloader().pause(task); + } else { + // SAF mode (UriDownloadTask) or task not found: cancel (re-download on resume) + await FileDownloader().cancelTaskWithId(bgTaskId); + } + } + _pendingDownloadContext.remove(globalKey); + await _transitionStatus(globalKey, DownloadStatus.paused); + await _database.removeFromQueue(globalKey); + } finally { + _pausingKeys.remove(globalKey); } - // Update status to paused and remove from queue so it doesn't restart - await _transitionStatus(globalKey, DownloadStatus.paused); - await _database.removeFromQueue(globalKey); } /// Resume a paused download Future resumeDownload(String globalKey, PlexClient client) async { + final bgTaskId = await _database.getBgTaskId(globalKey); + + // Try native resume first (only works for normal-mode DownloadTask that was paused) + if (bgTaskId != null) { + final task = await FileDownloader().taskForId(bgTaskId); + if (task != null && task is DownloadTask) { + final resumed = await FileDownloader().resume(task); + if (resumed) { + appLogger.i('Resumed download via background_downloader for $globalKey'); + await _database.updateDownloadStatus(globalKey, DownloadStatus.downloading.index); + _emitProgress(globalKey, DownloadStatus.downloading, 0); + return; + } + } + } + + // Native resume failed or not supported (SAF mode) — re-enqueue from scratch + await _database.updateBgTaskId(globalKey, null); await _transitionStatus(globalKey, DownloadStatus.queued); - // Re-add to queue (pauseDownload removes from queue) await _database.addToQueue(mediaGlobalKey: globalKey); _processQueue(client); } /// Retry a failed download Future retryDownload(String globalKey, PlexClient client) async { - // Clear error and reset retry count await _database.clearDownloadError(globalKey); - // Reset status to queued + await _database.updateBgTaskId(globalKey, null); await _transitionStatus(globalKey, DownloadStatus.queued); - // Re-add to queue await _database.addToQueue(mediaGlobalKey: globalKey); _processQueue(client); } /// Cancel a download Future cancelDownload(String globalKey) async { - final cancelToken = _activeDownloads[globalKey]; - if (cancelToken != null) { - cancelToken.cancel('Cancelled by user'); - _activeDownloads.remove(globalKey); + final bgTaskId = await _database.getBgTaskId(globalKey); + if (bgTaskId != null) { + await FileDownloader().cancelTaskWithId(bgTaskId); } + _pendingDownloadContext.remove(globalKey); await _transitionStatus(globalKey, DownloadStatus.cancelled); await _database.removeFromQueue(globalKey); } /// Delete a downloaded item and its files Future deleteDownload(String globalKey) async { - // Cancel if actively downloading - final cancelToken = _activeDownloads[globalKey]; - if (cancelToken != null) { - cancelToken.cancel('Download deleted'); - _activeDownloads.remove(globalKey); + // Cancel if actively downloading via background_downloader + final bgTaskId = await _database.getBgTaskId(globalKey); + if (bgTaskId != null) { + await FileDownloader().cancelTaskWithId(bgTaskId); } + _pendingDownloadContext.remove(globalKey); // Delete files from storage final parsed = parseGlobalKey(globalKey); @@ -1093,27 +1171,6 @@ class DownloadManagerService { // Delete files from storage (with progress updates) await _deleteMediaFilesWithMetadata(serverId, ratingKey); - // Clean up any .part file in SAF temp cache - if (_storageService.isUsingSaf) { - try { - final tempFileName = globalKey.replaceAll(':', '_'); - // We don't know the extension, so search for any matching .part file - final tempDir = await _storageService.getTempDownloadPath(''); - final tempDirObj = Directory(path.dirname(tempDir)); - if (await tempDirObj.exists()) { - await for (final entity in tempDirObj.list()) { - if (entity is File && - path.basename(entity.path).startsWith(tempFileName) && - entity.path.endsWith('.part')) { - await _deleteFileIfExists(entity, 'SAF temp partial download'); - } - } - } - } catch (e) { - appLogger.w('Failed to clean up SAF temp .part files', error: e); - } - } - // Delete from API cache await _apiCache.deleteForItem(serverId, ratingKey); @@ -1605,10 +1662,6 @@ class DownloadManagerService { } void dispose() { - _connectivitySubscription?.cancel(); - for (final token in _activeDownloads.values) { - token.cancel('Service disposed'); - } _progressController.close(); _deletionProgressController.close(); } diff --git a/lib/widgets/download_tree_view.dart b/lib/widgets/download_tree_view.dart index 098f1d92..6aab1a42 100644 --- a/lib/widgets/download_tree_view.dart +++ b/lib/widgets/download_tree_view.dart @@ -654,8 +654,8 @@ class _DownloadTreeItemState extends State<_DownloadTreeItem> { ), ], - // Progress bar - if (widget.node.status == DownloadStatus.downloading || widget.node.status == DownloadStatus.queued) ...[ + // Progress bar for active downloads + if (widget.node.status == DownloadStatus.downloading) ...[ const SizedBox(height: 8), LinearProgressIndicator( value: widget.node.progress, @@ -671,6 +671,17 @@ class _DownloadTreeItemState extends State<_DownloadTreeItem> { ), ], ], + + // Queued label + if (widget.node.status == DownloadStatus.queued) ...[ + const SizedBox(height: 4), + Text( + t.downloads.downloadQueued, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ], ], ), ), diff --git a/pubspec.lock b/pubspec.lock index 5a078048..ff38b009 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "2ea5322fe836c0aaf96aefd29ef1936771c71927f687cf18168dcc119666a45f" + url: "https://pub.dev" + source: hosted + version: "9.5.2" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9289c2b9..b543a970 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: flutter_svg: ^2.2.3 mobile_scanner: ^6.0.2 android_intent_plus: ^5.0.2 + background_downloader: ^9.5.2 dev_dependencies: flutter_test: