Merge the deduplication and dead-code removal pass

Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
This commit is contained in:
edde746
2026-07-26 19:41:23 +02:00
425 changed files with 12045 additions and 17543 deletions
+143 -302
View File
@@ -246,197 +246,56 @@ class AppDatabase extends _$AppDatabase {
final joinRows = await (select(
profileConnections,
)..orderBy([(t) => OrderingTerm.asc(t.profileId), (t) => OrderingTerm.asc(t.connectionId)])).get();
// Drift's generated serializer is the recovery image's column schema:
// `toJson`/`fromJson` use these camelCase keys, so the read and restore
// sides can never drift apart when a column is added or renamed.
return {
'connections': [
for (final row in connectionRows)
{
'id': row.id,
'kind': row.kind,
'displayName': row.displayName,
'configJson': row.configJson,
'isDefault': row.isDefault,
'createdAt': row.createdAt,
'lastAuthenticatedAt': row.lastAuthenticatedAt,
},
],
'profiles': [
for (final row in profileRows)
{
'id': row.id,
'kind': row.kind,
'displayName': row.displayName,
'avatarThumbUrl': row.avatarThumbUrl,
'configJson': row.configJson,
'sortOrder': row.sortOrder,
'createdAt': row.createdAt,
'lastUsedAt': row.lastUsedAt,
},
],
'profileConnections': [
for (final row in joinRows)
{
'profileId': row.profileId,
'connectionId': row.connectionId,
'userToken': row.userToken,
'userIdentifier': row.userIdentifier,
'isDefault': row.isDefault,
'tokenAcquiredAt': row.tokenAcquiredAt,
'lastUsedAt': row.lastUsedAt,
},
],
'connections': [for (final row in connectionRows) row.toJson()],
'profiles': [for (final row in profileRows) row.toJson()],
'profileConnections': [for (final row in joinRows) row.toJson()],
};
}
Future<Map<String, Object?>> _readPendingRecoveryRows() async {
final rows = await (select(offlineWatchProgress)..orderBy([(t) => OrderingTerm.asc(t.id)])).get();
return {
'offlineWatchProgress': [
for (final row in rows)
{
'id': row.id,
'profileId': row.profileId,
'serverId': row.serverId,
'clientScopeId': row.clientScopeId,
'ratingKey': row.ratingKey,
'globalKey': row.globalKey,
'actionType': row.actionType,
'viewOffset': row.viewOffset,
'duration': row.duration,
'shouldMarkWatched': row.shouldMarkWatched,
'createdAt': row.createdAt,
'updatedAt': row.updatedAt,
'syncAttempts': row.syncAttempts,
'lastError': row.lastError,
},
],
'offlineWatchProgress': [for (final row in rows) row.toJson()],
};
}
Future<void> _restoreRecoverySnapshot(TvosDatabaseRecoverySnapshot snapshot) async {
final connectionRows = _decodeRecoveryRows(snapshot.identity, 'connections', const {
'id',
'kind',
'displayName',
'configJson',
'isDefault',
'createdAt',
'lastAuthenticatedAt',
});
final profileRows = _decodeRecoveryRows(snapshot.identity, 'profiles', const {
'id',
'kind',
'displayName',
'avatarThumbUrl',
'configJson',
'sortOrder',
'createdAt',
'lastUsedAt',
});
final joinRows = _decodeRecoveryRows(snapshot.identity, 'profileConnections', const {
'profileId',
'connectionId',
'userToken',
'userIdentifier',
'isDefault',
'tokenAcquiredAt',
'lastUsedAt',
});
final pendingRows = _decodeRecoveryRows(snapshot.pending, 'offlineWatchProgress', const {
'id',
'profileId',
'serverId',
'clientScopeId',
'ratingKey',
'globalKey',
'actionType',
'viewOffset',
'duration',
'shouldMarkWatched',
'createdAt',
'updatedAt',
'syncAttempts',
'lastError',
});
final connectionRows = _decodeRecoveryRows(snapshot.identity, 'connections', ConnectionRow.fromJson);
final profileRows = _decodeRecoveryRows(snapshot.identity, 'profiles', ProfileRow.fromJson);
final joinRows = _decodeRecoveryRows(snapshot.identity, 'profileConnections', ProfileConnectionRow.fromJson);
final pendingRows = _decodeRecoveryRows(
snapshot.pending,
'offlineWatchProgress',
OfflineWatchProgressItem.fromJson,
);
// Recovery images from releases before the credential vault may contain
// plaintext secrets. Protect them before they cross into Drift; already
// protected values remain byte-identical because vault protection is
// idempotent.
for (final row in connectionRows) {
final kind = _requiredRecoveryValue<String>(row, 'kind');
final configJson = _requiredRecoveryValue<String>(row, 'configJson');
final decoded = jsonDecode(configJson);
for (var index = 0; index < connectionRows.length; index++) {
final row = connectionRows[index];
final decoded = jsonDecode(row.configJson);
if (decoded is! Map<String, dynamic>) {
throw const FormatException('Invalid connection configuration');
}
if (_containsPlaintextConnectionCredential(kind, decoded)) {
row['configJson'] = jsonEncode(await CredentialVault.protectConnectionConfig(kind, decoded));
if (_containsPlaintextConnectionCredential(row.kind, decoded)) {
connectionRows[index] = row.copyWith(
configJson: jsonEncode(await CredentialVault.protectConnectionConfig(row.kind, decoded)),
);
}
}
for (final row in joinRows) {
final token = _requiredRecoveryValue<String>(row, 'userToken');
if (token.isNotEmpty && !CredentialVault.isProtected(token)) {
row['userToken'] = await CredentialVault.protect(token);
for (var index = 0; index < joinRows.length; index++) {
final row = joinRows[index];
if (row.userToken.isNotEmpty && !CredentialVault.isProtected(row.userToken)) {
joinRows[index] = row.copyWith(userToken: await CredentialVault.protect(row.userToken));
}
}
final connectionCompanions = [
for (final row in connectionRows)
ConnectionsCompanion(
id: Value(_requiredRecoveryValue<String>(row, 'id')),
kind: Value(_requiredRecoveryValue<String>(row, 'kind')),
displayName: Value(_requiredRecoveryValue<String>(row, 'displayName')),
configJson: Value(_requiredRecoveryValue<String>(row, 'configJson')),
isDefault: Value(_requiredRecoveryValue<bool>(row, 'isDefault')),
createdAt: Value(_requiredRecoveryValue<int>(row, 'createdAt')),
lastAuthenticatedAt: Value(_nullableRecoveryValue<int>(row, 'lastAuthenticatedAt')),
),
];
final profileCompanions = [
for (final row in profileRows)
ProfilesCompanion(
id: Value(_requiredRecoveryValue<String>(row, 'id')),
kind: Value(_requiredRecoveryValue<String>(row, 'kind')),
displayName: Value(_requiredRecoveryValue<String>(row, 'displayName')),
avatarThumbUrl: Value(_nullableRecoveryValue<String>(row, 'avatarThumbUrl')),
configJson: Value(_requiredRecoveryValue<String>(row, 'configJson')),
sortOrder: Value(_requiredRecoveryValue<int>(row, 'sortOrder')),
createdAt: Value(_requiredRecoveryValue<int>(row, 'createdAt')),
lastUsedAt: Value(_nullableRecoveryValue<int>(row, 'lastUsedAt')),
),
];
final joinCompanions = [
for (final row in joinRows)
ProfileConnectionsCompanion(
profileId: Value(_requiredRecoveryValue<String>(row, 'profileId')),
connectionId: Value(_requiredRecoveryValue<String>(row, 'connectionId')),
userToken: Value(_requiredRecoveryValue<String>(row, 'userToken')),
userIdentifier: Value(_requiredRecoveryValue<String>(row, 'userIdentifier')),
isDefault: Value(_requiredRecoveryValue<bool>(row, 'isDefault')),
tokenAcquiredAt: Value(_nullableRecoveryValue<int>(row, 'tokenAcquiredAt')),
lastUsedAt: Value(_nullableRecoveryValue<int>(row, 'lastUsedAt')),
),
];
final pendingCompanions = [
for (final row in pendingRows)
OfflineWatchProgressCompanion(
id: Value(_requiredRecoveryValue<int>(row, 'id')),
profileId: Value(_nullableRecoveryValue<String>(row, 'profileId')),
serverId: Value(_requiredRecoveryValue<String>(row, 'serverId')),
clientScopeId: Value(_nullableRecoveryValue<String>(row, 'clientScopeId')),
ratingKey: Value(_requiredRecoveryValue<String>(row, 'ratingKey')),
globalKey: Value(_requiredRecoveryValue<String>(row, 'globalKey')),
actionType: Value(_requiredRecoveryValue<String>(row, 'actionType')),
viewOffset: Value(_nullableRecoveryValue<int>(row, 'viewOffset')),
duration: Value(_nullableRecoveryValue<int>(row, 'duration')),
shouldMarkWatched: Value(_requiredRecoveryValue<bool>(row, 'shouldMarkWatched')),
createdAt: Value(_requiredRecoveryValue<int>(row, 'createdAt')),
updatedAt: Value(_requiredRecoveryValue<int>(row, 'updatedAt')),
syncAttempts: Value(_requiredRecoveryValue<int>(row, 'syncAttempts')),
lastError: Value(_nullableRecoveryValue<String>(row, 'lastError')),
),
];
await transaction(() async {
// Recovery completion (the durable marker removal) is deliberately
// separate from this transaction. Replace the snapshot-owned rows so a
@@ -446,54 +305,57 @@ class AppDatabase extends _$AppDatabase {
await delete(profiles).go();
await delete(connections).go();
await delete(offlineWatchProgress).go();
for (final row in connectionCompanions) {
await into(connections).insert(row);
// `toCompanion(false)` writes every column explicitly, including the
// nulls, so a restored row is byte-identical to the captured one rather
// than picking up column defaults.
for (final row in connectionRows) {
await into(connections).insert(row.toCompanion(false));
}
for (final row in profileCompanions) {
await into(profiles).insert(row);
for (final row in profileRows) {
await into(profiles).insert(row.toCompanion(false));
}
for (final row in joinCompanions) {
await into(profileConnections).insert(row);
for (final row in joinRows) {
await into(profileConnections).insert(row.toCompanion(false));
}
for (final row in pendingCompanions) {
await into(offlineWatchProgress).insert(row);
for (final row in pendingRows) {
await into(offlineWatchProgress).insert(row.toCompanion(false));
}
});
}
static List<Map<String, Object?>> _decodeRecoveryRows(
static List<T> _decodeRecoveryRows<T extends DataClass>(
Map<String, Object?> group,
String key,
Set<String> expectedKeys,
T Function(Map<String, dynamic> json) fromJson,
) {
final value = group[key];
if (value is! List) throw const FormatException('Invalid tvOS database recovery image');
if (value is! List) throw _invalidRecoveryImage;
return [
for (final value in value)
if (value is Map<String, Object?> &&
value.keys.toSet().containsAll(expectedKeys) &&
value.length == expectedKeys.length)
value
else
throw const FormatException('Invalid tvOS database recovery image'),
for (final row in value)
if (row is Map<String, dynamic>) _decodeRecoveryRow(row, fromJson) else throw _invalidRecoveryImage,
];
}
static T _requiredRecoveryValue<T>(Map<String, Object?> row, String key) {
final value = row[key];
if (!row.containsKey(key) || value is! T) {
throw const FormatException('Invalid tvOS database recovery image');
/// Reads one row through drift's generated deserializer and rejects anything
/// that does not round-trip back to the exact same map. Drift already throws
/// on a missing or mistyped required column; the round-trip additionally
/// rejects unknown and missing-but-nullable columns, which the serializer
/// would otherwise accept silently.
static T _decodeRecoveryRow<T extends DataClass>(
Map<String, dynamic> row,
T Function(Map<String, dynamic> json) fromJson,
) {
final T decoded;
try {
decoded = fromJson(row);
} catch (_) {
throw _invalidRecoveryImage;
}
return value;
if (!mapEquals(decoded.toJson(), row)) throw _invalidRecoveryImage;
return decoded;
}
static T? _nullableRecoveryValue<T>(Map<String, Object?> row, String key) {
if (!row.containsKey(key)) throw const FormatException('Invalid tvOS database recovery image');
final value = row[key];
if (value == null) return null;
if (value is! T) throw const FormatException('Invalid tvOS database recovery image');
return value as T;
}
static const FormatException _invalidRecoveryImage = FormatException('Invalid tvOS database recovery image');
@override
int get schemaVersion => 20;
@@ -658,89 +520,27 @@ class AppDatabase extends _$AppDatabase {
}
if (from < 17) {
appLogger.i('Scoping pinned legacy Plex metadata before removing bare cache rows (v17 migration)');
await customStatement('''
WITH download_metadata_ids AS (
SELECT global_key, server_id, rating_key AS metadata_id
FROM downloaded_media
UNION
SELECT global_key, server_id, parent_rating_key AS metadata_id
FROM downloaded_media
WHERE parent_rating_key IS NOT NULL
AND parent_rating_key != ''
UNION
SELECT global_key, server_id, grandparent_rating_key AS metadata_id
FROM downloaded_media
WHERE grandparent_rating_key IS NOT NULL
AND grandparent_rating_key != ''
)
INSERT INTO api_cache (cache_key, data, pinned, cached_at)
SELECT DISTINCT
metadata.server_id
|| '/~plex-profile/'
|| owner.profile_id
|| ':'
|| substr(source.cache_key, length(metadata.server_id) + 2),
source.data,
source.pinned,
source.cached_at
FROM download_metadata_ids AS metadata
JOIN download_owners AS owner
ON owner.global_key = metadata.global_key
JOIN api_cache AS source
ON source.cache_key =
metadata.server_id || ':/library/metadata/' || metadata.metadata_id
OR source.cache_key =
metadata.server_id || ':/library/metadata/' || metadata.metadata_id || '/children'
WHERE source.pinned = 1
ON CONFLICT(cache_key) DO UPDATE SET
data = excluded.data,
pinned = excluded.pinned,
cached_at = excluded.cached_at
''');
await customStatement(
_rescopePinnedPlexMetadataStatement(
namespaceExpression: "'/~plex-profile/' || owner.profile_id || ':'",
ownerJoin: '''JOIN download_owners AS owner
ON owner.global_key = metadata.global_key''',
),
);
// A direct pre-v14 upgrade has no owners yet: profiles and owner
// adoption are bootstrapped only after the database opens. Preserve
// those downloads in the neutral Plex transfer namespace so the
// first profile can adopt them without inheriting legacy watch data.
await customStatement('''
WITH download_metadata_ids AS (
SELECT global_key, server_id, rating_key AS metadata_id
FROM downloaded_media
UNION
SELECT global_key, server_id, parent_rating_key AS metadata_id
FROM downloaded_media
WHERE parent_rating_key IS NOT NULL
AND parent_rating_key != ''
UNION
SELECT global_key, server_id, grandparent_rating_key AS metadata_id
FROM downloaded_media
WHERE grandparent_rating_key IS NOT NULL
AND grandparent_rating_key != ''
)
INSERT INTO api_cache (cache_key, data, pinned, cached_at)
SELECT DISTINCT
metadata.server_id
|| '/~plex-transfer:'
|| substr(source.cache_key, length(metadata.server_id) + 2),
source.data,
source.pinned,
source.cached_at
FROM download_metadata_ids AS metadata
JOIN api_cache AS source
ON source.cache_key =
metadata.server_id || ':/library/metadata/' || metadata.metadata_id
OR source.cache_key =
metadata.server_id || ':/library/metadata/' || metadata.metadata_id || '/children'
WHERE source.pinned = 1
AND NOT EXISTS (
SELECT 1
FROM download_owners AS owner
WHERE owner.global_key = metadata.global_key
)
ON CONFLICT(cache_key) DO UPDATE SET
data = excluded.data,
pinned = excluded.pinned,
cached_at = excluded.cached_at
''');
await customStatement(
_rescopePinnedPlexMetadataStatement(
namespaceExpression: "'/~plex-transfer:'",
ownerFilter: '''AND NOT EXISTS (
SELECT 1
FROM download_owners AS owner
WHERE owner.global_key = metadata.global_key
)''',
),
);
final transferRows = await customSelect('''
SELECT cache_key, data
@@ -941,10 +741,6 @@ class AppDatabase extends _$AppDatabase {
}
}
Expression<bool> _clientScopePredicate(GeneratedColumn<String> column, String? clientScopeId) {
return clientScopeId == null ? column.isNull() : column.equals(clientScopeId);
}
Expression<bool> _nullableTextPredicate(GeneratedColumn<String> column, String? value) {
return value == null ? column.isNull() : column.equals(value);
}
@@ -971,6 +767,7 @@ class AppDatabase extends _$AppDatabase {
}
/// Get pending watch actions for a specific server
@visibleForTesting
Future<List<OfflineWatchProgressItem>> getPendingWatchActionsForServer(ServerId serverId, {String? profileId}) {
return (select(offlineWatchProgress)
..where(
@@ -994,12 +791,13 @@ class AppDatabase extends _$AppDatabase {
(t) =>
matchesKey(t) &
(filterProfile ? _nullableTextPredicate(t.profileId, profileId) : const Constant(true)) &
(filterClientScope ? _clientScopePredicate(t.clientScopeId, clientScopeId) : const Constant(true)),
(filterClientScope ? _nullableTextPredicate(t.clientScopeId, clientScopeId) : const Constant(true)),
)
..orderBy([(t) => OrderingTerm.desc(t.updatedAt), (t) => OrderingTerm.desc(t.id)]);
}
/// Get the latest action for a specific item
@visibleForTesting
Future<OfflineWatchProgressItem?> getLatestWatchAction(
String globalKey, {
String? profileId,
@@ -1102,7 +900,7 @@ class AppDatabase extends _$AppDatabase {
(t) =>
t.globalKey.equals(globalKey) &
_nullableTextPredicate(t.profileId, profileId) &
_clientScopePredicate(t.clientScopeId, clientScopeId) &
_nullableTextPredicate(t.clientScopeId, clientScopeId) &
t.actionType.equals(OfflineActionType.progress.id),
)
..orderBy([(t) => OrderingTerm.asc(t.id)]))
@@ -1164,7 +962,7 @@ class AppDatabase extends _$AppDatabase {
(t) =>
t.globalKey.equals(globalKey) &
_nullableTextPredicate(t.profileId, profileId) &
_clientScopePredicate(t.clientScopeId, clientScopeId),
_nullableTextPredicate(t.clientScopeId, clientScopeId),
))
.go();
@@ -1384,29 +1182,21 @@ class AppDatabase extends _$AppDatabase {
}
}
Future<void> updateSyncRuleCount(String globalKey, int episodeCount) async {
await (update(
syncRules,
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(episodeCount: Value(episodeCount)));
Future<void> _writeSyncRule(String globalKey, SyncRulesCompanion values) async {
await (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(values);
}
Future<void> updateSyncRuleFilter(String globalKey, String downloadFilter) async {
await (update(
syncRules,
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(downloadFilter: Value(downloadFilter)));
}
Future<void> updateSyncRuleCount(String globalKey, int episodeCount) =>
_writeSyncRule(globalKey, SyncRulesCompanion(episodeCount: Value(episodeCount)));
Future<void> updateSyncRuleEnabled(String globalKey, bool enabled) async {
await (update(
syncRules,
)..where((t) => t.globalKey.equals(globalKey))).write(SyncRulesCompanion(enabled: Value(enabled)));
}
Future<void> updateSyncRuleFilter(String globalKey, String downloadFilter) =>
_writeSyncRule(globalKey, SyncRulesCompanion(downloadFilter: Value(downloadFilter)));
Future<void> updateSyncRuleLastExecuted(String globalKey) async {
await (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(
SyncRulesCompanion(lastExecutedAt: Value(DateTime.now().millisecondsSinceEpoch)),
);
}
Future<void> updateSyncRuleEnabled(String globalKey, bool enabled) =>
_writeSyncRule(globalKey, SyncRulesCompanion(enabled: Value(enabled)));
Future<void> updateSyncRuleLastExecuted(String globalKey) =>
_writeSyncRule(globalKey, SyncRulesCompanion(lastExecutedAt: Value(DateTime.now().millisecondsSinceEpoch)));
Future<void> completeSyncRuleExecution(String globalKey) {
return (update(syncRules)..where((t) => t.globalKey.equals(globalKey))).write(
@@ -1437,6 +1227,57 @@ class AppDatabase extends _$AppDatabase {
}
}
/// Builds the v17 statement that re-keys pinned legacy Plex metadata rows into
/// a scoped cache namespace.
///
/// The owned and the ownerless branch run the same operation over the same
/// `download_metadata_ids` set and differ only in three spots: the expression
/// spliced into the new `cache_key` ([namespaceExpression]), an optional join
/// that exposes the owning profile ([ownerJoin]), and an optional extra
/// predicate that keeps each branch to its own rows ([ownerFilter]).
String _rescopePinnedPlexMetadataStatement({
required String namespaceExpression,
String ownerJoin = '',
String ownerFilter = '',
}) =>
'''
WITH download_metadata_ids AS (
SELECT global_key, server_id, rating_key AS metadata_id
FROM downloaded_media
UNION
SELECT global_key, server_id, parent_rating_key AS metadata_id
FROM downloaded_media
WHERE parent_rating_key IS NOT NULL
AND parent_rating_key != ''
UNION
SELECT global_key, server_id, grandparent_rating_key AS metadata_id
FROM downloaded_media
WHERE grandparent_rating_key IS NOT NULL
AND grandparent_rating_key != ''
)
INSERT INTO api_cache (cache_key, data, pinned, cached_at)
SELECT DISTINCT
metadata.server_id
|| $namespaceExpression
|| substr(source.cache_key, length(metadata.server_id) + 2),
source.data,
source.pinned,
source.cached_at
FROM download_metadata_ids AS metadata
$ownerJoin
JOIN api_cache AS source
ON source.cache_key =
metadata.server_id || ':/library/metadata/' || metadata.metadata_id
OR source.cache_key =
metadata.server_id || ':/library/metadata/' || metadata.metadata_id || '/children'
WHERE source.pinned = 1
$ownerFilter
ON CONFLICT(cache_key) DO UPDATE SET
data = excluded.data,
pinned = excluded.pinned,
cached_at = excluded.cached_at
''';
Future<File> _resolveProductionDatabaseFile() async {
final dbFolder = (Platform.isAndroid || Platform.isIOS)
? await getApplicationDocumentsDirectory()
+3 -58
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import '../media/ids.dart';
import 'app_database.dart';
@@ -147,6 +148,7 @@ extension DownloadDatabaseOperations on AppDatabase {
return (await _validDownloadOwnerRows(globalKey)).length;
}
@visibleForTesting
Future<bool> hasDownloadOwner(String globalKey, {String? excludingProfileId}) async {
final rows = await _validDownloadOwnerRows(globalKey, excludingProfileId: excludingProfileId);
return rows.isNotEmpty;
@@ -278,64 +280,6 @@ extension DownloadDatabaseOperations on AppDatabase {
}
}
Future<void> insertDownload({
required ServerId serverId,
String? clientScopeId,
required String ratingKey,
required String globalKey,
required String type,
String? parentRatingKey,
String? grandparentRatingKey,
required int status,
int mediaIndex = 0,
String? mediaSourceId,
}) async {
await customUpdate(
'''
INSERT INTO downloaded_media (
server_id,
client_scope_id,
rating_key,
global_key,
type,
parent_rating_key,
grandparent_rating_key,
status,
media_index,
media_source_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(global_key) DO UPDATE SET
server_id = excluded.server_id,
client_scope_id = excluded.client_scope_id,
rating_key = excluded.rating_key,
type = excluded.type,
parent_rating_key = excluded.parent_rating_key,
grandparent_rating_key = excluded.grandparent_rating_key,
status = excluded.status,
progress = 0,
total_bytes = NULL,
downloaded_bytes = 0,
error_message = NULL,
retry_count = 0,
media_index = excluded.media_index,
media_source_id = excluded.media_source_id
''',
variables: [
Variable<String>(serverId),
Variable<String>(clientScopeId),
Variable<String>(ratingKey),
Variable<String>(globalKey),
Variable<String>(type),
Variable<String>(parentRatingKey),
Variable<String>(grandparentRatingKey),
Variable<int>(status),
Variable<int>(mediaIndex),
Variable<String>(mediaSourceId),
],
updates: {downloadedMedia},
);
}
Future<void> addToQueue({
required String mediaGlobalKey,
int priority = 0,
@@ -582,6 +526,7 @@ extension DownloadDatabaseOperations on AppDatabase {
return (await query.map((row) => row.read(count) ?? 0).getSingle());
}
@visibleForTesting
Future<Set<String>> getReferencedDownloadSafRoots() async {
final rows =
await (selectOnly(downloadedMedia)
+1 -1
View File
@@ -218,7 +218,7 @@ class ProfileConnections extends Table {
// Profile.virtualPlexHome from PlexHomeService's live cache, never
// persisted in `profiles`), so an FK here would reject every join row
// they need. Profile deletion instead cleans up join rows explicitly
// (removeAllProfileConnectionsAndCleanup in profile_connection_cleanup)
// (ProfileConnectionCleanup.removeAllProfileConnections)
// before calling ProfileRegistry.remove.
TextColumn get profileId => text()();
TextColumn get connectionId => text().references(Connections, #id, onDelete: KeyAction.cascade)();