migrate media artwork storage and mobile drawer navigation

This commit is contained in:
ngfchl
2026-07-19 11:30:36 +08:00
parent 8481276954
commit d762a1e4cc
2 changed files with 173 additions and 124 deletions
+100 -9
View File
@@ -21,15 +21,19 @@ class MediaLibraryStore {
);
_database = await openDatabase(
databasePath,
version: 4,
version: 5,
onCreate: (db, _) async {
await _createSchema(db);
},
onUpgrade: (db, _, _) async {
onUpgrade: (db, oldVersion, _) async {
await _createSchema(db);
if (oldVersion < 5) {
await _migrateArtworkBlobSchema(db);
}
},
);
await _createSchema(_database!);
await _vacuumIfFragmented(_database!);
return _database!;
}
@@ -211,7 +215,7 @@ class MediaLibraryStore {
INSERT OR REPLACE INTO media_items (
library_id, file_id, resource_path, cloud_name, file_size, gcid,
file_type, tmdb_id, media_kind, title, original_title,
release_date, overview, poster_path, backdrop_path, poster, backdrop,
release_date, overview, poster_path, backdrop_path,
has_chinese_audio, has_chinese_subtitle, collection_id,
collection_name, updated_at
)
@@ -219,7 +223,7 @@ class MediaLibraryStore {
library_id, file_id, resource_path, cloud_name, file_size, gcid,
file_type, tmdb_id, media_kind, title, original_title,
release_date, overview, $importedPosterPath, $importedBackdropPath,
NULL, NULL, has_chinese_audio, has_chinese_subtitle, collection_id,
has_chinese_audio, has_chinese_subtitle, collection_id,
collection_name, updated_at
FROM imported_backup.media_items
''');
@@ -235,9 +239,14 @@ class MediaLibraryStore {
Future<MediaLibraryStorageStats> optimizeStorage() async {
final db = await _db;
final before = await _databaseBytes(db.path);
final removedArtwork = await db.rawUpdate('''UPDATE media_items
SET poster = NULL, backdrop = NULL
WHERE poster IS NOT NULL OR backdrop IS NOT NULL''');
final columns = await _tableColumns(db, 'main', 'media_items');
final hasLegacyArtwork =
columns.contains('poster') || columns.contains('backdrop');
final removedArtwork = hasLegacyArtwork
? await db.rawUpdate('''UPDATE media_items
SET poster = NULL, backdrop = NULL
WHERE poster IS NOT NULL OR backdrop IS NOT NULL''')
: 0;
try {
await db.execute('PRAGMA wal_checkpoint(TRUNCATE)');
} on DatabaseException catch (error) {
@@ -548,8 +557,6 @@ class MediaLibraryStore {
overview TEXT NOT NULL,
poster_path TEXT,
backdrop_path TEXT,
poster BLOB,
backdrop BLOB,
has_chinese_audio INTEGER NOT NULL DEFAULT 0,
has_chinese_subtitle INTEGER NOT NULL DEFAULT 0,
collection_id INTEGER,
@@ -592,6 +599,90 @@ class MediaLibraryStore {
await _ensureColumn(db, 'media_items', 'backdrop_path', 'TEXT');
}
Future<void> _migrateArtworkBlobSchema(Database db) async {
final columns = await _tableColumns(db, 'main', 'media_items');
if (!columns.contains('poster') && !columns.contains('backdrop')) return;
await db.transaction((txn) async {
await txn.execute('''
CREATE TABLE media_items_compact (
library_id TEXT NOT NULL,
file_id TEXT NOT NULL,
resource_path TEXT NOT NULL,
cloud_name TEXT NOT NULL,
file_size INTEGER,
gcid TEXT,
file_type INTEGER NOT NULL,
tmdb_id INTEGER,
media_kind TEXT,
title TEXT NOT NULL,
original_title TEXT NOT NULL,
release_date TEXT NOT NULL,
overview TEXT NOT NULL,
poster_path TEXT,
backdrop_path TEXT,
has_chinese_audio INTEGER NOT NULL DEFAULT 0,
has_chinese_subtitle INTEGER NOT NULL DEFAULT 0,
collection_id INTEGER,
collection_name TEXT,
updated_at REAL NOT NULL,
PRIMARY KEY (library_id, file_id),
FOREIGN KEY (library_id) REFERENCES media_libraries(id) ON DELETE CASCADE
)
''');
await txn.execute('''
INSERT INTO media_items_compact (
library_id, file_id, resource_path, cloud_name, file_size, gcid,
file_type, tmdb_id, media_kind, title, original_title, release_date,
overview, poster_path, backdrop_path, has_chinese_audio,
has_chinese_subtitle, collection_id, collection_name, updated_at
)
SELECT
library_id, file_id, resource_path, cloud_name, file_size, gcid,
file_type, tmdb_id, media_kind, title, original_title, release_date,
overview, poster_path, backdrop_path, has_chinese_audio,
has_chinese_subtitle, collection_id, collection_name, updated_at
FROM media_items
''');
await txn.execute('DROP TABLE media_items');
await txn.execute(
'ALTER TABLE media_items_compact RENAME TO media_items',
);
await _createMediaItemIndexes(txn);
});
}
Future<void> _vacuumIfFragmented(Database db) async {
final pageCount = await db.rawQuery('PRAGMA page_count');
final freeList = await db.rawQuery('PRAGMA freelist_count');
final pages = _asInt(pageCount.firstOrNull?['page_count']) ?? 0;
final free = _asInt(freeList.firstOrNull?['freelist_count']) ?? 0;
if (pages == 0 || free < 1024 || free / pages < 0.2) return;
try {
await db.execute('PRAGMA wal_checkpoint(TRUNCATE)');
await db.execute('VACUUM');
} on DatabaseException {
// A failed optional compaction never prevents users from reading media.
}
}
Future<void> _createMediaItemIndexes(DatabaseExecutor db) async {
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_media_items_library_title '
'ON media_items(library_id, title COLLATE NOCASE)',
);
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_media_items_tmdb_id '
'ON media_items(tmdb_id)',
);
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_media_items_gcid ON media_items(gcid)',
);
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_media_items_collection '
'ON media_items(library_id, collection_id)',
);
}
MediaLibraryItem _itemFromRow(Map<String, Object?> row) {
return MediaLibraryItem.fromJson({
'libraryID': row['library_id'],