diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 0e35be2ee0..320ad640c6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10824,6 +10824,13 @@ }, "MetadataSearchDto": { "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, "checksum": { "type": "string" }, @@ -11743,6 +11750,13 @@ }, "RandomSearchDto": { "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, "city": { "nullable": true, "type": "string" @@ -12780,6 +12794,13 @@ }, "SmartSearchDto": { "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, "city": { "nullable": true, "type": "string" diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 579cba680e..2bca9a13f2 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -108,6 +108,9 @@ class BaseSearchDto { @ValidateUUID({ each: true, optional: true }) tagIds?: string[]; + @ValidateUUID({each: true, optional: true}) + albumIds?: string[]; + @Optional() @IsInt() @Max(5) diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 747a59c65b..55545659c3 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -91,6 +91,10 @@ export interface SearchTagOptions { tagIds?: string[]; } +export interface SearchAlbumOptions { + albumIds?: string[]; +} + export interface SearchOrderOptions { orderDirection?: 'asc' | 'desc'; } @@ -108,7 +112,8 @@ type BaseAssetSearchOptions = SearchDateOptions & SearchStatusOptions & SearchUserIdOptions & SearchPeopleOptions & - SearchTagOptions; + SearchTagOptions & + SearchAlbumOptions; export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 5e5c6c5fb4..a1a5507f22 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -228,6 +228,20 @@ export function hasPeople(qb: SelectQueryBuilder, personIds: ); } +export function inAlbums(qb: SelectQueryBuilder, albumIds: string[]) { + return qb.innerJoin( + (eb) => + eb + .selectFrom('albums_assets_assets') + .select('assetsId') + .where('albumsId', '=', anyUuid(albumIds!)) + .groupBy('assetsId') + .having((eb) => eb.fn.count('albumsId').distinct(), '=', albumIds.length) + .as('has_album'), + (join) => join.onRef('has_album.assetsId', '=', 'assets.id'), + ); +} + export function hasTags(qb: SelectQueryBuilder, tagIds: string[]) { return qb.innerJoin( (eb) => @@ -293,6 +307,7 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .selectFrom('assets') .selectAll('assets') .where('assets.visibility', '=', visibility) + .$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!)) .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) @@ -369,7 +384,7 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .$if(options.isMotion !== undefined, (qb) => qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), ) - .$if(!!options.isNotInAlbum, (qb) => + .$if(!!options.isNotInAlbum && (!options.albumIds || options.albumIds.length == 0), (qb) => qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))), ),