2024-04-27 20:10:27 +02:00
|
|
|
// ignore_for_file: null_argument_to_non_null_type
|
|
|
|
|
2022-07-13 14:23:48 +02:00
|
|
|
import 'dart:async';
|
2022-02-03 18:06:44 +02:00
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
2022-06-25 20:46:51 +02:00
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
2022-11-08 19:00:24 +02:00
|
|
|
import 'package:immich_mobile/shared/models/asset.dart';
|
2023-03-04 00:38:30 +02:00
|
|
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
2023-02-09 19:32:08 +02:00
|
|
|
import 'package:immich_mobile/shared/models/store.dart';
|
2023-05-25 05:52:43 +02:00
|
|
|
import 'package:immich_mobile/shared/models/user.dart';
|
2022-08-03 07:04:34 +02:00
|
|
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
2023-03-04 00:38:30 +02:00
|
|
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
2022-07-13 14:23:48 +02:00
|
|
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
2023-03-04 00:38:30 +02:00
|
|
|
import 'package:immich_mobile/shared/services/sync.service.dart';
|
|
|
|
import 'package:isar/isar.dart';
|
2022-11-30 18:58:07 +02:00
|
|
|
import 'package:logging/logging.dart';
|
2024-01-15 17:26:13 +02:00
|
|
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
2022-07-13 14:23:48 +02:00
|
|
|
import 'package:openapi/api.dart';
|
2022-02-03 18:06:44 +02:00
|
|
|
|
2022-07-13 14:23:48 +02:00
|
|
|
final assetServiceProvider = Provider(
|
|
|
|
(ref) => AssetService(
|
|
|
|
ref.watch(apiServiceProvider),
|
2023-03-04 00:38:30 +02:00
|
|
|
ref.watch(syncServiceProvider),
|
|
|
|
ref.watch(dbProvider),
|
2022-07-13 14:23:48 +02:00
|
|
|
),
|
|
|
|
);
|
2022-06-25 20:46:51 +02:00
|
|
|
|
2022-02-03 18:06:44 +02:00
|
|
|
class AssetService {
|
2022-07-13 14:23:48 +02:00
|
|
|
final ApiService _apiService;
|
2023-03-04 00:38:30 +02:00
|
|
|
final SyncService _syncService;
|
2022-11-30 18:58:07 +02:00
|
|
|
final log = Logger('AssetService');
|
2023-03-04 00:38:30 +02:00
|
|
|
final Isar _db;
|
2022-02-03 18:06:44 +02:00
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
AssetService(
|
|
|
|
this._apiService,
|
|
|
|
this._syncService,
|
|
|
|
this._db,
|
|
|
|
);
|
|
|
|
|
|
|
|
/// Checks the server for updated assets and updates the local database if
|
|
|
|
/// required. Returns `true` if there were any changes.
|
2023-05-25 05:52:43 +02:00
|
|
|
Future<bool> refreshRemoteAssets([User? user]) async {
|
2023-09-10 14:51:18 +02:00
|
|
|
user ??= Store.get<User>(StoreKey.currentUser);
|
2023-03-04 00:38:30 +02:00
|
|
|
final Stopwatch sw = Stopwatch()..start();
|
2023-03-27 04:35:52 +02:00
|
|
|
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
2023-05-25 05:52:43 +02:00
|
|
|
user,
|
2023-09-10 14:51:18 +02:00
|
|
|
_getRemoteAssetChanges,
|
|
|
|
_getRemoteAssets,
|
2023-03-27 04:35:52 +02:00
|
|
|
);
|
2023-03-04 00:38:30 +02:00
|
|
|
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
|
|
|
return changes;
|
|
|
|
}
|
2022-02-07 04:31:32 +02:00
|
|
|
|
2023-09-10 14:51:18 +02:00
|
|
|
/// Returns `(null, null)` if changes are invalid -> requires full sync
|
|
|
|
Future<(List<Asset>? toUpsert, List<String>? toDelete)>
|
|
|
|
_getRemoteAssetChanges(User user, DateTime since) async {
|
|
|
|
final deleted = await _apiService.auditApi
|
2024-01-22 18:49:51 +02:00
|
|
|
.getAuditDeletes(since, EntityType.ASSET, userId: user.id);
|
2023-09-10 14:51:18 +02:00
|
|
|
if (deleted == null || deleted.needsFullSync) return (null, null);
|
|
|
|
final assetDto = await _apiService.assetApi
|
|
|
|
.getAllAssets(userId: user.id, updatedAfter: since);
|
|
|
|
if (assetDto == null) return (null, null);
|
|
|
|
return (assetDto.map(Asset.remote).toList(), deleted.ids);
|
|
|
|
}
|
|
|
|
|
2024-03-06 18:15:54 +02:00
|
|
|
/// Returns the list of people of the given asset id.
|
|
|
|
// If the server is not reachable `null` is returned.
|
|
|
|
Future<List<PersonWithFacesResponseDto>?> getRemotePeopleOfAsset(
|
|
|
|
String remoteId,
|
|
|
|
) async {
|
|
|
|
try {
|
|
|
|
final AssetResponseDto? dto =
|
|
|
|
await _apiService.assetApi.getAssetInfo(remoteId);
|
|
|
|
|
|
|
|
return dto?.people;
|
|
|
|
} catch (error, stack) {
|
|
|
|
log.severe(
|
|
|
|
'Error while getting remote asset info: ${error.toString()}',
|
|
|
|
error,
|
|
|
|
stack,
|
|
|
|
);
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-26 18:16:02 +02:00
|
|
|
/// Returns `null` if the server state did not change, else list of assets
|
2023-09-10 14:51:18 +02:00
|
|
|
Future<List<Asset>?> _getRemoteAssets(User user) async {
|
2024-02-18 20:22:25 +02:00
|
|
|
const int chunkSize = 10000;
|
2022-11-30 18:58:07 +02:00
|
|
|
try {
|
2023-11-06 19:40:43 +02:00
|
|
|
final DateTime now = DateTime.now().toUtc();
|
|
|
|
final List<Asset> allAssets = [];
|
|
|
|
for (int i = 0;; i += chunkSize) {
|
|
|
|
final List<AssetResponseDto>? assets =
|
|
|
|
await _apiService.assetApi.getAllAssets(
|
|
|
|
userId: user.id,
|
|
|
|
// updatedBefore is important! without it we could
|
|
|
|
// a) get the same Asset multiple times in different versions (when
|
|
|
|
// the asset is modified while the chunks are loaded from the server)
|
|
|
|
// b) miss assets when new assets are inserted in between the calls
|
|
|
|
updatedBefore: now,
|
|
|
|
skip: i,
|
|
|
|
take: chunkSize,
|
|
|
|
);
|
|
|
|
if (assets == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
allAssets.addAll(assets.map(Asset.remote));
|
|
|
|
if (assets.length < chunkSize) {
|
|
|
|
break;
|
|
|
|
}
|
2023-03-04 00:38:30 +02:00
|
|
|
}
|
2023-11-06 19:40:43 +02:00
|
|
|
return allAssets;
|
2023-06-26 01:59:35 +02:00
|
|
|
} catch (error, stack) {
|
|
|
|
log.severe(
|
2024-02-24 05:38:57 +02:00
|
|
|
'Error while getting remote assets',
|
2023-06-26 01:59:35 +02:00
|
|
|
error,
|
|
|
|
stack,
|
|
|
|
);
|
2023-03-04 00:38:30 +02:00
|
|
|
return null;
|
2022-02-13 23:10:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 09:01:14 +02:00
|
|
|
Future<bool> deleteAssets(
|
|
|
|
Iterable<Asset> deleteAssets, {
|
|
|
|
bool? force = false,
|
|
|
|
}) async {
|
2022-02-13 23:10:42 +02:00
|
|
|
try {
|
2022-11-08 19:00:24 +02:00
|
|
|
final List<String> payload = [];
|
2022-02-13 23:10:42 +02:00
|
|
|
|
2022-11-08 19:00:24 +02:00
|
|
|
for (final asset in deleteAssets) {
|
2023-02-04 22:42:42 +02:00
|
|
|
payload.add(asset.remoteId!);
|
2022-02-13 23:10:42 +02:00
|
|
|
}
|
|
|
|
|
2023-10-06 09:01:14 +02:00
|
|
|
await _apiService.assetApi.deleteAssets(
|
|
|
|
AssetBulkDeleteDto(
|
|
|
|
ids: payload,
|
|
|
|
force: force,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
return true;
|
2023-06-26 01:59:35 +02:00
|
|
|
} catch (error, stack) {
|
2024-02-24 05:38:57 +02:00
|
|
|
log.severe("Error while deleting assets", error, stack);
|
2022-02-11 04:40:11 +02:00
|
|
|
}
|
2023-10-06 09:01:14 +02:00
|
|
|
return false;
|
2022-02-11 04:40:11 +02:00
|
|
|
}
|
2023-02-05 05:25:11 +02:00
|
|
|
|
2023-03-04 00:38:30 +02:00
|
|
|
/// Loads the exif information from the database. If there is none, loads
|
|
|
|
/// the exif info from the server (remote assets only)
|
|
|
|
Future<Asset> loadExif(Asset a) async {
|
|
|
|
a.exifInfo ??= await _db.exifInfos.get(a.id);
|
2023-05-17 19:36:02 +02:00
|
|
|
// fileSize is always filled on the server but not set on client
|
|
|
|
if (a.exifInfo?.fileSize == null) {
|
2023-03-04 00:38:30 +02:00
|
|
|
if (a.isRemote) {
|
2024-02-08 23:57:54 +02:00
|
|
|
final dto = await _apiService.assetApi.getAssetInfo(a.remoteId!);
|
2023-03-04 00:38:30 +02:00
|
|
|
if (dto != null && dto.exifInfo != null) {
|
2023-05-17 19:36:02 +02:00
|
|
|
final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
|
|
|
|
if (newExif != a.exifInfo) {
|
|
|
|
if (a.isInDb) {
|
|
|
|
_db.writeTxn(() => a.put(_db));
|
|
|
|
} else {
|
|
|
|
debugPrint("[loadExif] parameter Asset is not from DB!");
|
|
|
|
}
|
2023-03-04 00:38:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// TODO implement local exif info parsing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
|
2024-04-27 20:10:27 +02:00
|
|
|
Future<void> updateAssets(
|
2023-05-17 19:36:02 +02:00
|
|
|
List<Asset> assets,
|
2023-02-09 19:32:08 +02:00
|
|
|
UpdateAssetDto updateAssetDto,
|
|
|
|
) async {
|
2024-04-27 20:10:27 +02:00
|
|
|
return await _apiService.assetApi.updateAssets(
|
|
|
|
AssetBulkUpdateDto(
|
|
|
|
ids: assets.map((e) => e.remoteId!).toList(),
|
|
|
|
dateTimeOriginal: updateAssetDto.dateTimeOriginal,
|
|
|
|
isFavorite: updateAssetDto.isFavorite,
|
|
|
|
isArchived: updateAssetDto.isArchived,
|
|
|
|
latitude: updateAssetDto.latitude,
|
|
|
|
longitude: updateAssetDto.longitude,
|
2023-05-17 19:36:02 +02:00
|
|
|
),
|
|
|
|
);
|
2023-02-05 05:25:11 +02:00
|
|
|
}
|
|
|
|
|
2023-05-17 19:36:02 +02:00
|
|
|
Future<List<Asset?>> changeFavoriteStatus(
|
|
|
|
List<Asset> assets,
|
|
|
|
bool isFavorite,
|
2024-04-27 20:10:27 +02:00
|
|
|
) async {
|
|
|
|
try {
|
|
|
|
await updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite));
|
|
|
|
|
|
|
|
for (var element in assets) {
|
|
|
|
element.isFavorite = isFavorite;
|
|
|
|
}
|
|
|
|
|
|
|
|
await _syncService.upsertAssetsWithExif(assets);
|
|
|
|
|
|
|
|
return assets;
|
|
|
|
} catch (error, stack) {
|
|
|
|
log.severe("Error while changing favorite status", error, stack);
|
|
|
|
return Future.value(null);
|
|
|
|
}
|
2023-02-05 05:25:11 +02:00
|
|
|
}
|
2023-04-17 07:02:07 +02:00
|
|
|
|
2024-04-27 20:10:27 +02:00
|
|
|
Future<List<Asset?>> changeArchiveStatus(
|
|
|
|
List<Asset> assets,
|
|
|
|
bool isArchived,
|
|
|
|
) async {
|
|
|
|
try {
|
|
|
|
await updateAssets(assets, UpdateAssetDto(isArchived: isArchived));
|
|
|
|
|
|
|
|
for (var element in assets) {
|
|
|
|
element.isArchived = isArchived;
|
|
|
|
}
|
|
|
|
|
|
|
|
await _syncService.upsertAssetsWithExif(assets);
|
|
|
|
|
|
|
|
return assets;
|
|
|
|
} catch (error, stack) {
|
|
|
|
log.severe("Error while changing archive status", error, stack);
|
|
|
|
return Future.value(null);
|
|
|
|
}
|
2023-04-17 07:02:07 +02:00
|
|
|
}
|
2023-12-05 21:34:37 +02:00
|
|
|
|
|
|
|
Future<List<Asset?>> changeDateTime(
|
|
|
|
List<Asset> assets,
|
|
|
|
String updatedDt,
|
2024-04-27 20:10:27 +02:00
|
|
|
) async {
|
|
|
|
try {
|
|
|
|
await updateAssets(
|
|
|
|
assets,
|
|
|
|
UpdateAssetDto(dateTimeOriginal: updatedDt),
|
|
|
|
);
|
|
|
|
|
|
|
|
for (var element in assets) {
|
|
|
|
element.fileCreatedAt = DateTime.parse(updatedDt);
|
|
|
|
element.exifInfo?.dateTimeOriginal = DateTime.parse(updatedDt);
|
|
|
|
}
|
|
|
|
|
|
|
|
await _syncService.upsertAssetsWithExif(assets);
|
|
|
|
|
|
|
|
return assets;
|
|
|
|
} catch (error, stack) {
|
|
|
|
log.severe("Error while changing date/time status", error, stack);
|
|
|
|
return Future.value(null);
|
|
|
|
}
|
2023-12-05 21:34:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<Asset?>> changeLocation(
|
|
|
|
List<Asset> assets,
|
|
|
|
LatLng location,
|
2024-04-27 20:10:27 +02:00
|
|
|
) async {
|
|
|
|
try {
|
|
|
|
await updateAssets(
|
|
|
|
assets,
|
|
|
|
UpdateAssetDto(
|
|
|
|
latitude: location.latitude,
|
|
|
|
longitude: location.longitude,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
for (var element in assets) {
|
|
|
|
element.exifInfo?.lat = location.latitude;
|
|
|
|
element.exifInfo?.long = location.longitude;
|
|
|
|
}
|
|
|
|
|
|
|
|
await _syncService.upsertAssetsWithExif(assets);
|
|
|
|
|
|
|
|
return assets;
|
|
|
|
} catch (error, stack) {
|
|
|
|
log.severe("Error while changing location status", error, stack);
|
|
|
|
return Future.value(null);
|
|
|
|
}
|
2023-12-05 21:34:37 +02:00
|
|
|
}
|
2022-02-03 18:06:44 +02:00
|
|
|
}
|