diff --git a/lib/model/covers.dart b/lib/model/covers.dart index eadd26621..288a19f0a 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -32,7 +32,7 @@ class Covers { Covers._private(); Future init() async { - _rows = await metadataDb.loadAllCovers(); + _rows = await localMediaDb.loadAllCovers(); } int get count => _rows.length; @@ -59,7 +59,7 @@ class Covers { final oldRows = _rows.where((row) => row.filter == filter).toSet(); _rows.removeAll(oldRows); - await metadataDb.removeCovers({filter}); + await localMediaDb.removeCovers({filter}); final oldRow = oldRows.firstOrNull; final oldEntry = oldRow?.entryId; @@ -74,7 +74,7 @@ class Covers { color: color, ); _rows.add(row); - await metadataDb.addCovers({row}); + await localMediaDb.addCovers({row}); } if (oldEntry != entryId) _entryChangeStreamController.add({filter}); @@ -103,7 +103,7 @@ class Covers { } Future clear() async { - await metadataDb.clearCovers(); + await localMediaDb.clearCovers(); _rows.clear(); _entryChangeStreamController.add(null); diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db.dart similarity index 94% rename from lib/model/db/db_metadata.dart rename to lib/model/db/db.dart index c9216c3f1..2fa2b0b17 100644 --- a/lib/model/db/db_metadata.dart +++ b/lib/model/db/db.dart @@ -8,7 +8,7 @@ import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/viewer/video_playback.dart'; -abstract class MetadataDb { +abstract class LocalMediaDb { int get nextId; Future init(); @@ -27,12 +27,14 @@ abstract class MetadataDb { Future> loadEntriesById(Set ids); - Future saveEntries(Set entries); + Future insertEntries(Set entries); Future updateEntry(int id, AvesEntry entry); Future> searchLiveEntries(String query, {int? limit}); + Future> searchLiveDuplicates(int origin, Set? entries); + // date taken Future clearDates(); diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_sqflite.dart similarity index 93% rename from lib/model/db/db_metadata_sqflite.dart rename to lib/model/db/db_sqflite.dart index 9c2191a11..50ebd29f4 100644 --- a/lib/model/db/db_metadata_sqflite.dart +++ b/lib/model/db/db_sqflite.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:aves/model/covers.dart'; -import 'package:aves/model/db/db_metadata.dart'; -import 'package:aves/model/db/db_metadata_sqflite_upgrade.dart'; +import 'package:aves/model/db/db.dart'; +import 'package:aves/model/db/db_sqflite_upgrade.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/filters.dart'; @@ -16,7 +16,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; -class SqfliteMetadataDb implements MetadataDb { +class SqfliteLocalMediaDb implements LocalMediaDb { late Database _db; Future get path async => pContext.join(await getDatabasesPath(), 'metadata.db'); @@ -108,7 +108,7 @@ class SqfliteMetadataDb implements MetadataDb { ', resumeTimeMillis INTEGER' ')'); }, - onUpgrade: MetadataDbUpgrader.upgradeDb, + onUpgrade: LocalMediaDbUpgrader.upgradeDb, version: 11, ); @@ -209,7 +209,7 @@ class SqfliteMetadataDb implements MetadataDb { Future> loadEntriesById(Set ids) => _getByIds(ids, entryTable, AvesEntry.fromMap); @override - Future saveEntries(Set entries) async { + Future insertEntries(Set entries) async { if (entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final batch = _db.batch(); @@ -246,6 +246,35 @@ class SqfliteMetadataDb implements MetadataDb { return rows.map(AvesEntry.fromMap).toSet(); } + @override + Future> searchLiveDuplicates(int origin, Set? entries) async { + String where = 'origin = ? AND trashed = ?'; + if (entries != null) { + where += ' AND contentId IN (${entries.map((v) => v.contentId).join(',')})'; + } + final rows = await _db.query( + entryTable, + where: where, + whereArgs: [origin, 0], + groupBy: 'contentId', + having: 'COUNT(id) > 1', + ); + final duplicates = rows.map(AvesEntry.fromMap).toSet(); + if (duplicates.isEmpty) { + return {}; + } + + debugPrint('Found duplicates=$duplicates'); + if (entries != null) { + // return duplicates among the provided entries + final duplicateIds = duplicates.map((v) => v.id).toSet(); + return entries.where((v) => duplicateIds.contains(v.id)).toSet(); + } else { + // return latest duplicates for each content ID + return duplicates.groupFoldBy((v) => v.contentId, (prev, v) => prev != null && prev.id > v.id ? prev : v).values.toSet(); + } + } + // date taken @override diff --git a/lib/model/db/db_metadata_sqflite_upgrade.dart b/lib/model/db/db_sqflite_upgrade.dart similarity index 95% rename from lib/model/db/db_metadata_sqflite_upgrade.dart rename to lib/model/db/db_sqflite_upgrade.dart index ef0b66074..69c4055b8 100644 --- a/lib/model/db/db_metadata_sqflite_upgrade.dart +++ b/lib/model/db/db_sqflite_upgrade.dart @@ -1,18 +1,18 @@ -import 'package:aves/model/db/db_metadata_sqflite.dart'; +import 'package:aves/model/db/db_sqflite.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; -class MetadataDbUpgrader { - static const entryTable = SqfliteMetadataDb.entryTable; - static const dateTakenTable = SqfliteMetadataDb.dateTakenTable; - static const metadataTable = SqfliteMetadataDb.metadataTable; - static const addressTable = SqfliteMetadataDb.addressTable; - static const favouriteTable = SqfliteMetadataDb.favouriteTable; - static const coverTable = SqfliteMetadataDb.coverTable; - static const vaultTable = SqfliteMetadataDb.vaultTable; - static const trashTable = SqfliteMetadataDb.trashTable; - static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable; +class LocalMediaDbUpgrader { + static const entryTable = SqfliteLocalMediaDb.entryTable; + static const dateTakenTable = SqfliteLocalMediaDb.dateTakenTable; + static const metadataTable = SqfliteLocalMediaDb.metadataTable; + static const addressTable = SqfliteLocalMediaDb.addressTable; + static const favouriteTable = SqfliteLocalMediaDb.favouriteTable; + static const coverTable = SqfliteLocalMediaDb.coverTable; + static const vaultTable = SqfliteLocalMediaDb.vaultTable; + static const trashTable = SqfliteLocalMediaDb.trashTable; + static const videoPlaybackTable = SqfliteLocalMediaDb.videoPlaybackTable; // warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported // on SQLite <3.25.0, bundled on older Android devices diff --git a/lib/model/entry/entry.dart b/lib/model/entry/entry.dart index d7b91a58c..8d1e3c3d2 100644 --- a/lib/model/entry/entry.dart +++ b/lib/model/entry/entry.dart @@ -432,8 +432,8 @@ class AvesEntry with AvesEntryBase { if (isFlipped is bool) this.isFlipped = isFlipped; if (persist) { - await metadataDb.saveEntries({this}); - if (catalogMetadata != null) await metadataDb.saveCatalogMetadata({catalogMetadata!}); + await localMediaDb.updateEntry(id, this); + if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!}); } await _onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); @@ -451,7 +451,7 @@ class AvesEntry with AvesEntryBase { _tags = null; if (persist) { - await metadataDb.removeIds({id}, dataTypes: dataTypes); + await localMediaDb.removeIds({id}, dataTypes: dataTypes); } final updatedEntry = await mediaFetchService.getEntry(uri, mimeType); diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index 5c56b8ad3..19c85c78c 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -15,7 +15,7 @@ class Favourites with ChangeNotifier { Favourites._private(); Future init() async { - _rows = await metadataDb.loadAllFavourites(); + _rows = await localMediaDb.loadAllFavourites(); } int get count => _rows.length; @@ -29,7 +29,7 @@ class Favourites with ChangeNotifier { Future add(Set entries) async { final newRows = entries.map(_entryToRow).toSet(); - await metadataDb.addFavourites(newRows); + await localMediaDb.addFavourites(newRows); _rows.addAll(newRows); notifyListeners(); @@ -40,14 +40,14 @@ class Favourites with ChangeNotifier { Future removeIds(Set entryIds) async { final removedRows = _rows.where((row) => entryIds.contains(row.entryId)).toSet(); - await metadataDb.removeFavourites(removedRows); + await localMediaDb.removeFavourites(removedRows); removedRows.forEach(_rows.remove); notifyListeners(); } Future clear() async { - await metadataDb.clearFavourites(); + await localMediaDb.clearFavourites(); _rows.clear(); notifyListeners(); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 6f21d047d..e66b49601 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -136,7 +136,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place late Map _savedDates; Future loadDates() async { - _savedDates = Map.unmodifiable(await metadataDb.loadDates()); + _savedDates = Map.unmodifiable(await localMediaDb.loadDates()); } Set _getAppHiddenFilters() => { @@ -217,7 +217,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place final ids = entries.map((entry) => entry.id).toSet(); await favourites.removeIds(ids); await covers.removeIds(ids); - await metadataDb.removeIds(ids); + await localMediaDb.removeIds(ids); ids.forEach((id) => _entryById.remove); _rawEntries.removeAll(entries); @@ -278,10 +278,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place if (persist) { await covers.moveEntry(entry); final id = entry.id; - await metadataDb.updateEntry(id, entry); - await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata); - await metadataDb.updateAddress(id, entry.addressDetails); - await metadataDb.updateTrash(id, entry.trashDetails); + await localMediaDb.updateEntry(id, entry); + await localMediaDb.updateCatalogMetadata(id, entry.catalogMetadata); + await localMediaDb.updateAddress(id, entry.addressDetails); + await localMediaDb.updateTrash(id, entry.trashDetails); } } @@ -352,7 +352,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place if (sourceEntry != null) { fromAlbums.add(sourceEntry.directory); movedEntries.add(sourceEntry.copyWith( - id: metadataDb.nextId, + id: localMediaDb.nextId, uri: newFields['uri'] as String?, path: newFields['path'] as String?, contentId: newFields['contentId'] as int?, @@ -366,9 +366,9 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place debugPrint('failed to find source entry with uri=$sourceUri'); } }); - await metadataDb.saveEntries(movedEntries); - await metadataDb.saveCatalogMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet()); - await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).whereNotNull().toSet()); + await localMediaDb.insertEntries(movedEntries); + await localMediaDb.saveCatalogMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet()); + await localMediaDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).whereNotNull().toSet()); } else { await Future.forEach(movedOps, (movedOp) async { final newFields = movedOp.newFields; @@ -455,7 +455,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place await deviceService.requestGarbageCollection(); await Future.forEach(entries, (entry) async { await entry.catalog(background: background, force: dataTypes.contains(EntryDataType.catalog), persist: persist); - await metadataDb.updateCatalogMetadata(entry.id, entry.catalogMetadata); + await localMediaDb.updateCatalogMetadata(entry.id, entry.catalogMetadata); }); onCatalogMetadataChanged(); } @@ -463,7 +463,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place if (dataTypes.contains(EntryDataType.address)) { await Future.forEach(entries, (entry) async { await entry.locate(background: background, force: dataTypes.contains(EntryDataType.address), geocoderLocale: settings.appliedLocale); - await metadataDb.updateAddress(entry.id, entry.addressDetails); + await localMediaDb.updateAddress(entry.id, entry.addressDetails); }); onAddressMetadataChanged(); } diff --git a/lib/model/source/location/location.dart b/lib/model/source/location/location.dart index c8b5edac4..bfa00b1fb 100644 --- a/lib/model/source/location/location.dart +++ b/lib/model/source/location/location.dart @@ -24,7 +24,7 @@ mixin LocationMixin on CountryMixin, StateMixin { List sortedPlaces = List.unmodifiable([]); Future loadAddresses({Set? ids}) async { - final saved = await (ids != null ? metadataDb.loadAddressesById(ids) : metadataDb.loadAddresses()); + final saved = await (ids != null ? localMediaDb.loadAddressesById(ids) : localMediaDb.loadAddresses()); final idMap = entryById; saved.forEach((metadata) => idMap[metadata.id]?.addressDetails = metadata); invalidateEntries(); @@ -37,7 +37,7 @@ mixin LocationMixin on CountryMixin, StateMixin { final unlocatedIds = candidateEntries.where((entry) => !entry.hasGps).map((entry) => entry.id).toSet(); if (unlocatedIds.isNotEmpty) { - await metadataDb.removeIds(unlocatedIds, dataTypes: {EntryDataType.address}); + await localMediaDb.removeIds(unlocatedIds, dataTypes: {EntryDataType.address}); onAddressMetadataChanged(); } } @@ -71,7 +71,7 @@ mixin LocationMixin on CountryMixin, StateMixin { setProgress(done: ++progressDone, total: progressTotal); }); if (newAddresses.isNotEmpty) { - await metadataDb.saveAddresses(Set.unmodifiable(newAddresses)); + await localMediaDb.saveAddresses(Set.unmodifiable(newAddresses)); onAddressMetadataChanged(); } } @@ -129,7 +129,7 @@ mixin LocationMixin on CountryMixin, StateMixin { if (entry.hasFineAddress) { newAddresses.add(entry.addressDetails!); if (newAddresses.length >= commitCountThreshold) { - await metadataDb.saveAddresses(Set.unmodifiable(newAddresses)); + await localMediaDb.saveAddresses(Set.unmodifiable(newAddresses)); onAddressMetadataChanged(); newAddresses.clear(); } @@ -141,7 +141,7 @@ mixin LocationMixin on CountryMixin, StateMixin { setProgress(done: ++progressDone, total: progressTotal); } if (newAddresses.isNotEmpty) { - await metadataDb.saveAddresses(Set.unmodifiable(newAddresses)); + await localMediaDb.saveAddresses(Set.unmodifiable(newAddresses)); onAddressMetadataChanged(); } } diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index b8e808001..df927f831 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -57,7 +57,7 @@ class MediaStoreSource extends CollectionSource { Future _loadEssentials() async { final stopwatch = Stopwatch()..start(); state = SourceState.loading; - await metadataDb.init(); + await localMediaDb.init(); await vaults.init(); await favourites.init(); await covers.init(); @@ -67,8 +67,8 @@ class MediaStoreSource extends CollectionSource { if (currentTimeZoneOffset != catalogTimeZoneOffset) { // clear catalog metadata to get correct date/times when moving to a different time zone debugPrint('$runtimeType clear catalog metadata to get correct date/times'); - await metadataDb.clearDates(); - await metadataDb.clearCatalogMetadata(); + await localMediaDb.clearDates(); + await localMediaDb.clearCatalogMetadata(); settings.catalogTimeZoneRawOffsetMillis = currentTimeZoneOffset; } } @@ -92,13 +92,13 @@ class MediaStoreSource extends CollectionSource { final topIds = settings.topEntryIds?.toSet(); if (topIds != null) { debugPrint('$runtimeType refresh ${stopwatch.elapsed} load ${topIds.length} top entries'); - topEntries.addAll(await metadataDb.loadEntriesById(topIds)); + topEntries.addAll(await localMediaDb.loadEntriesById(topIds)); addEntries(topEntries); } } debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch known entries'); - final knownEntries = await metadataDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: directory); + final knownEntries = await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: directory); final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet(); debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete entries'); @@ -146,7 +146,7 @@ class MediaStoreSource extends CollectionSource { // clean up obsolete entries if (removedEntries.isNotEmpty) { debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries'); - await metadataDb.removeIds(removedEntries.map((entry) => entry.id).toSet()); + await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet()); } // verify paths because some apps move files without updating their `last modified date` @@ -185,7 +185,7 @@ class MediaStoreSource extends CollectionSource { // reuse known entry ID to overwrite it while preserving favourites, etc. final contentId = entry.contentId; final existingEntry = knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId) : null; - entry.id = existingEntry?.id ?? metadataDb.nextId; + entry.id = existingEntry?.id ?? localMediaDb.nextId; pendingNewEntries.add(entry); if (pendingNewEntries.length >= refreshCount) { @@ -198,7 +198,13 @@ class MediaStoreSource extends CollectionSource { if (allNewEntries.isNotEmpty) { debugPrint('$runtimeType refresh ${stopwatch.elapsed} save new entries'); - await metadataDb.saveEntries(allNewEntries); + await localMediaDb.insertEntries(allNewEntries); + + // TODO TLAD [971] check duplicates + final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, allNewEntries); + if (duplicates.isNotEmpty) { + unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}'), StackTrace.current)); + } // new entries include existing entries with obsolete paths // so directories may be added, but also removed or simply have their content summary changed @@ -276,7 +282,7 @@ class MediaStoreSource extends CollectionSource { if (existingEntry != null) { entriesToRefresh.add(existingEntry); } else { - sourceEntry.id = metadataDb.nextId; + sourceEntry.id = localMediaDb.nextId; newEntries.add(sourceEntry); } final existingDirectory = existingEntry?.directory; @@ -304,7 +310,14 @@ class MediaStoreSource extends CollectionSource { if (newEntries.isNotEmpty) { addEntries(newEntries); - await metadataDb.saveEntries(newEntries); + await localMediaDb.insertEntries(newEntries); + + // TODO TLAD [971] check duplicates + final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries); + if (duplicates.isNotEmpty) { + unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}'), StackTrace.current)); + } + await analyze(analysisController, entries: newEntries); } @@ -346,7 +359,7 @@ class MediaStoreSource extends CollectionSource { // vault Future _loadVaultEntries(String? directory) async { - addEntries(await metadataDb.loadEntries(origin: EntryOrigins.vault, directory: directory)); + addEntries(await localMediaDb.loadEntries(origin: EntryOrigins.vault, directory: directory)); } Future _refreshVaultEntries({ @@ -367,7 +380,7 @@ class MediaStoreSource extends CollectionSource { final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true); if (sourceEntry != null) { newEntries.add(sourceEntry.copyWith( - id: metadataDb.nextId, + id: localMediaDb.nextId, origin: EntryOrigins.vault, )); } diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 537ee0c53..efd96d26e 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -17,7 +17,7 @@ mixin TagMixin on SourceBase { List sortedTags = List.unmodifiable([]); Future loadCatalogMetadata({Set? ids}) async { - final saved = await (ids != null ? metadataDb.loadCatalogMetadataById(ids) : metadataDb.loadCatalogMetadata()); + final saved = await (ids != null ? localMediaDb.loadCatalogMetadataById(ids) : localMediaDb.loadCatalogMetadata()); final idMap = entryById; saved.forEach((metadata) => idMap[metadata.id]?.catalogMetadata = metadata); invalidateEntries(); @@ -48,7 +48,7 @@ mixin TagMixin on SourceBase { if (entry.isCatalogued) { newMetadata.add(entry.catalogMetadata!); if (newMetadata.length >= commitCountThreshold) { - await metadataDb.saveCatalogMetadata(Set.unmodifiable(newMetadata)); + await localMediaDb.saveCatalogMetadata(Set.unmodifiable(newMetadata)); onCatalogMetadataChanged(); newMetadata.clear(); } @@ -59,7 +59,7 @@ mixin TagMixin on SourceBase { } setProgress(done: ++progressDone, total: progressTotal); } - await metadataDb.saveCatalogMetadata(Set.unmodifiable(newMetadata)); + await localMediaDb.saveCatalogMetadata(Set.unmodifiable(newMetadata)); onCatalogMetadataChanged(); } diff --git a/lib/model/source/trash.dart b/lib/model/source/trash.dart index 98393b0b1..03062d80d 100644 --- a/lib/model/source/trash.dart +++ b/lib/model/source/trash.dart @@ -14,7 +14,7 @@ mixin TrashMixin on SourceBase { static const Duration binKeepDuration = Duration(days: 30); Future loadTrashDetails() async { - final saved = await metadataDb.loadAllTrashDetails(); + final saved = await localMediaDb.loadAllTrashDetails(); final idMap = entryById; saved.forEach((details) => idMap[details.id]?.trashDetails = details); } @@ -63,13 +63,13 @@ mixin TrashMixin on SourceBase { entry.trashed = true; entry.trashDetails = _buildTrashDetails(id); // persist - await metadataDb.updateEntry(id, entry); - await metadataDb.updateTrash(id, entry.trashDetails); + await localMediaDb.updateEntry(id, entry); + await localMediaDb.updateTrash(id, entry.trashDetails); } else { // there is no matching entry final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true); if (sourceEntry != null) { - final id = metadataDb.nextId; + final id = localMediaDb.nextId; sourceEntry.id = id; sourceEntry.path = pContext.join(recoveryPath, pContext.basename(untrackedPath)); sourceEntry.trashed = true; diff --git a/lib/model/vaults/vaults.dart b/lib/model/vaults/vaults.dart index 1491a9ec0..651f08614 100644 --- a/lib/model/vaults/vaults.dart +++ b/lib/model/vaults/vaults.dart @@ -23,7 +23,7 @@ class Vaults extends ChangeNotifier { Vaults._private(); Future init() async { - _rows = await metadataDb.loadAllVaults(); + _rows = await localMediaDb.loadAllVaults(); _vaultDirPaths = null; final screenStateStream = Platform.isAndroid ? AvesScreenState().screenStateStream : null; if (screenStateStream != null) { @@ -44,7 +44,7 @@ class Vaults extends ChangeNotifier { VaultDetails? detailsForPath(String dirPath) => _rows.firstWhereOrNull((v) => v.path == dirPath); Future create(VaultDetails details) async { - await metadataDb.addVaults({details}); + await localMediaDb.addVaults({details}); _rows.add(details); _vaultDirPaths = null; @@ -56,7 +56,7 @@ class Vaults extends ChangeNotifier { final details = dirPaths.map(detailsForPath).whereNotNull().toSet(); if (details.isEmpty) return; - await metadataDb.removeVaults(details); + await localMediaDb.removeVaults(details); await Future.forEach(details, (v) => securityService.writeValue(v.passKey, null)); @@ -74,7 +74,7 @@ class Vaults extends ChangeNotifier { if (newName == null) return; final newDetails = oldDetails.copyWith(name: newName); - await metadataDb.updateVault(oldDetails.name, newDetails); + await localMediaDb.updateVault(oldDetails.name, newDetails); final pass = await securityService.readValue(oldDetails.passKey); if (pass != null) { @@ -96,7 +96,7 @@ class Vaults extends ChangeNotifier { final oldDetails = detailsForPath(newDetails.path); if (oldDetails == null) return; - await metadataDb.updateVault(newDetails.name, newDetails); + await localMediaDb.updateVault(newDetails.name, newDetails); _rows ..remove(oldDetails) @@ -104,7 +104,7 @@ class Vaults extends ChangeNotifier { } Future clear() async { - await metadataDb.clearVaults(); + await localMediaDb.clearVaults(); _rows.clear(); _vaultDirPaths = null; } @@ -146,7 +146,7 @@ class Vaults extends ChangeNotifier { final newEntries = await recoverUntrackedItems(source, dirPath); if (newEntries.isNotEmpty) { source.addEntries(newEntries); - await metadataDb.saveEntries(newEntries); + await localMediaDb.insertEntries(newEntries); unawaited(source.analyze(null, entries: newEntries)); } @@ -168,7 +168,7 @@ class Vaults extends ChangeNotifier { final uri = Uri.file(untrackedPath).toString(); final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true); if (sourceEntry != null) { - sourceEntry.id = metadataDb.nextId; + sourceEntry.id = localMediaDb.nextId; sourceEntry.origin = EntryOrigins.vault; newEntries.add(sourceEntry); } else { diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index da6042143..3460d890b 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -48,7 +48,7 @@ Future _init() async { WidgetsFlutterBinding.ensureInitialized(); initPlatformServices(); await androidFileUtils.init(); - await metadataDb.init(); + await localMediaDb.init(); await device.init(); await mobileServices.init(); await settings.init(monitorPlatformSettings: false); diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index 905ab96c0..e66851f7d 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -1,6 +1,6 @@ import 'package:aves/model/availability.dart'; -import 'package:aves/model/db/db_metadata.dart'; -import 'package:aves/model/db/db_metadata_sqflite.dart'; +import 'package:aves/model/db/db.dart'; +import 'package:aves/model/db/db_sqflite.dart'; import 'package:aves/model/settings/store_shared_pref.dart'; import 'package:aves/services/app_service.dart'; import 'package:aves/services/device_service.dart'; @@ -32,7 +32,7 @@ final SettingsStore settingsStore = SharedPrefSettingsStore(); final p.Context pContext = getIt(); final AvesAvailability availability = getIt(); -final MetadataDb metadataDb = getIt(); +final LocalMediaDb localMediaDb = getIt(); final AvesVideoControllerFactory videoControllerFactory = getIt(); final AvesVideoMetadataFetcher videoMetadataFetcher = getIt(); @@ -54,7 +54,7 @@ final WindowService windowService = getIt(); void initPlatformServices() { getIt.registerLazySingleton(p.Context.new); getIt.registerLazySingleton(LiveAvesAvailability.new); - getIt.registerLazySingleton(SqfliteMetadataDb.new); + getIt.registerLazySingleton(SqfliteLocalMediaDb.new); getIt.registerLazySingleton(MpvVideoControllerFactory.new); getIt.registerLazySingleton(FfmpegVideoMetadataFetcher.new); diff --git a/lib/services/global_search.dart b/lib/services/global_search.dart index c1b03e950..a7b4d9ac6 100644 --- a/lib/services/global_search.dart +++ b/lib/services/global_search.dart @@ -29,7 +29,7 @@ Future _init() async { // service initialization for path context, database initPlatformServices(); - await metadataDb.init(); + await localMediaDb.init(); // `intl` initialization for date formatting await initializeDateFormatting(); @@ -55,8 +55,8 @@ Future>> _getSuggestions(dynamic args) async { debugPrint('getSuggestions query=$query, locale=$locale use24hour=$use24hour'); if (query is String && locale is String) { - final entries = (await metadataDb.searchLiveEntries(query, limit: 9)).toList(); - final catalogMetadata = await metadataDb.loadCatalogMetadataById(entries.map((entry) => entry.id).toSet()); + final entries = (await localMediaDb.searchLiveEntries(query, limit: 9)).toList(); + final catalogMetadata = await localMediaDb.loadCatalogMetadataById(entries.map((entry) => entry.id).toSet()); catalogMetadata.forEach((metadata) => entries.firstWhereOrNull((entry) => entry.id == metadata.id)?.catalogMetadata = metadata); entries.sort(AvesEntrySort.compareByDate); diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 43ccea299..a198c41af 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -647,9 +647,11 @@ class _AvesAppState extends State with WidgetsBindingObserver { Future _onAnalysisCompletion() async { debugPrint('Analysis completed'); - await _mediaStoreSource.loadCatalogMetadata(); - await _mediaStoreSource.loadAddresses(); - _mediaStoreSource.updateDerivedFilters(); + if (_mediaStoreSource.initState != SourceInitializationState.none) { + await _mediaStoreSource.loadCatalogMetadata(); + await _mediaStoreSource.loadAddresses(); + _mediaStoreSource.updateDerivedFilters(); + } } void _onError(String? error) => reportService.recordError(error, null); diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 541d66321..39d81351d 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -70,7 +70,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.reset().then((_) => _reload()), + onPressed: () => localMediaDb.reset().then((_) => _reload()), child: const Text('Reset'), ), ], @@ -93,7 +93,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearEntries().then((_) => _reload()), + onPressed: () => localMediaDb.clearEntries().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -114,7 +114,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearDates().then((_) => _reload()), + onPressed: () => localMediaDb.clearDates().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -135,7 +135,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _reload()), + onPressed: () => localMediaDb.clearCatalogMetadata().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -156,7 +156,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearAddresses().then((_) => _reload()), + onPressed: () => localMediaDb.clearAddresses().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -177,7 +177,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearTrashDetails().then((_) => _reload()), + onPressed: () => localMediaDb.clearTrashDetails().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -261,7 +261,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearVideoPlayback().then((_) => _reload()), + onPressed: () => localMediaDb.clearVideoPlayback().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -281,16 +281,16 @@ class _DebugAppDatabaseSectionState extends State with } void _startDbReport() { - _dbFileSizeLoader = metadataDb.dbFileSize(); - _dbEntryLoader = metadataDb.loadEntries(); - _dbDateLoader = metadataDb.loadDates(); - _dbMetadataLoader = metadataDb.loadCatalogMetadata(); - _dbAddressLoader = metadataDb.loadAddresses(); - _dbTrashLoader = metadataDb.loadAllTrashDetails(); - _dbVaultsLoader = metadataDb.loadAllVaults(); - _dbFavouritesLoader = metadataDb.loadAllFavourites(); - _dbCoversLoader = metadataDb.loadAllCovers(); - _dbVideoPlaybackLoader = metadataDb.loadAllVideoPlayback(); + _dbFileSizeLoader = localMediaDb.dbFileSize(); + _dbEntryLoader = localMediaDb.loadEntries(); + _dbDateLoader = localMediaDb.loadDates(); + _dbMetadataLoader = localMediaDb.loadCatalogMetadata(); + _dbAddressLoader = localMediaDb.loadAddresses(); + _dbTrashLoader = localMediaDb.loadAllTrashDetails(); + _dbVaultsLoader = localMediaDb.loadAllVaults(); + _dbFavouritesLoader = localMediaDb.loadAllFavourites(); + _dbCoversLoader = localMediaDb.loadAllCovers(); + _dbVideoPlaybackLoader = localMediaDb.loadAllVideoPlayback(); setState(() {}); } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index f5c176819..84e57c7a0 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -245,7 +245,7 @@ class _HomePageState extends State { Future _initViewerEssentials() async { // for video playback storage - await metadataDb.init(); + await localMediaDb.init(); } bool _isViewerSourceable(AvesEntry? viewerEntry) { diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index ef99c82ca..a6a76a855 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -40,12 +40,12 @@ class _DbTabState extends State { void _loadDatabase() { final id = entry.id; - _dbDateLoader = metadataDb.loadDates().then((values) => values[id]); - _dbEntryLoader = metadataDb.loadEntriesById({id}).then((values) => values.firstOrNull); - _dbMetadataLoader = metadataDb.loadCatalogMetadata().then((values) => values.firstWhereOrNull((row) => row.id == id)); - _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.id == id)); - _dbTrashDetailsLoader = metadataDb.loadAllTrashDetails().then((values) => values.firstWhereOrNull((row) => row.id == id)); - _dbVideoPlaybackLoader = metadataDb.loadVideoPlayback(id); + _dbDateLoader = localMediaDb.loadDates().then((values) => values[id]); + _dbEntryLoader = localMediaDb.loadEntriesById({id}).then((values) => values.firstOrNull); + _dbMetadataLoader = localMediaDb.loadCatalogMetadata().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbAddressLoader = localMediaDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbTrashDetailsLoader = localMediaDb.loadAllTrashDetails().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbVideoPlaybackLoader = localMediaDb.loadVideoPlayback(id); setState(() {}); } @@ -93,6 +93,15 @@ class _DbTabState extends State { }, child: const Text('Untrack entry'), ), + ElevatedButton( + onPressed: () async { + final duplicates = {entry.copyWith(id: localMediaDb.nextId)}; + final source = context.read(); + source.addEntries(duplicates); + await localMediaDb.insertEntries(duplicates); + }, + child: const Text('Duplicate entry'), + ), InfoRowGroup( info: { 'uri': data.uri, @@ -184,7 +193,7 @@ class _DbTabState extends State { ElevatedButton( onPressed: () async { entry.trashDetails = null; - await metadataDb.updateTrash(entry.id, entry.trashDetails); + await localMediaDb.updateTrash(entry.id, entry.trashDetails); _loadDatabase(); }, child: const Text('Remove details'), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 850d2e9de..9cdd65722 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -568,9 +568,12 @@ class _EntryViewerStackState extends State with EntryViewContr } else if (notification is CastNotification) { _cast(notification.enabled); } else if (notification is FullImageLoadedNotification) { - final viewStateController = context.read().getOrCreateController(notification.entry); // microtask so that listeners do not trigger during build - scheduleMicrotask(() => viewStateController.fullImageNotifier.value = notification.image); + scheduleMicrotask(() { + if (!mounted) return; + final viewStateController = context.read().getController(notification.entry); + viewStateController?.fullImageNotifier.value = notification.image; + }); } else if (notification is EntryDeletedNotification) { _onEntryRemoved(context, notification.entries); } else if (notification is EntryMovedNotification) { diff --git a/lib/widgets/viewer/video/db_playback_state_handler.dart b/lib/widgets/viewer/video/db_playback_state_handler.dart index c862ccc0b..15be982da 100644 --- a/lib/widgets/viewer/video/db_playback_state_handler.dart +++ b/lib/widgets/viewer/video/db_playback_state_handler.dart @@ -16,12 +16,12 @@ class DatabasePlaybackStateHandler extends PlaybackStateHandler { @override Future getResumeTime({required int entryId, required BuildContext context}) async { - final playback = await metadataDb.loadVideoPlayback(entryId); + final playback = await localMediaDb.loadVideoPlayback(entryId); final resumeTime = playback?.resumeTimeMillis ?? 0; if (resumeTime == 0) return null; // clear on retrieval - await metadataDb.removeVideoPlayback({entryId}); + await localMediaDb.removeVideoPlayback({entryId}); switch (settings.videoResumptionMode) { case VideoResumptionMode.never: @@ -54,14 +54,14 @@ class DatabasePlaybackStateHandler extends PlaybackStateHandler { @override Future saveResumeTime({required int entryId, required int position, required double progress}) async { if (resumeTimeSaveMinProgress < progress && progress < resumeTimeSaveMaxProgress) { - await metadataDb.addVideoPlayback({ + await localMediaDb.addVideoPlayback({ VideoPlaybackRow( entryId: entryId, resumeTimeMillis: position, ) }); } else { - await metadataDb.removeVideoPlayback({entryId}); + await localMediaDb.removeVideoPlayback({entryId}); } } } diff --git a/test/fake/metadata_db.dart b/test/fake/db.dart similarity index 94% rename from test/fake/metadata_db.dart rename to test/fake/db.dart index 9f38ee70e..c7c113f4a 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/db.dart @@ -1,5 +1,5 @@ import 'package:aves/model/covers.dart'; -import 'package:aves/model/db/db_metadata.dart'; +import 'package:aves/model/db/db.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/filters.dart'; @@ -10,7 +10,7 @@ import 'package:aves/model/vaults/details.dart'; import 'package:flutter/foundation.dart'; import 'package:test/fake.dart'; -class FakeMetadataDb extends Fake implements MetadataDb { +class FakeAvesDb extends Fake implements LocalMediaDb { static int _lastId = 0; @override @@ -28,7 +28,7 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future> loadEntries({int? origin, String? directory}) => SynchronousFuture({}); @override - Future saveEntries(Set entries) => SynchronousFuture(null); + Future insertEntries(Set entries) => SynchronousFuture(null); @override Future updateEntry(int id, AvesEntry entry) => SynchronousFuture(null); diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 994f42d3b..90fd30089 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/availability.dart'; import 'package:aves/model/covers.dart'; -import 'package:aves/model/db/db_metadata.dart'; +import 'package:aves/model/db/db.dart'; import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; @@ -35,7 +35,7 @@ import '../fake/availability.dart'; import '../fake/device_service.dart'; import '../fake/media_fetch_service.dart'; import '../fake/media_store_service.dart'; -import '../fake/metadata_db.dart'; +import '../fake/db.dart'; import '../fake/metadata_fetch_service.dart'; import '../fake/report_service.dart'; import '../fake/storage_service.dart'; @@ -58,7 +58,7 @@ void main() { // specify Posix style path context for consistent behaviour when running tests on Windows getIt.registerLazySingleton(() => p.Context(style: p.Style.posix)); getIt.registerLazySingleton(FakeAvesAvailability.new); - getIt.registerLazySingleton(FakeMetadataDb.new); + getIt.registerLazySingleton(FakeAvesDb.new); getIt.registerLazySingleton(FakeAppService.new); getIt.registerLazySingleton(FakeDeviceService.new);