diff --git a/mobile/lib/domain/services/sync_linked_album.service.dart b/mobile/lib/domain/services/sync_linked_album.service.dart index 3bc76083b8..ff60e0ef74 100644 --- a/mobile/lib/domain/services/sync_linked_album.service.dart +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -39,24 +39,35 @@ class SyncLinkedAlbumService { await Future.wait( selectedAlbums.map((localAlbum) async { - final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId; - if (linkedRemoteAlbumId == null) { - _log.warning("No linked remote album ID found for local album: ${localAlbum.name}"); - return; - } + try { + final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId; + if (linkedRemoteAlbumId == null) { + _log.warning("No linked remote album ID found for local album: ${localAlbum.name}"); + return; + } - final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId); - if (remoteAlbum == null) { - _log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId"); - return; - } + final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId); + if (remoteAlbum == null) { + _log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId"); + return; + } - // get assets that are uploaded but not in the remote album - final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId); - _log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}"); - if (assetIds.isNotEmpty) { - final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds); - await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added); + // get assets that are uploaded but not in the remote album + final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId); + _log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}"); + if (assetIds.isNotEmpty) { + final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds); + await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added); + } + } on RemoteAlbumNotFoundException catch (e) { + // server doesn't have the linked album anymore. drop the cached row; + // KeyAction.setNull on LocalAlbumEntity.linkedRemoteAlbumId nulls + // the link via FK cascade, and the next manageLinkedAlbums run + // will recreate or re-link by name. + _log.warning("Pruning stale linked album for ${localAlbum.name} (server returned 'Album not found' for ${e.albumId})"); + await _remoteAlbumRepository.deleteAlbum(e.albumId); + } catch (error, stack) { + _log.severe("Linked album sync failed for ${localAlbum.name}", error, stack); } }), ); diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index a0c7a3732a..2ae6e23891 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -42,17 +42,24 @@ class DriftAlbumApiRepository extends ApiRepository { } Future<({List added, List failed})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List added = [], failed = []; - for (final dto in response) { - if (dto.success) { - added.add(dto.id); - } else { - failed.add(dto.id); + try { + final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); + final List added = [], failed = []; + for (final dto in response) { + if (dto.success) { + added.add(dto.id); + } else { + failed.add(dto.id); + } } - } - return (added: added, failed: failed); + return (added: added, failed: failed); + } on ApiException catch (e) { + if (e.code == 400 && (e.message?.contains('"message":"Album not found"') ?? false)) { + throw RemoteAlbumNotFoundException(albumId); + } + rethrow; + } } Future updateAlbum( @@ -104,6 +111,14 @@ class DriftAlbumApiRepository extends ApiRepository { } } +class RemoteAlbumNotFoundException implements Exception { + final String albumId; + const RemoteAlbumNotFoundException(this.albumId); + + @override + String toString() => 'RemoteAlbumNotFoundException: $albumId'; +} + extension on AlbumResponseDto { RemoteAlbum toRemoteAlbum(final UserDto user) { return RemoteAlbum(