1
0
mirror of https://github.com/immich-app/immich.git synced 2025-08-08 23:07:06 +02:00

refactor(mobile): album api repository for album service (#12791)

* refactor(mobile): album api repository for album service
This commit is contained in:
Fynn Petersen-Frey
2024-09-20 15:32:37 +02:00
committed by GitHub
parent 94fc1f213a
commit 3868736799
20 changed files with 751 additions and 223 deletions

View File

@ -6,63 +6,61 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final albumServiceProvider = Provider(
(ref) => AlbumService(
ref.watch(apiServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(entityServiceProvider),
ref.watch(albumRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(backupRepositoryProvider),
ref.watch(albumMediaRepositoryProvider),
ref.watch(albumApiRepositoryProvider),
),
);
class AlbumService {
final ApiService _apiService;
final UserService _userService;
final SyncService _syncService;
final EntityService _entityService;
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IUserRepository _userRepository;
final IBackupRepository _backupAlbumRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final Logger _log = Logger('AlbumService');
Completer<bool> _localCompleter = Completer()..complete(false);
Completer<bool> _remoteCompleter = Completer()..complete(false);
AlbumService(
this._apiService,
this._userService,
this._syncService,
this._entityService,
this._albumRepository,
this._assetRepository,
this._userRepository,
this._backupAlbumRepository,
this._albumMediaRepository,
this._albumApiRepository,
);
/// Checks all selected device albums for changes of albums and their assets
@ -164,17 +162,11 @@ class AlbumService {
bool changes = false;
try {
await _userService.refreshUsers();
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
.getAllAlbums(shared: isShared ? true : null);
if (serverAlbums == null) {
return false;
}
final List<Album> serverAlbums =
await _albumApiRepository.getAll(shared: isShared ? true : null);
changes = await _syncService.syncRemoteAlbumsToDb(
serverAlbums,
isShared: isShared,
loadDetails: (dto) async => dto.assetCount == dto.assets.length
? dto
: (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
);
} finally {
_remoteCompleter.complete(changes);
@ -188,30 +180,13 @@ class AlbumService {
Iterable<Asset> assets, [
Iterable<User> sharedUsers = const [],
]) async {
try {
AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
CreateAlbumDto(
albumName: albumName,
assetIds: assets.map((asset) => asset.remoteId!).toList(),
albumUsers: sharedUsers
.map(
(e) => AlbumUserCreateDto(
userId: e.id,
role: AlbumUserRole.editor,
),
)
.toList(),
),
);
if (remote != null) {
final Album album = await Album.remote(remote);
await _albumRepository.create(album);
return album;
}
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
}
return null;
final Album album = await _albumApiRepository.create(
albumName,
assetIds: assets.map((asset) => asset.remoteId!),
sharedUserIds: sharedUsers.map((user) => user.id),
);
await _entityService.fillAlbumWithDatabaseEntities(album);
return _albumRepository.create(album);
}
/*
@ -243,32 +218,21 @@ class AlbumService {
Album album,
) async {
try {
var response = await _apiService.albumsApi.addAssetsToAlbum(
final result = await _albumApiRepository.addAssets(
album.remoteId!,
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
assets.map((asset) => asset.remoteId!),
);
if (response != null) {
List<Asset> successAssets = [];
List<String> duplicatedAssets = [];
final List<Asset> addedAssets = result.added
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
.toList();
for (final result in response) {
if (result.success) {
successAssets
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
} else if (!result.success &&
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
duplicatedAssets.add(result.id);
}
}
await _updateAssets(album.id, add: addedAssets);
await _updateAssets(album.id, add: successAssets);
return AlbumAddAssetsResponse(
alreadyInAlbum: duplicatedAssets,
successfullyAdded: successAssets.length,
);
}
return AlbumAddAssetsResponse(
alreadyInAlbum: result.duplicates,
successfullyAdded: addedAssets.length,
);
} catch (e) {
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
}
@ -293,20 +257,11 @@ class AlbumService {
Album album,
) async {
try {
final List<AlbumUserAddDto> albumUsers = sharedUserIds
.map((userId) => AlbumUserAddDto(userId: userId))
.toList();
final result = await _apiService.albumsApi.addUsersToAlbum(
album.remoteId!,
AddUsersDto(albumUsers: albumUsers),
);
if (result != null) {
album.sharedUsers.addAll(await _userRepository.getByIds(sharedUserIds));
album.shared = result.shared;
await _albumRepository.update(album);
return true;
}
final updatedAlbum =
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
await _albumRepository.update(updatedAlbum);
return true;
} catch (e) {
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
}
@ -315,15 +270,13 @@ class AlbumService {
Future<bool> setActivityEnabled(Album album, bool enabled) async {
try {
final result = await _apiService.albumsApi.updateAlbumInfo(
final updatedAlbum = await _albumApiRepository.update(
album.remoteId!,
UpdateAlbumDto(isActivityEnabled: enabled),
activityEnabled: enabled,
);
if (result != null) {
album.activityEnabled = enabled;
await _albumRepository.update(album);
return true;
}
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
await _albumRepository.update(updatedAlbum);
return true;
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
}
@ -334,7 +287,7 @@ class AlbumService {
try {
final user = Store.get(StoreKey.currentUser);
if (album.owner.value?.isarId == user.isarId) {
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
await _albumApiRepository.delete(album.remoteId!);
}
if (album.shared) {
final foreignAssets =
@ -365,7 +318,7 @@ class AlbumService {
Future<bool> leaveAlbum(Album album) async {
try {
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
return true;
} catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}");
@ -378,21 +331,14 @@ class AlbumService {
Iterable<Asset> assets,
) async {
try {
final response = await _apiService.albumsApi.removeAssetFromAlbum(
final result = await _albumApiRepository.removeAssets(
album.remoteId!,
BulkIdsDto(
ids: assets.map((asset) => asset.remoteId!).toList(),
),
assets.map((asset) => asset.remoteId!),
);
if (response != null) {
final toRemove = response.every((e) => e.success)
? assets
: response
.where((e) => e.success)
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
await _updateAssets(album.id, remove: toRemove.toList());
return true;
}
final toRemove = result.removed
.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
await _updateAssets(album.id, remove: toRemove.toList());
return true;
} catch (e) {
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
}
@ -404,9 +350,9 @@ class AlbumService {
User user,
) async {
try {
await _apiService.albumsApi.removeUserFromAlbum(
await _albumApiRepository.removeUser(
album.remoteId!,
user.id,
userId: user.id,
);
album.sharedUsers.remove(user);
@ -427,15 +373,12 @@ class AlbumService {
String newAlbumTitle,
) async {
try {
await _apiService.albumsApi.updateAlbumInfo(
album = await _albumApiRepository.update(
album.remoteId!,
UpdateAlbumDto(
albumName: newAlbumTitle,
),
name: newAlbumTitle,
);
album.name = newAlbumTitle;
await _entityService.fillAlbumWithDatabaseEntities(album);
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeTitleAlbum ${e.toString()}");
@ -456,12 +399,8 @@ class AlbumService {
for (final albumName in albumNames) {
Album? album = await getAlbumByName(albumName, true);
album ??= await createAlbum(albumName, []);
if (album != null && album.remoteId != null) {
await _apiService.albumsApi.addAssetsToAlbum(
album.remoteId!,
BulkIdsDto(ids: assetIds),
);
await _albumApiRepository.addAssets(album.remoteId!, assetIds);
}
}
}

View File

@ -13,12 +13,14 @@ import 'package:immich_mobile/main.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
@ -363,23 +365,33 @@ class BackgroundService {
PartnerService partnerService = PartnerService(apiService, db);
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
UserRepository userRepository = UserRepository(db);
BackupRepository backupAlbumRepository = BackupRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository();
UserRepository userRepository = UserRepository(db);
AlbumApiRepository albumApiRepository =
AlbumApiRepository(apiService.albumsApi);
HashService hashService = HashService(db, this, albumMediaRepository);
SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
EntityService entityService =
EntityService(assetRepository, userRepository);
SyncService syncSerive = SyncService(
db,
hashService,
entityService,
albumMediaRepository,
albumApiRepository,
);
UserService userService =
UserService(apiService, db, syncSerive, partnerService);
AlbumService albumService = AlbumService(
apiService,
userService,
syncSerive,
entityService,
albumRepository,
assetRepository,
userRepository,
backupAlbumRepository,
albumMediaRepository,
albumApiRepository,
);
BackupService backupService = BackupService(
apiService,

View File

@ -0,0 +1,52 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
class EntityService {
final IAssetRepository _assetRepository;
final IUserRepository _userRepository;
EntityService(
this._assetRepository,
this._userRepository,
);
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
final ownerId = album.ownerId;
if (ownerId != null) {
// replace owner with user from database
album.owner.value = await _userRepository.get(ownerId);
}
final thumbnailAssetId =
album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId;
if (thumbnailAssetId != null) {
// set thumbnail with asset from database
album.thumbnail.value =
await _assetRepository.getByRemoteId(thumbnailAssetId);
}
if (album.remoteUsers.isNotEmpty) {
// replace all users with users from database
final users = await _userRepository
.getByIds(album.remoteUsers.map((user) => user.id).toList());
album.sharedUsers.clear();
album.sharedUsers.addAll(users);
}
if (album.remoteAssets.isNotEmpty) {
// replace all assets with assets from database
final assets = await _assetRepository
.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!));
album.assets.clear();
album.assets.addAll(assets);
}
return album;
}
}
final entityServiceProvider = Provider(
(ref) => EntityService(
ref.watch(assetRepositoryProvider),
ref.watch(userRepositoryProvider),
),
);

View File

@ -8,9 +8,12 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
@ -18,24 +21,33 @@ import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final syncServiceProvider = Provider(
(ref) => SyncService(
ref.watch(dbProvider),
ref.watch(hashServiceProvider),
ref.watch(entityServiceProvider),
ref.watch(albumMediaRepositoryProvider),
ref.watch(albumApiRepositoryProvider),
),
);
class SyncService {
final Isar _db;
final HashService _hashService;
final EntityService _entityService;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService');
SyncService(this._db, this._hashService, this._albumMediaRepository);
SyncService(
this._db,
this._hashService,
this._entityService,
this._albumMediaRepository,
this._albumApiRepository,
);
// public methods:
@ -65,11 +77,10 @@ class SyncService {
/// Syncs remote albums to the database
/// returns `true` if there were any changes
Future<bool> syncRemoteAlbumsToDb(
List<AlbumResponseDto> remote, {
List<Album> remote, {
required bool isShared,
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
}) =>
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
/// Syncs all device albums and their assets to the database
/// Returns `true` if there were any changes
@ -289,11 +300,10 @@ class SyncService {
/// Syncs remote albums to the database
/// returns `true` if there were any changes
Future<bool> _syncRemoteAlbumsToDb(
List<AlbumResponseDto> remote,
List<Album> remoteAlbums,
bool isShared,
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
) async {
remote.sortBy((e) => e.id);
remoteAlbums.sortBy((e) => e.remoteId!);
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
@ -310,14 +320,14 @@ class SyncService {
final List<Asset> existing = [];
final bool changes = await diffSortedLists(
remote,
remoteAlbums,
dbAlbums,
compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
both: (AlbumResponseDto a, Album b) =>
_syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
onlyFirst: (AlbumResponseDto a) =>
_addAlbumFromServer(a, existing, loadDetails),
onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
compare: (remoteAlbum, dbAlbum) =>
remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!),
both: (remoteAlbum, dbAlbum) =>
_syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing),
onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing),
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
);
if (isShared && toDelete.isNotEmpty) {
@ -338,26 +348,22 @@ class SyncService {
/// syncing changes from local back to server)
/// accumulates
Future<bool> _syncRemoteAlbum(
AlbumResponseDto dto,
Album dto,
Album album,
List<Asset> deleteCandidates,
List<Asset> existing,
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
) async {
if (!_hasAlbumResponseDtoChanged(dto, album)) {
if (!_hasRemoteAlbumChanged(dto, album)) {
return false;
}
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
// i.e. it will always be null. Save it here.
final originalDto = dto;
dto = await loadDetails(dto);
if (dto.assetCount != dto.assets.length) {
return false;
}
dto = await _albumApiRepository.get(dto.remoteId!);
final assetsInDb =
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
final List<Asset> assetsOnRemote = dto.getAssets();
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
final (toAdd, toUpdate, toUnlink) = _diffAssets(
assetsOnRemote,
@ -368,15 +374,16 @@ class SyncService {
// update shared users
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
final List<User> users = dto.remoteUsers.toList()
..sort((a, b) => a.id.compareTo(b.id));
final List<String> userIdsToAdd = [];
final List<User> usersToUnlink = [];
diffSortedListsSync(
dto.albumUsers,
users,
sharedUsers,
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
compare: (User a, User b) => a.id.compareTo(b.id),
both: (a, b) => false,
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
onlyFirst: (User a) => userIdsToAdd.add(a.id),
onlySecond: (User a) => usersToUnlink.add(a),
);
@ -386,19 +393,19 @@ class SyncService {
final assetsToLink = existingInDb + updated;
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
album.name = dto.albumName;
album.name = dto.name;
album.shared = dto.shared;
album.createdAt = dto.createdAt;
album.modifiedAt = dto.updatedAt;
album.modifiedAt = dto.modifiedAt;
album.startDate = dto.startDate;
album.endDate = dto.endDate;
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
album.shared = dto.shared;
album.activityEnabled = dto.isActivityEnabled;
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
album.activityEnabled = dto.activityEnabled;
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
album.thumbnail.value = await _db.assets
.where()
.remoteIdEqualTo(dto.albumThumbnailAssetId)
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
.findFirst();
}
@ -434,27 +441,26 @@ class SyncService {
/// (shared) assets to the database beforehand
/// accumulates assets already existing in the database
Future<void> _addAlbumFromServer(
AlbumResponseDto dto,
Album album,
List<Asset> existing,
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
) async {
if (dto.assetCount != dto.assets.length) {
dto = await loadDetails(dto);
if (album.remoteAssetCount != album.remoteAssets.length) {
album = await _albumApiRepository.get(album.remoteId!);
}
if (dto.assetCount == dto.assets.length) {
if (album.remoteAssetCount == album.remoteAssets.length) {
// in case an album contains assets not yet present in local DB:
// put missing album assets into local DB
final (existingInDb, updated) =
await _linkWithExistingFromDb(dto.getAssets());
await _linkWithExistingFromDb(album.remoteAssets.toList());
existing.addAll(existingInDb);
await upsertAssetsWithExif(updated);
final Album a = await Album.remote(dto);
await _db.writeTxn(() => _db.albums.store(a));
await _entityService.fillAlbumWithDatabaseEntities(album);
await _db.writeTxn(() => _db.albums.store(album));
} else {
_log.warning(
"Failed to add album from server: assetCount ${dto.assetCount} != "
"asset array length ${dto.assets.length} for album ${dto.albumName}");
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
"asset array length ${album.remoteAssets.length} for album ${album.name}");
}
}
@ -919,17 +925,17 @@ class SyncService {
}
/// returns `true` if the albums differ on the surface
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
return dto.assetCount != a.assetCount ||
dto.albumName != a.name ||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
dto.shared != a.shared ||
dto.albumUsers.length != a.sharedUsers.length ||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
remoteAlbum.name != dbAlbum.name ||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
remoteAlbum.shared != dbAlbum.shared ||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
!remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
!isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
!isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
!isAtSameMomentAs(
dto.lastModifiedAssetTimestamp,
a.lastModifiedAssetTimestamp,
remoteAlbum.lastModifiedAssetTimestamp,
dbAlbum.lastModifiedAssetTimestamp,
);
}