import 'package:async/async.dart'; import 'package:immich_mobile/domain/interfaces/album.interface.dart'; import 'package:immich_mobile/domain/interfaces/album_asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/album_etag.interface.dart'; import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/database.interface.dart'; import 'package:immich_mobile/domain/interfaces/device_album.interface.dart'; import 'package:immich_mobile/domain/models/album.model.dart'; import 'package:immich_mobile/domain/models/album_etag.model.dart'; import 'package:immich_mobile/domain/services/asset_sync.service.dart'; import 'package:immich_mobile/domain/services/hash.service.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/collection_util.dart'; import 'package:immich_mobile/utils/isolate_helper.dart'; import 'package:immich_mobile/utils/mixins/log.mixin.dart'; class AlbumSyncService with LogMixin { AlbumSyncService(); final _fullDeviceSyncCache = AsyncCache.ephemeral(); Future performFullDeviceSyncIsolate() async { return await _fullDeviceSyncCache .fetch(() async => await IsolateHelper.run(performFullDeviceSync)); } Future performFullDeviceSync() async { try { final Stopwatch stopwatch = Stopwatch()..start(); final deviceAlbums = await di().getAll(); final dbAlbums = await di().getAll(localOnly: true); final hasChange = await CollectionUtil.diffLists( dbAlbums, deviceAlbums, compare: Album.compareByLocalId, both: _syncDeviceAlbum, // Album is in DB but not anymore in device. Remove album and album specific assets onlyFirst: _removeDeviceAlbum, onlySecond: _addDeviceAlbum, ); log.i("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); return hasChange; } catch (e, s) { log.e("Error performing full device sync", e, s); } return false; } Future _syncDeviceAlbum( Album dbAlbum, Album deviceAlbum, { DateTime? modifiedUntil, }) async { assert(dbAlbum.id != null, "Album ID from DB is null"); final albumEtag = await di().get(dbAlbum.id!) ?? AlbumETag.initial(); final assetCountInDevice = await di().getAssetCount(deviceAlbum.localId!); final albumNotUpdated = deviceAlbum.name == dbAlbum.name && dbAlbum.modifiedTime.isAtSameMomentAs(deviceAlbum.modifiedTime) && assetCountInDevice == albumEtag.assetCount; if (albumNotUpdated) { log.i("Device Album ${deviceAlbum.name} not updated. Skipping sync."); return false; } await _addDeviceAlbum(dbAlbum, modifiedUntil: modifiedUntil); return true; } Future _addDeviceAlbum(Album album, {DateTime? modifiedUntil}) async { try { log.i("Syncing device album ${album.name}"); final albumId = (await di().upsert(album))?.id; // break fast if we cannot add an album if (albumId == null) { log.d("Failed creating device album. Skipped assets from album"); return; } final assets = await di().getHashedAssetsForAlbum( album.localId!, modifiedUntil: modifiedUntil, ); await di().txn(() async { final albumAssetsInDB = await di().getAssetsForAlbum(albumId); await di().upsertAssetsToDb( assets, albumAssetsInDB, isRemoteSync: false, ); // This is needed to get the updated assets for device album with valid db id field final albumAssets = await di() .getForLocalIds(assets.map((a) => a.localId!)); await di().addAssetIds( albumId, albumAssets.map((a) => a.id!), ); await di().upsert( album.copyWith(thumbnailAssetId: albumAssets.firstOrNull?.id), ); // Update ETag final albumETag = AlbumETag( albumId: albumId, assetCount: assets.length, modifiedTime: album.modifiedTime, ); await di().upsert(albumETag); }); } catch (e, s) { log.w("Error while adding device album", e, s); } } Future _removeDeviceAlbum(Album album) async { assert(album.id != null, "Album ID from DB is null"); log.i("Removing device album ${album.name}"); final albumId = album.id!; try { await di().txn(() async { final toRemove = await di().getAssetIdsOnlyInAlbum(albumId); await di().deleteId(albumId); await di().deleteAlbumId(albumId); await di().deleteIds(toRemove); }); } catch (e, s) { log.w("Error while removing device album", e, s); } } }