From 48927f5fb90d06dccb066f103c67853f84b3606e Mon Sep 17 00:00:00 2001 From: Andreas Gerstmayr Date: Mon, 13 May 2024 15:28:57 +0200 Subject: [PATCH] feat(server, web): include pictures of shared albums on map (#7439) * feat(server, web): include pictures of shared albums on map * run prettier * re-create api clients * implement suggestions from code review * shared from partner -> shared from partners * rename to 'include shared partner assets' * chore: fix tsc error in server and prettier in web * fix: include assets shared via owner albums --------- Co-authored-by: Zack Pollard Co-authored-by: Jason Rasmussen --- mobile/openapi/doc/AssetApi.md | 6 ++-- mobile/openapi/lib/api/asset_api.dart | 13 ++++++-- mobile/openapi/test/asset_api_test.dart | 2 +- open-api/immich-openapi-specs.json | 8 +++++ open-api/typescript-sdk/src/fetch-client.ts | 6 ++-- server/src/dtos/search.dto.ts | 3 ++ server/src/interfaces/asset.interface.ts | 2 +- server/src/repositories/asset.repository.ts | 32 ++++++++++++------- server/src/services/asset.service.spec.ts | 5 +++ server/src/services/asset.service.ts | 16 +++++++++- .../map-page/map-settings-modal.svelte | 7 +++- web/src/lib/stores/preferences.store.ts | 2 ++ .../[[assetId=id]]/+page.svelte | 3 +- 13 files changed, 81 insertions(+), 24 deletions(-) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index d710ef926a..a1491c79a2 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -500,7 +500,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getMapMarkers** -> List getMapMarkers(fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners) +> List getMapMarkers(fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners, withSharedAlbums) @@ -528,9 +528,10 @@ final fileCreatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final isArchived = true; // bool | final isFavorite = true; // bool | final withPartners = true; // bool | +final withSharedAlbums = true; // bool | try { - final result = api_instance.getMapMarkers(fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners); + final result = api_instance.getMapMarkers(fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners, withSharedAlbums); print(result); } catch (e) { print('Exception when calling AssetApi->getMapMarkers: $e\n'); @@ -546,6 +547,7 @@ Name | Type | Description | Notes **isArchived** | **bool**| | [optional] **isFavorite** | **bool**| | [optional] **withPartners** | **bool**| | [optional] + **withSharedAlbums** | **bool**| | [optional] ### Return type diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 6515046869..0363ee73b0 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -522,7 +522,9 @@ class AssetApi { /// * [bool] isFavorite: /// /// * [bool] withPartners: - Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, }) async { + /// + /// * [bool] withSharedAlbums: + Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { // ignore: prefer_const_declarations final path = r'/asset/map-marker'; @@ -548,6 +550,9 @@ class AssetApi { if (withPartners != null) { queryParams.addAll(_queryParams('', 'withPartners', withPartners)); } + if (withSharedAlbums != null) { + queryParams.addAll(_queryParams('', 'withSharedAlbums', withSharedAlbums)); + } const contentTypes = []; @@ -574,8 +579,10 @@ class AssetApi { /// * [bool] isFavorite: /// /// * [bool] withPartners: - Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, }) async { - final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, ); + /// + /// * [bool] withSharedAlbums: + Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { + final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 0a278daa32..aa6fa6c278 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -65,7 +65,7 @@ void main() { // TODO }); - //Future> getMapMarkers({ DateTime fileCreatedAfter, DateTime fileCreatedBefore, bool isArchived, bool isFavorite, bool withPartners }) async + //Future> getMapMarkers({ DateTime fileCreatedAfter, DateTime fileCreatedBefore, bool isArchived, bool isFavorite, bool withPartners, bool withSharedAlbums }) async test('test getMapMarkers', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index eea90fb1c9..8cfa31c3c6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1386,6 +1386,14 @@ "schema": { "type": "boolean" } + }, + { + "name": "withSharedAlbums", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } } ], "responses": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 23b3b00bed..2db110fa15 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1442,12 +1442,13 @@ export function runAssetJobs({ assetJobsDto }: { body: assetJobsDto }))); } -export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners }: { +export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners, withSharedAlbums }: { fileCreatedAfter?: string; fileCreatedBefore?: string; isArchived?: boolean; isFavorite?: boolean; withPartners?: boolean; + withSharedAlbums?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -1457,7 +1458,8 @@ export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, fileCreatedBefore, isArchived, isFavorite, - withPartners + withPartners, + withSharedAlbums }))}`, { ...opts })); diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 31d4195e7a..4d05b9f3aa 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -317,6 +317,9 @@ export class MapMarkerDto { @ValidateBoolean({ optional: true }) withPartners?: boolean; + + @ValidateBoolean({ optional: true }) + withSharedAlbums?: boolean; } export class MemoryLaneDto { diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 2c8f077cfb..79d90dde67 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -183,7 +183,7 @@ export interface IAssetRepository { softDeleteAll(ids: string[]): Promise; restoreAll(ids: string[]): Promise; findLivePhotoMatch(options: LivePhotoSearchOptions): Promise; - getMapMarkers(ownerIds: string[], options?: MapMarkerSearchOptions): Promise; + getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise; getStatistics(ownerId: string, options: AssetStatsOptions): Promise; getTimeBuckets(options: TimeBucketOptions): Promise; getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise; diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 7c359aa895..f9ed1c468c 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -490,9 +490,24 @@ export class AssetRepository implements IAssetRepository { }); } - async getMapMarkers(ownerIds: string[], options: MapMarkerSearchOptions = {}): Promise { + async getMapMarkers( + ownerIds: string[], + albumIds: string[], + options: MapMarkerSearchOptions = {}, + ): Promise { const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options; + const where = { + isVisible: true, + isArchived, + exifInfo: { + latitude: Not(IsNull()), + longitude: Not(IsNull()), + }, + isFavorite, + fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore), + }; + const assets = await this.repository.find({ select: { id: true, @@ -504,17 +519,10 @@ export class AssetRepository implements IAssetRepository { longitude: true, }, }, - where: { - ownerId: In([...ownerIds]), - isVisible: true, - isArchived, - exifInfo: { - latitude: Not(IsNull()), - longitude: Not(IsNull()), - }, - isFavorite, - fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore), - }, + where: [ + { ...where, ownerId: In([...ownerIds]) }, + { ...where, albums: { id: In([...albumIds]) } }, + ], relations: { exifInfo: true, }, diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 5a61f70da8..2673e2436d 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -2,6 +2,7 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; @@ -18,6 +19,7 @@ import { faceStub } from 'test/fixtures/face.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; import { userStub } from 'test/fixtures/user.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; @@ -160,6 +162,7 @@ describe(AssetService.name, () => { let configMock: Mocked; let partnerMock: Mocked; let assetStackMock: Mocked; + let albumMock: Mocked; let loggerMock: Mocked; it('should work', () => { @@ -182,6 +185,7 @@ describe(AssetService.name, () => { configMock = newSystemConfigRepositoryMock(); partnerMock = newPartnerRepositoryMock(); assetStackMock = newAssetStackRepositoryMock(); + albumMock = newAlbumRepositoryMock(); loggerMock = newLoggerRepositoryMock(); sut = new AssetService( @@ -194,6 +198,7 @@ describe(AssetService.name, () => { eventMock, partnerMock, assetStackMock, + albumMock, loggerMock, ); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 5ffa940e7b..b1eff50a93 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -29,6 +29,7 @@ import { UpdateStackParentDto } from 'src/dtos/stack.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { LibraryType } from 'src/entities/library.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; @@ -78,6 +79,7 @@ export class AssetService { @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository, + @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetService.name); @@ -167,6 +169,7 @@ export class AssetService { async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise { const userIds: string[] = [auth.user.id]; + // TODO convert to SQL join if (options.withPartners) { const partners = await this.partnerRepository.getAll(auth.user.id); const partnersIds = partners @@ -174,7 +177,18 @@ export class AssetService { .map((partner) => partner.sharedById); userIds.push(...partnersIds); } - return this.assetRepository.getMapMarkers(userIds, options); + + // TODO convert to SQL join + const albumIds: string[] = []; + if (options.withSharedAlbums) { + const [ownedAlbums, sharedAlbums] = await Promise.all([ + this.albumRepository.getOwned(auth.user.id), + this.albumRepository.getShared(auth.user.id), + ]); + albumIds.push(...ownedAlbums.map((album) => album.id), ...sharedAlbums.map((album) => album.id)); + } + + return this.assetRepository.getMapMarkers(userIds, albumIds, options); } async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise { diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte index 460363722e..aded865a06 100644 --- a/web/src/lib/components/map-page/map-settings-modal.svelte +++ b/web/src/lib/components/map-page/map-settings-modal.svelte @@ -30,7 +30,12 @@ - + + {#if customDateRange}
diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 591802f488..3e4bf09b28 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -47,6 +47,7 @@ export interface MapSettings { includeArchived: boolean; onlyFavorites: boolean; withPartners: boolean; + withSharedAlbums: boolean; relativeDate: string; dateAfter: string; dateBefore: string; @@ -57,6 +58,7 @@ export const mapSettings = persisted('map-settings', { includeArchived: false, onlyFavorites: false, withPartners: false, + withSharedAlbums: false, relativeDate: '', dateAfter: '', dateBefore: '', diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 593250a7c9..1b5923663b 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -47,7 +47,7 @@ } abortController = new AbortController(); - const { includeArchived, onlyFavorites, withPartners } = $mapSettings; + const { includeArchived, onlyFavorites, withPartners, withSharedAlbums } = $mapSettings; const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates(); return await getMapMarkers( @@ -57,6 +57,7 @@ fileCreatedAfter: fileCreatedAfter || undefined, fileCreatedBefore, withPartners: withPartners || undefined, + withSharedAlbums: withSharedAlbums || undefined, }, { signal: abortController.signal,