mirror of
https://github.com/immich-app/immich.git
synced 2025-07-08 10:44:15 -04:00
fix removeAlbum + tests
This commit is contained in:
parent
197e3d8f65
commit
ca42420ca3
@ -5,7 +5,8 @@ import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
|
|||||||
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/local_album_asset.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/local_album_asset.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/asset.model.dart'
|
||||||
|
hide AssetType;
|
||||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@ -114,18 +115,43 @@ class SyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeLocalAlbum(LocalAlbum album) async {
|
Future<void> removeLocalAlbum(LocalAlbum a) async {
|
||||||
_log.info("Removing device album ${album.name}");
|
_log.info("Removing device album ${a.name}");
|
||||||
try {
|
try {
|
||||||
|
// Do not request title to speed things up on iOS
|
||||||
|
final filter = albumFilter;
|
||||||
|
filter.setOption(
|
||||||
|
AssetType.image,
|
||||||
|
filter.getOption(AssetType.image).copyWith(needTitle: false),
|
||||||
|
);
|
||||||
|
filter.setOption(
|
||||||
|
AssetType.video,
|
||||||
|
filter.getOption(AssetType.video).copyWith(needTitle: false),
|
||||||
|
);
|
||||||
|
final deviceAlbum =
|
||||||
|
await _albumMediaRepository.refresh(a.id, filter: filter);
|
||||||
|
final assetsToDelete =
|
||||||
|
(await _localAlbumAssetRepository.getAssetsForAlbum(deviceAlbum.id))
|
||||||
|
.map((asset) => asset.localId)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
// Remove all assets that are only in this particular album
|
// Remove all assets that are only in this particular album
|
||||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||||
final assetsToDelete =
|
final assetsOnlyInAlbum = assetsToDelete.isEmpty
|
||||||
await _localAlbumRepository.getAssetIdsOnlyInAlbum(album.id);
|
? <String>{}
|
||||||
|
: (await _localAlbumRepository.getAssetIdsOnlyInAlbum(deviceAlbum.id))
|
||||||
|
.toSet();
|
||||||
await _localAlbumRepository.transaction(() async {
|
await _localAlbumRepository.transaction(() async {
|
||||||
if (assetsToDelete.isNotEmpty) {
|
// Delete all assets that are only in this particular album
|
||||||
await _localAssetRepository.deleteIds(assetsToDelete);
|
await _localAssetRepository.deleteIds(
|
||||||
}
|
assetsToDelete.intersection(assetsOnlyInAlbum),
|
||||||
await _localAlbumRepository.delete(album.id);
|
);
|
||||||
|
// Unlink the others
|
||||||
|
await _localAlbumAssetRepository.unlinkAssetsFromAlbum(
|
||||||
|
deviceAlbum.id,
|
||||||
|
assetsToDelete.difference(assetsOnlyInAlbum),
|
||||||
|
);
|
||||||
|
await _localAlbumRepository.delete(deviceAlbum.id);
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_log.warning("Error while removing device album", e, s);
|
_log.warning("Error while removing device album", e, s);
|
||||||
@ -312,7 +338,8 @@ class SyncService {
|
|||||||
backupSelection: dbAlbum.backupSelection,
|
backupSelection: dbAlbum.backupSelection,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only query for assets unique to album if we have assets to delete
|
// Remove all assets that are only in this particular album
|
||||||
|
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||||
final assetsOnlyInAlbum = assetsToDelete.isEmpty
|
final assetsOnlyInAlbum = assetsToDelete.isEmpty
|
||||||
? <String>{}
|
? <String>{}
|
||||||
: (await _localAlbumRepository.getAssetIdsOnlyInAlbum(deviceAlbum.id))
|
: (await _localAlbumRepository.getAssetIdsOnlyInAlbum(deviceAlbum.id))
|
||||||
@ -326,8 +353,7 @@ class SyncService {
|
|||||||
assetsToAdd.map((a) => a.localId),
|
assetsToAdd.map((a) => a.localId),
|
||||||
);
|
);
|
||||||
await _localAlbumRepository.upsert(updatedAlbum);
|
await _localAlbumRepository.upsert(updatedAlbum);
|
||||||
// Remove all assets that are only in this particular album
|
// Delete all assets that are only in this particular album
|
||||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
|
||||||
await _localAssetRepository.deleteIds(
|
await _localAssetRepository.deleteIds(
|
||||||
assetsToDelete.intersection(assetsOnlyInAlbum),
|
assetsToDelete.intersection(assetsOnlyInAlbum),
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
@ -120,8 +121,16 @@ void main() {
|
|||||||
when(() => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any()))
|
when(() => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any()))
|
||||||
.thenAnswer((_) async => {});
|
.thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
when(() => mockLocalAlbumAssetRepo.unlinkAssetsFromAlbum(any(), any()))
|
||||||
|
.thenAnswer((_) async => {});
|
||||||
|
|
||||||
when(() => mockLocalAlbumRepo.upsert(any())).thenAnswer((_) async => {});
|
when(() => mockLocalAlbumRepo.upsert(any())).thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
when(() => mockLocalAlbumRepo.delete(any())).thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
when(() => mockLocalAssetRepo.deleteIds(any()))
|
||||||
|
.thenAnswer((_) async => {});
|
||||||
|
|
||||||
when(() => mockLocalAlbumRepo.transaction<Null>(any()))
|
when(() => mockLocalAlbumRepo.transaction<Null>(any()))
|
||||||
.thenAnswer((_) async {
|
.thenAnswer((_) async {
|
||||||
final capturedCallback = verify(
|
final capturedCallback = verify(
|
||||||
@ -136,28 +145,18 @@ void main() {
|
|||||||
test(
|
test(
|
||||||
'album filter should be properly configured with expected settings',
|
'album filter should be properly configured with expected settings',
|
||||||
() {
|
() {
|
||||||
// Access the album filter from the service
|
|
||||||
final albumFilter = sut.albumFilter;
|
final albumFilter = sut.albumFilter;
|
||||||
|
|
||||||
// Verify image option settings
|
|
||||||
final imageOption = albumFilter.getOption(AssetType.image);
|
final imageOption = albumFilter.getOption(AssetType.image);
|
||||||
expect(imageOption.needTitle, isTrue);
|
expect(imageOption.needTitle, isTrue);
|
||||||
expect(imageOption.sizeConstraint.ignoreSize, isTrue);
|
expect(imageOption.sizeConstraint.ignoreSize, isTrue);
|
||||||
|
|
||||||
// Verify video option settings
|
|
||||||
final videoOption = albumFilter.getOption(AssetType.video);
|
final videoOption = albumFilter.getOption(AssetType.video);
|
||||||
expect(videoOption.needTitle, isTrue);
|
expect(videoOption.needTitle, isTrue);
|
||||||
expect(videoOption.sizeConstraint.ignoreSize, isTrue);
|
expect(videoOption.sizeConstraint.ignoreSize, isTrue);
|
||||||
expect(videoOption.durationConstraint.allowNullable, isTrue);
|
expect(videoOption.durationConstraint.allowNullable, isTrue);
|
||||||
|
|
||||||
// Verify containsPathModified flag
|
|
||||||
expect(albumFilter.containsPathModified, isTrue);
|
expect(albumFilter.containsPathModified, isTrue);
|
||||||
|
|
||||||
// Verify time conditions are ignored
|
|
||||||
expect(albumFilter.createTimeCond.ignore, isTrue);
|
expect(albumFilter.createTimeCond.ignore, isTrue);
|
||||||
expect(albumFilter.updateTimeCond.ignore, isTrue);
|
expect(albumFilter.updateTimeCond.ignore, isTrue);
|
||||||
|
|
||||||
// Verify ordering
|
|
||||||
expect(albumFilter.orders.length, 1);
|
expect(albumFilter.orders.length, 1);
|
||||||
expect(
|
expect(
|
||||||
albumFilter.orders.firstOrNull?.type,
|
albumFilter.orders.firstOrNull?.type,
|
||||||
@ -183,7 +182,6 @@ void main() {
|
|||||||
deviceAlbum.copyWith(updatedAt: earlier),
|
deviceAlbum.copyWith(updatedAt: earlier),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify: method returns without making any changes
|
|
||||||
expect(result, isFalse);
|
expect(result, isFalse);
|
||||||
verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(any()));
|
verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(any()));
|
||||||
verifyNever(() => mockLocalAssetRepo.upsertAll(any()));
|
verifyNever(() => mockLocalAssetRepo.upsertAll(any()));
|
||||||
@ -197,7 +195,6 @@ void main() {
|
|||||||
test(
|
test(
|
||||||
'early return when device album has fewer assets than DB album',
|
'early return when device album has fewer assets than DB album',
|
||||||
() async {
|
() async {
|
||||||
// Execute
|
|
||||||
final result = await sut.handleOnlyAssetsAdded(
|
final result = await sut.handleOnlyAssetsAdded(
|
||||||
dbAlbum,
|
dbAlbum,
|
||||||
deviceAlbum.copyWith(assetCount: dbAlbum.assetCount - 1),
|
deviceAlbum.copyWith(assetCount: dbAlbum.assetCount - 1),
|
||||||
@ -345,9 +342,9 @@ void main() {
|
|||||||
verify(() => mockLocalAlbumRepo.upsert(captureAny()));
|
verify(() => mockLocalAlbumRepo.upsert(captureAny()));
|
||||||
albumUpsertCall.called(1);
|
albumUpsertCall.called(1);
|
||||||
verify(() => mockLocalAssetRepo.upsertAll(any())).called(1);
|
verify(() => mockLocalAssetRepo.upsertAll(any())).called(1);
|
||||||
verify(
|
verify(() =>
|
||||||
() => mockLocalAlbumAssetRepo.linkAssetsToAlbum(albumId, any()),
|
mockLocalAlbumAssetRepo.linkAssetsToAlbum(albumId, any()))
|
||||||
).called(1);
|
.called(1);
|
||||||
|
|
||||||
final capturedAlbum =
|
final capturedAlbum =
|
||||||
albumUpsertCall.captured.singleOrNull as LocalAlbum?;
|
albumUpsertCall.captured.singleOrNull as LocalAlbum?;
|
||||||
@ -357,5 +354,83 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('removeLocalAlbum: ', () {
|
||||||
|
test(
|
||||||
|
'removing album with no assets correctly calls delete',
|
||||||
|
() async {
|
||||||
|
when(() => mockLocalAlbumAssetRepo.getAssetsForAlbum(albumId))
|
||||||
|
.thenAnswer((_) async => []);
|
||||||
|
when(() => mockLocalAlbumRepo.getAssetIdsOnlyInAlbum(albumId))
|
||||||
|
.thenAnswer((_) async => []);
|
||||||
|
|
||||||
|
await sut.removeLocalAlbum(deviceAlbum);
|
||||||
|
|
||||||
|
verify(() => mockLocalAssetRepo.deleteIds([])).called(1);
|
||||||
|
verify(() => mockLocalAlbumRepo.delete(albumId)).called(1);
|
||||||
|
verify(
|
||||||
|
() => mockLocalAlbumAssetRepo.unlinkAssetsFromAlbum(albumId, {}),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'removing album with assets unique to that album deletes those assets',
|
||||||
|
() async {
|
||||||
|
final uniqueAssetIds = deviceAssets.map((a) => a.localId).toList();
|
||||||
|
when(() => mockLocalAlbumRepo.getAssetIdsOnlyInAlbum(albumId))
|
||||||
|
.thenAnswer((_) async => uniqueAssetIds);
|
||||||
|
|
||||||
|
await sut.removeLocalAlbum(deviceAlbum);
|
||||||
|
|
||||||
|
verify(() => mockLocalAssetRepo.deleteIds(uniqueAssetIds)).called(1);
|
||||||
|
verify(() => mockLocalAlbumRepo.delete(albumId)).called(1);
|
||||||
|
verify(() => mockLocalAlbumAssetRepo.unlinkAssetsFromAlbum(any(), {}))
|
||||||
|
.called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'removing album with shared assets unlinks those assets',
|
||||||
|
() async {
|
||||||
|
final assetIds = deviceAssets.map((a) => a.localId).toList();
|
||||||
|
when(() => mockLocalAlbumRepo.getAssetIdsOnlyInAlbum(albumId))
|
||||||
|
.thenAnswer((_) async => []);
|
||||||
|
|
||||||
|
await sut.removeLocalAlbum(deviceAlbum);
|
||||||
|
|
||||||
|
verify(() => mockLocalAssetRepo.deleteIds([])).called(1);
|
||||||
|
verify(
|
||||||
|
() => mockLocalAlbumAssetRepo.unlinkAssetsFromAlbum(
|
||||||
|
albumId,
|
||||||
|
assetIds,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verify(() => mockLocalAlbumRepo.delete(albumId)).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'removing album with mixed assets (some unique, some shared)',
|
||||||
|
() async {
|
||||||
|
final uniqueAssetIds = ['asset-1', 'asset-2'];
|
||||||
|
final sharedAssetIds = ['asset-0', 'asset-3', 'asset-4'];
|
||||||
|
|
||||||
|
when(() => mockLocalAlbumRepo.getAssetIdsOnlyInAlbum(albumId))
|
||||||
|
.thenAnswer((_) async => uniqueAssetIds);
|
||||||
|
|
||||||
|
await sut.removeLocalAlbum(deviceAlbum);
|
||||||
|
|
||||||
|
verify(() => mockLocalAssetRepo.deleteIds(uniqueAssetIds)).called(1);
|
||||||
|
verify(
|
||||||
|
() => mockLocalAlbumAssetRepo.unlinkAssetsFromAlbum(
|
||||||
|
albumId,
|
||||||
|
sharedAssetIds,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verify(() => mockLocalAlbumRepo.delete(albumId)).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user