diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 7ddea756c0c49..4b6171d61ab47 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -553,7 +553,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) # **getAllAssets** -> List getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch) +> List getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch) @@ -581,11 +581,12 @@ final api_instance = AssetApi(); final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final isFavorite = true; // bool | final isArchived = true; // bool | +final withoutThumbs = true; // bool | Include assets without thumbnails final skip = 8.14; // num | final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client try { - final result = api_instance.getAllAssets(userId, isFavorite, isArchived, skip, ifNoneMatch); + final result = api_instance.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch); print(result); } catch (e) { print('Exception when calling AssetApi->getAllAssets: $e\n'); @@ -599,6 +600,7 @@ Name | Type | Description | Notes **userId** | **String**| | [optional] **isFavorite** | **bool**| | [optional] **isArchived** | **bool**| | [optional] + **withoutThumbs** | **bool**| Include assets without thumbnails | [optional] **skip** | **num**| | [optional] **ifNoneMatch** | **String**| ETag of data already cached on the client | [optional] diff --git a/mobile/openapi/doc/GetAssetCountByTimeBucketDto.md b/mobile/openapi/doc/GetAssetCountByTimeBucketDto.md index e770c3f9bb578..5ba4078539a43 100644 --- a/mobile/openapi/doc/GetAssetCountByTimeBucketDto.md +++ b/mobile/openapi/doc/GetAssetCountByTimeBucketDto.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **timeGroup** | [**TimeGroupEnum**](TimeGroupEnum.md) | | **userId** | **String** | | [optional] +**withoutThumbs** | **bool** | Include assets without thumbnails | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index ff272d4c2738b..8b0eaa2be8757 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -525,11 +525,14 @@ class AssetApi { /// /// * [bool] isArchived: /// + /// * [bool] withoutThumbs: + /// Include assets without thumbnails + /// /// * [num] skip: /// /// * [String] ifNoneMatch: /// ETag of data already cached on the client - Future getAllAssetsWithHttpInfo({ String? userId, bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async { + Future getAllAssetsWithHttpInfo({ String? userId, bool? isFavorite, bool? isArchived, bool? withoutThumbs, num? skip, String? ifNoneMatch, }) async { // ignore: prefer_const_declarations final path = r'/asset'; @@ -549,6 +552,9 @@ class AssetApi { if (isArchived != null) { queryParams.addAll(_queryParams('', 'isArchived', isArchived)); } + if (withoutThumbs != null) { + queryParams.addAll(_queryParams('', 'withoutThumbs', withoutThumbs)); + } if (skip != null) { queryParams.addAll(_queryParams('', 'skip', skip)); } @@ -581,12 +587,15 @@ class AssetApi { /// /// * [bool] isArchived: /// + /// * [bool] withoutThumbs: + /// Include assets without thumbnails + /// /// * [num] skip: /// /// * [String] ifNoneMatch: /// ETag of data already cached on the client - Future?> getAllAssets({ String? userId, bool? isFavorite, bool? isArchived, num? skip, String? ifNoneMatch, }) async { - final response = await getAllAssetsWithHttpInfo( userId: userId, isFavorite: isFavorite, isArchived: isArchived, skip: skip, ifNoneMatch: ifNoneMatch, ); + Future?> getAllAssets({ String? userId, bool? isFavorite, bool? isArchived, bool? withoutThumbs, num? skip, String? ifNoneMatch, }) async { + final response = await getAllAssetsWithHttpInfo( userId: userId, isFavorite: isFavorite, isArchived: isArchived, withoutThumbs: withoutThumbs, skip: skip, ifNoneMatch: ifNoneMatch, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/get_asset_count_by_time_bucket_dto.dart b/mobile/openapi/lib/model/get_asset_count_by_time_bucket_dto.dart index 619c5fe86867a..57e2836319573 100644 --- a/mobile/openapi/lib/model/get_asset_count_by_time_bucket_dto.dart +++ b/mobile/openapi/lib/model/get_asset_count_by_time_bucket_dto.dart @@ -15,6 +15,7 @@ class GetAssetCountByTimeBucketDto { GetAssetCountByTimeBucketDto({ required this.timeGroup, this.userId, + this.withoutThumbs, }); TimeGroupEnum timeGroup; @@ -27,19 +28,30 @@ class GetAssetCountByTimeBucketDto { /// String? userId; + /// Include assets without thumbnails + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withoutThumbs; + @override bool operator ==(Object other) => identical(this, other) || other is GetAssetCountByTimeBucketDto && other.timeGroup == timeGroup && - other.userId == userId; + other.userId == userId && + other.withoutThumbs == withoutThumbs; @override int get hashCode => // ignore: unnecessary_parenthesis (timeGroup.hashCode) + - (userId == null ? 0 : userId!.hashCode); + (userId == null ? 0 : userId!.hashCode) + + (withoutThumbs == null ? 0 : withoutThumbs!.hashCode); @override - String toString() => 'GetAssetCountByTimeBucketDto[timeGroup=$timeGroup, userId=$userId]'; + String toString() => 'GetAssetCountByTimeBucketDto[timeGroup=$timeGroup, userId=$userId, withoutThumbs=$withoutThumbs]'; Map toJson() { final json = {}; @@ -49,6 +61,11 @@ class GetAssetCountByTimeBucketDto { } else { // json[r'userId'] = null; } + if (this.withoutThumbs != null) { + json[r'withoutThumbs'] = this.withoutThumbs; + } else { + // json[r'withoutThumbs'] = null; + } return json; } @@ -73,6 +90,7 @@ class GetAssetCountByTimeBucketDto { return GetAssetCountByTimeBucketDto( timeGroup: TimeGroupEnum.fromJson(json[r'timeGroup'])!, userId: mapValueOfType(json, r'userId'), + withoutThumbs: mapValueOfType(json, r'withoutThumbs'), ); } return null; diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 79c74cc8e01ff..75d6e953b8ffd 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -72,7 +72,7 @@ void main() { // Get all AssetEntity belong to the user // - //Future> getAllAssets({ String userId, bool isFavorite, bool isArchived, num skip, String ifNoneMatch }) async + //Future> getAllAssets({ String userId, bool isFavorite, bool isArchived, bool withoutThumbs, num skip, String ifNoneMatch }) async test('test getAllAssets', () async { // TODO }); diff --git a/mobile/openapi/test/get_asset_count_by_time_bucket_dto_test.dart b/mobile/openapi/test/get_asset_count_by_time_bucket_dto_test.dart index 5fa7c11bec167..ca0f586a6b0df 100644 --- a/mobile/openapi/test/get_asset_count_by_time_bucket_dto_test.dart +++ b/mobile/openapi/test/get_asset_count_by_time_bucket_dto_test.dart @@ -26,6 +26,12 @@ void main() { // TODO }); + // Include assets without thumbnails + // bool withoutThumbs + test('to test the property `withoutThumbs`', () async { + // TODO + }); + }); diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index de7a9df9e3ee3..6f2bf49527ff4 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -6,7 +6,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm/repository/Repository'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto'; -import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; +import { GetAssetCountByTimeBucketDto, TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; @@ -37,7 +37,7 @@ export interface IAssetRepository { getLocationsByUserId(userId: string): Promise; 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 dfc62f0c98e16..f0fe84d717dce 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 84bededd7b36b..8a908e746d72f 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 58104d5d68393..d4e6c3c45a7ed 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 36a4dad1d8ff9..41c4a4669b08d 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 b777df42c108a..6ec6ffcb3731f 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 0adbe217aa722..65b0d09d56754 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 92e8e47191136..92db34f738b83 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 bcbb649297ff7..2617d77bf67e4 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 7b2ad8054beb5..8efeb1b866f93 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');