From cab5477656a9c6677dcc01af87e1e953c779a61b Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sun, 4 Jun 2023 04:41:27 +0200 Subject: [PATCH] fix(web+server): showing assets without thumbnail (#2652) * fix(web+server): showing assets without thumbnail * missed change --- mobile/openapi/doc/AssetApi.md | Bin 58005 -> 58186 bytes .../doc/GetAssetCountByTimeBucketDto.md | Bin 495 -> 574 bytes mobile/openapi/lib/api/asset_api.dart | Bin 52128 -> 52471 bytes .../get_asset_count_by_time_bucket_dto.dart | Bin 4229 -> 5018 bytes mobile/openapi/test/asset_api_test.dart | Bin 5582 -> 5602 bytes ...t_asset_count_by_time_bucket_dto_test.dart | Bin 714 -> 864 bytes .../src/api-v1/asset/asset-repository.ts | 51 +++++++++--------- .../immich/src/api-v1/asset/asset.service.ts | 2 +- .../src/api-v1/asset/dto/asset-search.dto.ts | 8 +++ .../dto/get-asset-count-by-time-bucket.dto.ts | 12 ++++- server/immich-openapi-specs.json | 13 +++++ web/src/api/open-api/api.ts | 32 ++++++++--- .../components/photos-page/asset-grid.svelte | 3 +- .../side-bar/side-bar.svelte | 5 +- web/src/routes/(user)/archive/+page.svelte | 5 +- web/src/routes/(user)/favorites/+page.svelte | 5 +- 16 files changed, 98 insertions(+), 38 deletions(-) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 7ddea756c0c49d7328ca46a825e4fdb476e7fdc1..4b6171d61ab4751fd9a986a49ffbc7703d3140be 100644 GIT binary patch delta 170 zcmbPwl=;*#<_-5lc*`?OGV)7HLNZEolZrRr4SB(WDl}Q}q_CoAUUE)pN~%I)adB!% zu>w?&LJ3GyUSeiW@#KAx7g6N3Py$k%mzbGTJlUFY3jhS!8*l&s delta 11 ScmdnT@}7Bv7vtoaj7tF=D+Gc7 diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index ff272d4c2738b3a31aa6ad6c9c59579852c837ad..8b0eaa2be87577e1f4f4f7b61bbeaf50d549e308 100644 GIT binary patch delta 291 zcmZ25o%#Dr<_*(bCOdSpS(Im%WaO8Ygk+TFCKX$8DJbac>nkWIc;+SNl%}LABo-H^ zmJ};MRVkE!l;tI6<`hpB@T!`e&BQgC+tpt2u6i&*|&#{NeLd=+)#Jo*cK~0TI0R-|(GE$3BEm5$w zMbja{GRaUu8>~SC#XTUUd8Iiy3ib*H3RY;k=Cc&b>7a_)s-S3}EX4XnEHh0(1JkFI z=dwzoDprqGu*DK2lh3fq^P-r#`6H_ui?sr(%~lGzi3MSaIi;!oX(5#bsWzaHvD3&Z R&d<|PC{jl?fS0R+4FCoRjwJv9 delta 38 wcmV+>0NMYVCxs!f)B&^X0h0lf90jqH`UR4c$OdSWP6za})(8;; getDetectedObjectsByUserId(userId: string): Promise; getSearchPropertiesByUserId(userId: string): Promise; - getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise; + getAssetCountByTimeBucket(userId: string, dto: GetAssetCountByTimeBucketDto): Promise; getAssetCountByUserId(userId: string): Promise; getArchivedAssetCountByUserId(userId: string): Promise; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; @@ -119,36 +119,35 @@ export class AssetRepository implements IAssetRepository { return builder.getMany(); } - async getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum) { - let result: AssetCountByTimeBucket[] = []; + async getAssetCountByTimeBucket( + userId: string, + dto: GetAssetCountByTimeBucketDto, + ): Promise { + const builder = this.assetRepository + .createQueryBuilder('asset') + .select(`COUNT(asset.id)::int`, 'count') + .where('"ownerId" = :userId', { userId: userId }) + .andWhere('asset.isVisible = true') + .andWhere('asset.isArchived = false'); - if (timeBucket === TimeGroupEnum.Month) { - result = await this.assetRepository - .createQueryBuilder('asset') - .select(`COUNT(asset.id)::int`, 'count') + // Using a parameter for this doesn't work https://github.com/typeorm/typeorm/issues/7308 + if (dto.timeGroup === TimeGroupEnum.Month) { + builder .addSelect(`date_trunc('month', "fileCreatedAt")`, 'timeBucket') - .where('"ownerId" = :userId', { userId: userId }) - .andWhere('asset.resizePath is not NULL') - .andWhere('asset.isVisible = true') - .andWhere('asset.isArchived = false') .groupBy(`date_trunc('month', "fileCreatedAt")`) - .orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC') - .getRawMany(); - } else if (timeBucket === TimeGroupEnum.Day) { - result = await this.assetRepository - .createQueryBuilder('asset') - .select(`COUNT(asset.id)::int`, 'count') + .orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC'); + } else if (dto.timeGroup === TimeGroupEnum.Day) { + builder .addSelect(`date_trunc('day', "fileCreatedAt")`, 'timeBucket') - .where('"ownerId" = :userId', { userId: userId }) - .andWhere('asset.resizePath is not NULL') - .andWhere('asset.isVisible = true') - .andWhere('asset.isArchived = false') .groupBy(`date_trunc('day', "fileCreatedAt")`) - .orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC') - .getRawMany(); + .orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC'); } - return result; + if (!dto.withoutThumbs) { + builder.andWhere('asset.resizePath is not NULL'); + } + + return builder.getRawMany(); } async getSearchPropertiesByUserId(userId: string): Promise { @@ -231,7 +230,7 @@ export class AssetRepository implements IAssetRepository { return this.assetRepository.find({ where: { ownerId, - resizePath: Not(IsNull()), + resizePath: dto.withoutThumbs ? undefined : Not(IsNull()), isVisible: true, isFavorite: dto.isFavorite, isArchived: dto.isArchived, diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index dfc62f0c98..f0fe84d717 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -533,7 +533,7 @@ export class AssetService { const result = await this._assetRepository.getAssetCountByTimeBucket( getAssetCountByTimeBucketDto.userId || authUser.id, - getAssetCountByTimeBucketDto.timeGroup, + getAssetCountByTimeBucketDto, ); return mapAssetCountByTimeBucket(result); diff --git a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts index 84bededd7b..8a908e746d 100644 --- a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts @@ -16,6 +16,14 @@ export class AssetSearchDto { @Transform(toBoolean) isArchived?: boolean; + /** + * Include assets without thumbnails + */ + @IsOptional() + @IsBoolean() + @Transform(toBoolean) + withoutThumbs?: boolean; + @IsOptional() @IsNumber() skip?: number; diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts index 58104d5d68..d4e6c3c45a 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { toBoolean } from '../../../utils/transform.util'; export enum TimeGroupEnum { Day = 'day', @@ -19,4 +21,12 @@ export class GetAssetCountByTimeBucketDto { @IsUUID('4') @ApiProperty({ format: 'uuid' }) userId?: string; + + /** + * Include assets without thumbnails + */ + @IsOptional() + @IsBoolean() + @Transform(toBoolean) + withoutThumbs?: boolean; } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 36a4dad1d8..41c4a4669b 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3380,6 +3380,15 @@ "type": "boolean" } }, + { + "name": "withoutThumbs", + "required": false, + "in": "query", + "description": "Include assets without thumbnails", + "schema": { + "type": "boolean" + } + }, { "name": "skip", "required": false, @@ -6221,6 +6230,10 @@ "userId": { "type": "string", "format": "uuid" + }, + "withoutThumbs": { + "type": "boolean", + "description": "Include assets without thumbnails" } }, "required": [ diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index b777df42c1..6ec6ffcb37 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1396,6 +1396,12 @@ export interface GetAssetCountByTimeBucketDto { * @memberof GetAssetCountByTimeBucketDto */ 'userId'?: string; + /** + * Include assets without thumbnails + * @type {boolean} + * @memberof GetAssetCountByTimeBucketDto + */ + 'withoutThumbs'?: boolean; } @@ -4999,12 +5005,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] + * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { + getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5038,6 +5045,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['isArchived'] = isArchived; } + if (withoutThumbs !== undefined) { + localVarQueryParameter['withoutThumbs'] = withoutThumbs; + } + if (skip !== undefined) { localVarQueryParameter['skip'] = skip; } @@ -5970,13 +5981,14 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] + * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options); + async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6259,13 +6271,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {string} [userId] * @param {boolean} [isFavorite] * @param {boolean} [isArchived] + * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { - return localVarFp.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); + getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { + return localVarFp.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); }, /** * @@ -6627,6 +6640,13 @@ export interface AssetApiGetAllAssetsRequest { */ readonly isArchived?: boolean + /** + * Include assets without thumbnails + * @type {boolean} + * @memberof AssetApiGetAllAssets + */ + readonly withoutThumbs?: boolean + /** * * @type {number} @@ -7071,7 +7091,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 0adbe217aa..65b0d09d56 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -29,7 +29,8 @@ const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({ getAssetCountByTimeBucketDto: { timeGroup: TimeGroupEnum.Month, - userId: user?.id + userId: user?.id, + withoutThumbs: true } }); bucketInfo = assetCountByTimebucket; diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 92e8e47191..92db34f738 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -30,7 +30,10 @@ const getFavoriteCount = async () => { try { - const { data: assets } = await api.assetApi.getAllAssets({ isFavorite: true }); + const { data: assets } = await api.assetApi.getAllAssets({ + isFavorite: true, + withoutThumbs: true + }); return { favorites: assets.length diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte index bcbb649297..2617d77bf6 100644 --- a/web/src/routes/(user)/archive/+page.svelte +++ b/web/src/routes/(user)/archive/+page.svelte @@ -26,7 +26,10 @@ onMount(async () => { try { - const { data: assets } = await api.assetApi.getAllAssets({ isArchived: true }); + const { data: assets } = await api.assetApi.getAllAssets({ + isArchived: true, + withoutThumbs: true + }); $archivedAsset = assets; } catch { handleError(Error, 'Unable to load archived assets'); diff --git a/web/src/routes/(user)/favorites/+page.svelte b/web/src/routes/(user)/favorites/+page.svelte index 7b2ad8054b..8efeb1b866 100644 --- a/web/src/routes/(user)/favorites/+page.svelte +++ b/web/src/routes/(user)/favorites/+page.svelte @@ -28,7 +28,10 @@ onMount(async () => { try { - const { data: assets } = await api.assetApi.getAllAssets({ isFavorite: true }); + const { data: assets } = await api.assetApi.getAllAssets({ + isFavorite: true, + withoutThumbs: true + }); favorites = assets; } catch { handleError(Error, 'Unable to load favorites');