diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index d710ef926a..a1491c79a2 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 6515046869..0363ee73b0 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 0a278daa32..aa6fa6c278 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ 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,