mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-26 00:44:42 +01:00
1014 lines
29 KiB
Dart
1014 lines
29 KiB
Dart
import 'dart:io' as io;
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:np_async/np_async.dart';
|
|
import 'package:np_codegen/np_codegen.dart';
|
|
import 'package:np_collection/np_collection.dart';
|
|
import 'package:np_common/object_util.dart';
|
|
import 'package:np_common/or_null.dart';
|
|
import 'package:np_common/type.dart';
|
|
import 'package:np_datetime/np_datetime.dart';
|
|
import 'package:np_db/np_db.dart';
|
|
import 'package:np_db_sqlite/src/converter.dart';
|
|
import 'package:np_db_sqlite/src/database.dart';
|
|
import 'package:np_db_sqlite/src/database_extension.dart';
|
|
import 'package:np_db_sqlite/src/isolate_util.dart';
|
|
import 'package:np_db_sqlite/src/table.dart';
|
|
import 'package:np_db_sqlite/src/util.dart';
|
|
import 'package:np_platform_util/np_platform_util.dart';
|
|
|
|
part 'sqlite_api.g.dart';
|
|
|
|
@npLog
|
|
class NpDbSqlite implements NpDb {
|
|
NpDbSqlite();
|
|
|
|
@override
|
|
Future<void> initMainIsolate({
|
|
required int? androidSdk,
|
|
}) async {
|
|
initDrift();
|
|
if (getRawPlatform() == NpPlatform.android && androidSdk! < 24) {
|
|
_log.info("[initMainIsolate] Workaround Android 6- bug");
|
|
// see: https://github.com/flutter/flutter/issues/73318 and
|
|
// https://github.com/simolus3/drift/issues/895
|
|
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
|
}
|
|
|
|
// use driftIsolate to prevent DB blocking the UI thread
|
|
if (getRawPlatform() == NpPlatform.web) {
|
|
// no isolate support on web
|
|
_db = SqliteDb();
|
|
} else {
|
|
_db = await createDb();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> initBackgroundIsolate({
|
|
required int? androidSdk,
|
|
}) async {
|
|
initDrift();
|
|
// service already runs in an isolate
|
|
_db = SqliteDb();
|
|
}
|
|
|
|
@visibleForTesting
|
|
Future<void> initWithDb({
|
|
required SqliteDb db,
|
|
}) async {
|
|
initDrift();
|
|
_db = db;
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() {
|
|
return _db.close();
|
|
}
|
|
|
|
@override
|
|
Future<io.File> export(io.Directory dir) => exportSqliteDb(_db, dir);
|
|
|
|
@override
|
|
Future<U> compute<T, U>(NpDbComputeCallback<T, U> callback, T args) {
|
|
return _db.isolate(args, (db, message) async {
|
|
final that = NpDbSqlite();
|
|
await that.initWithDb(db: db);
|
|
return callback(that, message);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> addAccounts(List<DbAccount> accounts) {
|
|
return _db.use((db) async {
|
|
await db.insertAccounts(accounts);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> clearAndInitWithAccounts(List<DbAccount> accounts) {
|
|
return _db.use((db) async {
|
|
await db.truncate();
|
|
await db.insertAccounts(accounts);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteAccount(DbAccount account) {
|
|
return _db.use((db) async {
|
|
await db.deleteAccount(account);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbAlbum>> getAlbumsByAlbumFileIds({
|
|
required DbAccount account,
|
|
required List<int> fileIds,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryAlbumsByAlbumFileIds(
|
|
account: ByAccount.db(account),
|
|
fileIds: fileIds,
|
|
);
|
|
});
|
|
return sqlObjs.toDbAlbums();
|
|
}
|
|
|
|
@override
|
|
Future<void> syncAlbum({
|
|
required DbAccount account,
|
|
required DbFile albumFile,
|
|
required DbAlbum album,
|
|
}) async {
|
|
final sqlAlbum = AlbumConverter.toSql(album);
|
|
await _db.use((db) async {
|
|
await db.syncAlbum(
|
|
account: ByAccount.db(account),
|
|
albumFileEtag: albumFile.etag,
|
|
obj: sqlAlbum,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFaceRecognitionPerson>> getFaceRecognitionPersons({
|
|
required DbAccount account,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryFaceRecognitionPersons(
|
|
account: ByAccount.db(account),
|
|
);
|
|
});
|
|
return sqlObjs.toDbFaceRecognitionPersons();
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFaceRecognitionPerson>> searchFaceRecognitionPersonsByName({
|
|
required DbAccount account,
|
|
required String name,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.searchFaceRecognitionPersonByName(
|
|
account: ByAccount.db(account),
|
|
name: name,
|
|
);
|
|
});
|
|
return sqlObjs.toDbFaceRecognitionPersons();
|
|
}
|
|
|
|
@override
|
|
Future<DbSyncResult> syncFaceRecognitionPersons({
|
|
required DbAccount account,
|
|
required List<DbFaceRecognitionPerson> persons,
|
|
}) async {
|
|
int sorter(DbFaceRecognitionPerson a, DbFaceRecognitionPerson b) =>
|
|
a.name.compareTo(b.name);
|
|
final to = persons.sorted(sorter);
|
|
return await _db.use((db) async {
|
|
final sqlObjs = await db.queryFaceRecognitionPersons(
|
|
account: ByAccount.db(account),
|
|
);
|
|
final from =
|
|
sqlObjs.map(FaceRecognitionPersonConverter.fromSql).sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info(
|
|
"[replaceFaceRecognitionPersons] New persons: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info(
|
|
"[replaceFaceRecognitionPersons] Removed persons: ${deletes.toReadableString()}");
|
|
final updates = to.where((t) {
|
|
final f = from.firstWhereOrNull((e) => e.name == t.name);
|
|
return f != null && f != t;
|
|
}).toList();
|
|
_log.info(
|
|
"[replaceFaceRecognitionPersons] Updated persons: ${updates.toReadableString()}");
|
|
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
|
await db.replaceFaceRecognitionPersons(
|
|
account: ByAccount.db(account),
|
|
inserts: inserts,
|
|
deletes: deletes,
|
|
updates: updates,
|
|
);
|
|
}
|
|
return DbSyncResult(
|
|
insert: inserts.length,
|
|
delete: deletes.length,
|
|
update: updates.length,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFile>> getFilesByDirKey({
|
|
required DbAccount account,
|
|
required DbFileKey dir,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryFilesByDirKey(
|
|
account: ByAccount.db(account),
|
|
dir: dir,
|
|
);
|
|
});
|
|
return sqlObjs.toDbFiles();
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFile>> getFilesByDirKeyAndLocation({
|
|
required DbAccount account,
|
|
required String dirRelativePath,
|
|
required String? place,
|
|
required String countryCode,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryFilesByLocation(
|
|
account: ByAccount.db(account),
|
|
dirRelativePath: dirRelativePath,
|
|
place: place,
|
|
countryCode: countryCode,
|
|
);
|
|
});
|
|
return sqlObjs.toDbFiles();
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFile>> getFilesByFileIds({
|
|
required DbAccount account,
|
|
required Iterable<int> fileIds,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryFilesByFileIds(
|
|
account: ByAccount.db(account),
|
|
fileIds: fileIds,
|
|
);
|
|
});
|
|
return sqlObjs.toDbFiles();
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFile>> getFilesByTimeRange({
|
|
required DbAccount account,
|
|
required List<String> dirRoots,
|
|
required TimeRange range,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryFilesByTimeRange(
|
|
account: ByAccount.db(account),
|
|
dirRoots: dirRoots,
|
|
range: range,
|
|
);
|
|
});
|
|
return sqlObjs.toDbFiles();
|
|
}
|
|
|
|
@override
|
|
Future<void> updateFileByFileId({
|
|
required DbAccount account,
|
|
required int fileId,
|
|
String? relativePath,
|
|
OrNull<bool>? isFavorite,
|
|
OrNull<bool>? isArchived,
|
|
OrNull<DateTime>? overrideDateTime,
|
|
DateTime? bestDateTime,
|
|
OrNull<DbImageData>? imageData,
|
|
OrNull<DbLocation>? location,
|
|
}) async {
|
|
await _db.use((db) async {
|
|
await db.updateFileByFileId(
|
|
account: ByAccount.db(account),
|
|
fileId: fileId,
|
|
relativePath: relativePath,
|
|
isFavorite: isFavorite,
|
|
isArchived: isArchived,
|
|
overrideDateTime: overrideDateTime,
|
|
bestDateTime: bestDateTime,
|
|
imageData: imageData,
|
|
location: location,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> updateFilesByFileIds({
|
|
required DbAccount account,
|
|
required List<int> fileIds,
|
|
OrNull<bool>? isFavorite,
|
|
OrNull<bool>? isArchived,
|
|
}) async {
|
|
await _db.use((db) async {
|
|
await db.updateFilesByFileIds(
|
|
account: ByAccount.db(account),
|
|
fileIds: fileIds,
|
|
isFavorite: isFavorite,
|
|
isArchived: isArchived,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> syncDirFiles({
|
|
required DbAccount account,
|
|
required DbFileKey dirFile,
|
|
required List<DbFile> files,
|
|
}) async {
|
|
final sqlFiles = await files.toSql();
|
|
await _db.use((db) async {
|
|
await db.syncDirFiles(
|
|
account: ByAccount.db(account),
|
|
dirFile: dirFile,
|
|
objs: sqlFiles,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> syncFile({
|
|
required DbAccount account,
|
|
required DbFile file,
|
|
}) async {
|
|
final sqlFile = FileConverter.toSql(file);
|
|
await _db.use((db) async {
|
|
await db.syncFile(
|
|
account: ByAccount.db(account),
|
|
obj: sqlFile,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<DbSyncIdResult> syncFavoriteFiles({
|
|
required DbAccount account,
|
|
required List<int> favoriteFileIds,
|
|
}) async {
|
|
int sorter(int a, int b) => a.compareTo(b);
|
|
final to = favoriteFileIds.sorted(sorter);
|
|
return await _db.use((db) async {
|
|
final sqlObjs = await db.queryFileIds(
|
|
account: ByAccount.db(account),
|
|
isFavorite: true,
|
|
);
|
|
final from = sqlObjs.sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info(
|
|
"[syncFavoriteFiles] New favorites: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info(
|
|
"[syncFavoriteFiles] Removed favorites: ${deletes.toReadableString()}");
|
|
if (inserts.isNotEmpty) {
|
|
await db.updateFilesByFileIds(
|
|
account: ByAccount.db(account),
|
|
fileIds: inserts,
|
|
isFavorite: const OrNull(true),
|
|
);
|
|
}
|
|
if (deletes.isNotEmpty) {
|
|
await db.updateFilesByFileIds(
|
|
account: ByAccount.db(account),
|
|
fileIds: deletes,
|
|
isFavorite: const OrNull(false),
|
|
);
|
|
}
|
|
return DbSyncIdResult(
|
|
insert: inserts,
|
|
delete: deletes,
|
|
update: const [],
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<int> countFilesByMissingMetadata({
|
|
required DbAccount account,
|
|
required List<String> mimes,
|
|
required String ownerId,
|
|
}) async {
|
|
return _db.use((db) async {
|
|
return await db.countFiles(
|
|
account: ByAccount.db(account),
|
|
isMissingMetadata: true,
|
|
mimes: mimes,
|
|
ownerId: ownerId,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteFile({
|
|
required DbAccount account,
|
|
required DbFileKey file,
|
|
}) async {
|
|
await _db.use((db) async {
|
|
return await db.deleteFile(
|
|
account: ByAccount.db(account),
|
|
file: file,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<Map<int, String>> getDirFileIdToEtagByLikeRelativePath({
|
|
required DbAccount account,
|
|
required String relativePath,
|
|
}) async {
|
|
return await _db.use((db) async {
|
|
return await db.getDirFileIdToEtagByLikeRelativePath(
|
|
account: ByAccount.db(account),
|
|
relativePath: relativePath,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> truncateDir({
|
|
required DbAccount account,
|
|
required DbFileKey dir,
|
|
}) async {
|
|
await _db.use((db) async {
|
|
return await db.truncateDir(
|
|
account: ByAccount.db(account),
|
|
dir: dir,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbFileDescriptor>> getFileDescriptors({
|
|
required DbAccount account,
|
|
List<int>? fileIds,
|
|
List<String>? includeRelativeRoots,
|
|
List<String>? includeRelativeDirs,
|
|
List<String>? excludeRelativeRoots,
|
|
List<String>? relativePathKeywords,
|
|
String? location,
|
|
bool? isFavorite,
|
|
bool? isArchived,
|
|
List<String>? mimes,
|
|
TimeRange? timeRange,
|
|
int? offset,
|
|
int? limit,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryFileDescriptors(
|
|
account: ByAccount.db(account),
|
|
fileIds: fileIds,
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
includeRelativeDirs: includeRelativeDirs,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
relativePathKeywords: relativePathKeywords,
|
|
location: location,
|
|
isFavorite: isFavorite,
|
|
isArchived: isArchived,
|
|
mimes: mimes,
|
|
timeRange: timeRange,
|
|
offset: offset,
|
|
limit: limit,
|
|
);
|
|
});
|
|
return sqlObjs.toDbFileDescriptors();
|
|
}
|
|
|
|
@override
|
|
Future<DbFilesSummary> getFilesSummary({
|
|
required DbAccount account,
|
|
List<String>? includeRelativeRoots,
|
|
List<String>? includeRelativeDirs,
|
|
List<String>? excludeRelativeRoots,
|
|
List<String>? mimes,
|
|
}) async {
|
|
final result = await _db.use((db) async {
|
|
return await db.countFileGroupsByDate(
|
|
account: ByAccount.db(account),
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
includeRelativeDirs: includeRelativeDirs,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
mimes: mimes,
|
|
isArchived: false,
|
|
);
|
|
});
|
|
return DbFilesSummary(
|
|
items: result.dateCount
|
|
.map((key, value) => MapEntry(key, DbFilesSummaryItem(count: value))),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<DbFilesMemory> getFilesMemories({
|
|
required DbAccount account,
|
|
required Date at,
|
|
required int radius,
|
|
List<String>? includeRelativeRoots,
|
|
List<String>? excludeRelativeRoots,
|
|
List<String>? mimes,
|
|
}) async {
|
|
final result = await _db.use((db) async {
|
|
return await db.queryFileDescriptorMemories(
|
|
account: ByAccount.db(account),
|
|
at: at,
|
|
radius: radius,
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
mimes: mimes,
|
|
);
|
|
});
|
|
final memories = <int, List<DbFileDescriptor>>{};
|
|
for (final r in result.map(FileDescriptorConverter.fromSql)) {
|
|
(memories[r.bestDateTime.year] ??= <DbFileDescriptor>[]).add(r);
|
|
}
|
|
return DbFilesMemory(memories: memories);
|
|
}
|
|
|
|
@override
|
|
Future<DbLocationGroupResult> groupLocations({
|
|
required DbAccount account,
|
|
List<String>? includeRelativeRoots,
|
|
List<String>? excludeRelativeRoots,
|
|
}) async {
|
|
List<ImageLocationGroup>? nameResult, admin1Result, admin2Result, ccResult;
|
|
await _db.use((db) async {
|
|
try {
|
|
nameResult = await db.groupImageLocationsByName(
|
|
account: ByAccount.db(account),
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
_log.shout("[groupLocation] Failed while groupImageLocationsByName", e,
|
|
stackTrace);
|
|
}
|
|
try {
|
|
admin1Result = await db.groupImageLocationsByAdmin1(
|
|
account: ByAccount.db(account),
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
_log.shout("[groupLocation] Failed while groupImageLocationsByAdmin1",
|
|
e, stackTrace);
|
|
}
|
|
try {
|
|
admin2Result = await db.groupImageLocationsByAdmin2(
|
|
account: ByAccount.db(account),
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
_log.shout("[groupLocation] Failed while groupImageLocationsByAdmin2",
|
|
e, stackTrace);
|
|
}
|
|
try {
|
|
ccResult = await db.groupImageLocationsByCountryCode(
|
|
account: ByAccount.db(account),
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
_log.shout(
|
|
"[groupLocation] Failed while groupImageLocationsByCountryCode",
|
|
e,
|
|
stackTrace);
|
|
}
|
|
});
|
|
return DbLocationGroupResult(
|
|
name: nameResult?.toDbLocationGroups() ?? [],
|
|
admin1: admin1Result?.toDbLocationGroups() ?? [],
|
|
admin2: admin2Result?.toDbLocationGroups() ?? [],
|
|
countryCode: ccResult?.toDbLocationGroups() ?? [],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<DbImageLatLng>> getImageLatLngWithFileIds({
|
|
required DbAccount account,
|
|
TimeRange? timeRange,
|
|
List<String>? includeRelativeRoots,
|
|
List<String>? excludeRelativeRoots,
|
|
List<String>? mimes,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryImageLatLngWithFileIds(
|
|
account: ByAccount.db(account),
|
|
timeRange: timeRange,
|
|
includeRelativeRoots: includeRelativeRoots,
|
|
excludeRelativeRoots: excludeRelativeRoots,
|
|
mimes: mimes,
|
|
);
|
|
});
|
|
return sqlObjs.computeAll((e) => DbImageLatLng(
|
|
lat: e.lat,
|
|
lng: e.lng,
|
|
fileId: e.fileId,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<List<DbNcAlbum>> getNcAlbums({
|
|
required DbAccount account,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryNcAlbums(
|
|
account: ByAccount.db(account),
|
|
);
|
|
});
|
|
return sqlObjs.toDbNcAlbums();
|
|
}
|
|
|
|
@override
|
|
Future<void> addNcAlbum({
|
|
required DbAccount account,
|
|
required DbNcAlbum album,
|
|
}) async {
|
|
await _db.use((db) async {
|
|
await db.insertNcAlbum(account: account, album: album);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteNcAlbum({
|
|
required DbAccount account,
|
|
required DbNcAlbum album,
|
|
}) async {
|
|
await _db.use((db) async {
|
|
await db.deleteNcAlbum(account: account, album: album);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<DbSyncResult> syncNcAlbums({
|
|
required DbAccount account,
|
|
required List<DbNcAlbum> albums,
|
|
}) async {
|
|
int sorter(DbNcAlbum a, DbNcAlbum b) =>
|
|
a.relativePath.compareTo(b.relativePath);
|
|
final to = albums.sorted(sorter);
|
|
return await _db.use((db) async {
|
|
final sqlObjs = await db.queryNcAlbums(
|
|
account: ByAccount.db(account),
|
|
);
|
|
final from = sqlObjs.map(NcAlbumConverter.fromSql).sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info("[syncNcAlbums] New nc albums: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info(
|
|
"[syncNcAlbums] Removed nc albums: ${deletes.toReadableString()}");
|
|
final updates = to.where((t) {
|
|
final f =
|
|
from.firstWhereOrNull((e) => e.relativePath == t.relativePath);
|
|
return f != null && f != t;
|
|
}).toList();
|
|
_log.info(
|
|
"[syncNcAlbums] Updated nc albums: ${updates.toReadableString()}");
|
|
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
|
await db.replaceNcAlbums(
|
|
account: ByAccount.db(account),
|
|
inserts: inserts,
|
|
deletes: deletes,
|
|
updates: updates,
|
|
);
|
|
}
|
|
return DbSyncResult(
|
|
insert: inserts.length,
|
|
delete: deletes.length,
|
|
update: updates.length,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbNcAlbumItem>> getNcAlbumItemsByParent({
|
|
required DbAccount account,
|
|
required DbNcAlbum parent,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryNcAlbumItemsByParentRelativePath(
|
|
account: ByAccount.db(account),
|
|
parentRelativePath: parent.relativePath,
|
|
);
|
|
});
|
|
return sqlObjs.toDbNcAlbumItems();
|
|
}
|
|
|
|
@override
|
|
Future<DbSyncResult> syncNcAlbumItems({
|
|
required DbAccount account,
|
|
required DbNcAlbum album,
|
|
required List<DbNcAlbumItem> items,
|
|
}) async {
|
|
int sorter(DbNcAlbumItem a, DbNcAlbumItem b) =>
|
|
a.fileId.compareTo(b.fileId);
|
|
final to = items.sorted(sorter);
|
|
return await _db.use((db) async {
|
|
final sqlObjs = await db.queryNcAlbumItemsByParentRelativePath(
|
|
account: ByAccount.db(account),
|
|
parentRelativePath: album.relativePath,
|
|
);
|
|
final int parentRowId;
|
|
if (sqlObjs.isNotEmpty) {
|
|
parentRowId = sqlObjs.first.parent;
|
|
} else {
|
|
final parent = await db.queryNcAlbumByRelativePath(
|
|
account: ByAccount.db(account),
|
|
relativePath: album.relativePath,
|
|
);
|
|
parentRowId = parent!.rowId;
|
|
}
|
|
|
|
final from = sqlObjs.map(NcAlbumItemConverter.fromSql).sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info(
|
|
"[syncNcAlbumItems] New nc album items: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info(
|
|
"[syncNcAlbumItems] Removed nc album items: ${deletes.toReadableString()}");
|
|
final updates = to.where((t) {
|
|
final f = from.firstWhereOrNull((e) => e.fileId == t.fileId);
|
|
return f != null && f != t;
|
|
}).toList();
|
|
_log.info(
|
|
"[syncNcAlbumItems] Updated nc album items: ${updates.toReadableString()}");
|
|
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
|
await db.replaceNcAlbumItems(
|
|
parentRowId: parentRowId,
|
|
inserts: inserts,
|
|
deletes: deletes,
|
|
updates: updates,
|
|
);
|
|
}
|
|
return DbSyncResult(
|
|
insert: inserts.length,
|
|
delete: deletes.length,
|
|
update: updates.length,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbRecognizeFace>> getRecognizeFaces({
|
|
required DbAccount account,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryRecognizeFaces(
|
|
account: ByAccount.db(account),
|
|
);
|
|
});
|
|
return sqlObjs.toDbRecognizeFaces();
|
|
}
|
|
|
|
@override
|
|
Future<List<DbRecognizeFaceItem>> getRecognizeFaceItemsByFaceLabel({
|
|
required DbAccount account,
|
|
required String label,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryRecognizeFaceItemsByFaceLabel(
|
|
account: ByAccount.db(account),
|
|
label: label,
|
|
);
|
|
});
|
|
return sqlObjs.toDbRecognizeFaceItems();
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, List<DbRecognizeFaceItem>>>
|
|
getRecognizeFaceItemsByFaceLabels({
|
|
required DbAccount account,
|
|
required List<String> labels,
|
|
ErrorWithValueHandler<String>? onError,
|
|
}) async {
|
|
final results = <String, List<RecognizeFaceItem>>{};
|
|
await _db.use((db) async {
|
|
for (final l in labels) {
|
|
try {
|
|
results[l] = await db.queryRecognizeFaceItemsByFaceLabel(
|
|
account: ByAccount.db(account),
|
|
label: l,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
onError?.call(l, e, stackTrace);
|
|
}
|
|
}
|
|
});
|
|
return results.asyncMap((key, value) =>
|
|
value.toDbRecognizeFaceItems().then((v) => MapEntry(key, v)));
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, DbRecognizeFaceItem>>
|
|
getLatestRecognizeFaceItemsByFaceLabels({
|
|
required DbAccount account,
|
|
required List<String> labels,
|
|
ErrorWithValueHandler<String>? onError,
|
|
}) async {
|
|
final results = <String, List<RecognizeFaceItem>>{};
|
|
await _db.use((db) async {
|
|
for (final l in labels) {
|
|
try {
|
|
results[l] = await db.queryRecognizeFaceItemsByFaceLabel(
|
|
account: ByAccount.db(account),
|
|
label: l,
|
|
orderBy: [RecognizeFaceItemSort.fileIdDesc],
|
|
limit: 1,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
onError?.call(l, e, stackTrace);
|
|
}
|
|
}
|
|
});
|
|
return results.asyncMap((key, value) =>
|
|
value.toDbRecognizeFaceItems().then((v) => MapEntry(key, v.first)));
|
|
}
|
|
|
|
@override
|
|
Future<bool> syncRecognizeFacesAndItems({
|
|
required DbAccount account,
|
|
required Map<DbRecognizeFace, List<DbRecognizeFaceItem>> data,
|
|
}) async {
|
|
int sorter(DbRecognizeFace a, DbRecognizeFace b) =>
|
|
a.label.compareTo(b.label);
|
|
int itemSorter(DbRecognizeFaceItem a, DbRecognizeFaceItem b) =>
|
|
a.fileId.compareTo(b.fileId);
|
|
final faces = data.keys;
|
|
final to = faces.sorted(sorter);
|
|
final toItems =
|
|
data.map((key, value) => MapEntry(key, value.sorted(itemSorter)));
|
|
return await _db.use((db) async {
|
|
var result = false;
|
|
final sqlAccount = await db.accountOf(ByAccount.db(account));
|
|
final sqlObjs = await db.queryRecognizeFaces(
|
|
account: ByAccount.sql(sqlAccount),
|
|
);
|
|
final from = sqlObjs.map(RecognizeFaceConverter.fromSql).sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info(
|
|
"[syncRecognizeFacesAndItems] New faces: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info(
|
|
"[syncRecognizeFacesAndItems] Removed faces: ${deletes.toReadableString()}");
|
|
final updates = to.where((t) {
|
|
final f = from.firstWhereOrNull((e) => e.label == t.label);
|
|
return f != null && f != t;
|
|
}).toList();
|
|
_log.info(
|
|
"[syncRecognizeFacesAndItems] Updated faces: ${updates.toReadableString()}");
|
|
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
|
await db.replaceRecognizeFaces(
|
|
account: ByAccount.sql(sqlAccount),
|
|
inserts: inserts,
|
|
deletes: deletes,
|
|
updates: updates,
|
|
);
|
|
result = true;
|
|
}
|
|
sqlObjs.addAll(await db.queryRecognizeFaces(
|
|
account: ByAccount.sql(sqlAccount),
|
|
labels: inserts.map((e) => e.label).toList(),
|
|
));
|
|
|
|
for (final d in data.entries) {
|
|
try {
|
|
result |= await _replaceRecognizeFaceItems(
|
|
db,
|
|
sqlAccount: sqlAccount,
|
|
face: sqlObjs.firstWhere((e) => e.label == d.key.label),
|
|
items: toItems[d.key]!,
|
|
sorter: itemSorter,
|
|
);
|
|
} catch (e, stackTrace) {
|
|
_log.shout(
|
|
"[syncRecognizeFacesAndItems] Failed to replace items for face: ${d.key}",
|
|
e,
|
|
stackTrace,
|
|
);
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<DbTag>> getTags({
|
|
required DbAccount account,
|
|
}) async {
|
|
final sqlObjs = await _db.use((db) async {
|
|
return await db.queryTags(
|
|
account: ByAccount.db(account),
|
|
);
|
|
});
|
|
return sqlObjs.toDbTags();
|
|
}
|
|
|
|
@override
|
|
Future<DbTag?> getTagByDisplayName({
|
|
required DbAccount account,
|
|
required String displayName,
|
|
}) async {
|
|
final sqlObj = await _db.use((db) async {
|
|
return await db.queryTagByDisplayName(
|
|
account: ByAccount.db(account),
|
|
displayName: displayName,
|
|
);
|
|
});
|
|
return sqlObj?.let(TagConverter.fromSql);
|
|
}
|
|
|
|
@override
|
|
Future<DbSyncIdResult> syncTags({
|
|
required DbAccount account,
|
|
required List<DbTag> tags,
|
|
}) async {
|
|
int sorter(DbTag a, DbTag b) => a.id.compareTo(b.id);
|
|
final to = tags.sorted(sorter);
|
|
return await _db.use((db) async {
|
|
final sqlObjs = await db.queryTags(
|
|
account: ByAccount.db(account),
|
|
);
|
|
final from = sqlObjs.map(TagConverter.fromSql).sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info("[syncTags] New tags: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info("[syncTags] Removed tags: ${deletes.toReadableString()}");
|
|
final updates = to.where((t) {
|
|
final f = from.firstWhereOrNull((e) => e.id == t.id);
|
|
return f != null && f != t;
|
|
}).toList();
|
|
_log.info("[syncTags] Updated tags: ${updates.toReadableString()}");
|
|
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
|
await db.replaceTags(
|
|
account: ByAccount.db(account),
|
|
inserts: inserts,
|
|
deletes: deletes,
|
|
updates: updates,
|
|
);
|
|
}
|
|
return DbSyncIdResult(
|
|
insert: inserts.map((e) => e.id).toList(),
|
|
delete: deletes.map((e) => e.id).toList(),
|
|
update: updates.map((e) => e.id).toList(),
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> migrateV55(
|
|
void Function(int current, int count)? onProgress) async {
|
|
await _db.use((db) async {
|
|
await db.migrateV55(onProgress);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> sqlVacuum() async {
|
|
await _db.useNoTransaction((db) async {
|
|
await db.customStatement("VACUUM;");
|
|
});
|
|
}
|
|
|
|
Future<bool> _replaceRecognizeFaceItems(
|
|
SqliteDb db, {
|
|
required Account sqlAccount,
|
|
required RecognizeFace face,
|
|
required List<DbRecognizeFaceItem> items,
|
|
required int Function(DbRecognizeFaceItem, DbRecognizeFaceItem) sorter,
|
|
}) async {
|
|
final to = items;
|
|
final sqlObjs = await db.queryRecognizeFaceItemsByFaceLabel(
|
|
account: ByAccount.sql(sqlAccount),
|
|
label: face.label,
|
|
);
|
|
final from = sqlObjs.map(RecognizeFaceItemConverter.fromSql).sorted(sorter);
|
|
final diff = getDiffWith(from, to, sorter);
|
|
final inserts = diff.onlyInB;
|
|
_log.info(
|
|
"[_replaceRecognizeFaceItems] New faces: ${inserts.toReadableString()}");
|
|
final deletes = diff.onlyInA;
|
|
_log.info(
|
|
"[_replaceRecognizeFaceItems] Removed faces: ${deletes.toReadableString()}");
|
|
final updates = to.where((t) {
|
|
final f = from.firstWhereOrNull((e) => e.fileId == t.fileId);
|
|
return f != null && f != t;
|
|
}).toList();
|
|
_log.info(
|
|
"[_replaceRecognizeFaceItems] Updated faces: ${updates.toReadableString()}");
|
|
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
|
await db.replaceRecognizeFaceItems(
|
|
face: face,
|
|
inserts: inserts,
|
|
deletes: deletes,
|
|
updates: updates,
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Deprecated("For compatibility only")
|
|
SqliteDb get compatDb => _db;
|
|
|
|
late final SqliteDb _db;
|
|
}
|