#971 duplicate detection

This commit is contained in:
Thibault Deckers 2024-08-30 19:21:06 +02:00
parent 2c41e82444
commit 306025380f
23 changed files with 179 additions and 121 deletions

View file

@ -32,7 +32,7 @@ class Covers {
Covers._private();
Future<void> 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<void> clear() async {
await metadataDb.clearCovers();
await localMediaDb.clearCovers();
_rows.clear();
_entryChangeStreamController.add(null);

View file

@ -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<void> init();
@ -27,12 +27,14 @@ abstract class MetadataDb {
Future<Set<AvesEntry>> loadEntriesById(Set<int> ids);
Future<void> saveEntries(Set<AvesEntry> entries);
Future<void> insertEntries(Set<AvesEntry> entries);
Future<void> updateEntry(int id, AvesEntry entry);
Future<Set<AvesEntry>> searchLiveEntries(String query, {int? limit});
Future<Set<AvesEntry>> searchLiveDuplicates(int origin, Set<AvesEntry>? entries);
// date taken
Future<void> clearDates();

View file

@ -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<String> 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<Set<AvesEntry>> loadEntriesById(Set<int> ids) => _getByIds(ids, entryTable, AvesEntry.fromMap);
@override
Future<void> saveEntries(Set<AvesEntry> entries) async {
Future<void> insertEntries(Set<AvesEntry> 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<Set<AvesEntry>> searchLiveDuplicates(int origin, Set<AvesEntry>? 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<int?, AvesEntry>((v) => v.contentId, (prev, v) => prev != null && prev.id > v.id ? prev : v).values.toSet();
}
}
// date taken
@override

View file

@ -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

View file

@ -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);

View file

@ -15,7 +15,7 @@ class Favourites with ChangeNotifier {
Favourites._private();
Future<void> init() async {
_rows = await metadataDb.loadAllFavourites();
_rows = await localMediaDb.loadAllFavourites();
}
int get count => _rows.length;
@ -29,7 +29,7 @@ class Favourites with ChangeNotifier {
Future<void> add(Set<AvesEntry> 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<void> removeIds(Set<int> 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<void> clear() async {
await metadataDb.clearFavourites();
await localMediaDb.clearFavourites();
_rows.clear();
notifyListeners();

View file

@ -136,7 +136,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
late Map<int?, int?> _savedDates;
Future<void> loadDates() async {
_savedDates = Map.unmodifiable(await metadataDb.loadDates());
_savedDates = Map.unmodifiable(await localMediaDb.loadDates());
}
Set<CollectionFilter> _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<MoveOpEvent>(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();
}

View file

@ -24,7 +24,7 @@ mixin LocationMixin on CountryMixin, StateMixin {
List<String> sortedPlaces = List.unmodifiable([]);
Future<void> loadAddresses({Set<int>? 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();
}
}

View file

@ -57,7 +57,7 @@ class MediaStoreSource extends CollectionSource {
Future<void> _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<void> _loadVaultEntries(String? directory) async {
addEntries(await metadataDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
addEntries(await localMediaDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
}
Future<void> _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,
));
}

View file

@ -17,7 +17,7 @@ mixin TagMixin on SourceBase {
List<String> sortedTags = List.unmodifiable([]);
Future<void> loadCatalogMetadata({Set<int>? 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();
}

View file

@ -14,7 +14,7 @@ mixin TrashMixin on SourceBase {
static const Duration binKeepDuration = Duration(days: 30);
Future<void> 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;

View file

@ -23,7 +23,7 @@ class Vaults extends ChangeNotifier {
Vaults._private();
Future<void> 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<void> 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<void> 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 {

View file

@ -48,7 +48,7 @@ Future<void> _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);

View file

@ -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<p.Context>();
final AvesAvailability availability = getIt<AvesAvailability>();
final MetadataDb metadataDb = getIt<MetadataDb>();
final LocalMediaDb localMediaDb = getIt<LocalMediaDb>();
final AvesVideoControllerFactory videoControllerFactory = getIt<AvesVideoControllerFactory>();
final AvesVideoMetadataFetcher videoMetadataFetcher = getIt<AvesVideoMetadataFetcher>();
@ -54,7 +54,7 @@ final WindowService windowService = getIt<WindowService>();
void initPlatformServices() {
getIt.registerLazySingleton<p.Context>(p.Context.new);
getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
getIt.registerLazySingleton<MetadataDb>(SqfliteMetadataDb.new);
getIt.registerLazySingleton<LocalMediaDb>(SqfliteLocalMediaDb.new);
getIt.registerLazySingleton<AvesVideoControllerFactory>(MpvVideoControllerFactory.new);
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(FfmpegVideoMetadataFetcher.new);

View file

@ -29,7 +29,7 @@ Future<void> _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<List<Map<String, String?>>> _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);

View file

@ -647,10 +647,12 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Future<void> _onAnalysisCompletion() async {
debugPrint('Analysis completed');
if (_mediaStoreSource.initState != SourceInitializationState.none) {
await _mediaStoreSource.loadCatalogMetadata();
await _mediaStoreSource.loadAddresses();
_mediaStoreSource.updateDerivedFilters();
}
}
void _onError(String? error) => reportService.recordError(error, null);

View file

@ -70,7 +70,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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<DebugAppDatabaseSection> 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(() {});
}

View file

@ -245,7 +245,7 @@ class _HomePageState extends State<HomePage> {
Future<void> _initViewerEssentials() async {
// for video playback storage
await metadataDb.init();
await localMediaDb.init();
}
bool _isViewerSourceable(AvesEntry? viewerEntry) {

View file

@ -40,12 +40,12 @@ class _DbTabState extends State<DbTab> {
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<DbTab> {
},
child: const Text('Untrack entry'),
),
ElevatedButton(
onPressed: () async {
final duplicates = {entry.copyWith(id: localMediaDb.nextId)};
final source = context.read<CollectionSource>();
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<DbTab> {
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'),

View file

@ -568,9 +568,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} else if (notification is CastNotification) {
_cast(notification.enabled);
} else if (notification is FullImageLoadedNotification) {
final viewStateController = context.read<ViewStateConductor>().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<ViewStateConductor>().getController(notification.entry);
viewStateController?.fullImageNotifier.value = notification.image;
});
} else if (notification is EntryDeletedNotification) {
_onEntryRemoved(context, notification.entries);
} else if (notification is EntryMovedNotification) {

View file

@ -16,12 +16,12 @@ class DatabasePlaybackStateHandler extends PlaybackStateHandler {
@override
Future<int?> 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<void> 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});
}
}
}

View file

@ -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<Set<AvesEntry>> loadEntries({int? origin, String? directory}) => SynchronousFuture({});
@override
Future<void> saveEntries(Set<AvesEntry> entries) => SynchronousFuture(null);
Future<void> insertEntries(Set<AvesEntry> entries) => SynchronousFuture(null);
@override
Future<void> updateEntry(int id, AvesEntry entry) => SynchronousFuture(null);

View file

@ -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>(() => p.Context(style: p.Style.posix));
getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new);
getIt.registerLazySingleton<MetadataDb>(FakeMetadataDb.new);
getIt.registerLazySingleton<LocalMediaDb>(FakeAvesDb.new);
getIt.registerLazySingleton<AppService>(FakeAppService.new);
getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new);