From f952bc0b64c9e6f7f2a8320bc12a31576040be4e Mon Sep 17 00:00:00 2001 From: Jason Rasmussen <jrasm91@gmail.com> Date: Fri, 14 Jul 2023 09:30:17 -0400 Subject: [PATCH] refactor(server): asset stats (#3253) * refactor(server): asset stats * chore: open api --- cli/src/api/open-api/api.ts | 194 +++++++---------- mobile/openapi/.openapi-generator/FILES | 6 +- mobile/openapi/README.md | 5 +- mobile/openapi/doc/AssetApi.md | 162 ++++++--------- ...esponseDto.md => AssetStatsResponseDto.md} | 10 +- mobile/openapi/lib/api.dart | 2 +- mobile/openapi/lib/api/asset_api.dart | 140 ++++++------- mobile/openapi/lib/api_client.dart | 4 +- .../asset_count_by_user_id_response_dto.dart | 130 ------------ .../lib/model/asset_stats_response_dto.dart | 114 ++++++++++ mobile/openapi/test/asset_api_test.dart | 13 +- ...art => asset_stats_response_dto_test.dart} | 24 +-- server/immich-openapi-specs.json | 108 ++++------ server/src/domain/asset/asset.repository.ts | 8 + server/src/domain/asset/asset.service.spec.ts | 44 +++- server/src/domain/asset/asset.service.ts | 6 + .../domain/asset/dto/asset-statistics.dto.ts | 37 ++++ server/src/domain/asset/dto/index.ts | 1 + .../immich/api-v1/asset/asset-repository.ts | 57 +---- .../immich/api-v1/asset/asset.controller.ts | 10 - .../immich/api-v1/asset/asset.service.spec.ts | 35 ---- .../src/immich/api-v1/asset/asset.service.ts | 9 - .../asset-count-by-user-id-response.dto.ts | 18 -- .../immich/controllers/asset.controller.ts | 7 + .../infra/repositories/asset.repository.ts | 36 ++++ .../repositories/asset.repository.mock.ts | 1 + web/src/api/open-api/api.ts | 195 +++++++----------- .../side-bar/side-bar.svelte | 59 +----- web/src/routes/(user)/photos/+page.svelte | 10 +- 29 files changed, 601 insertions(+), 844 deletions(-) rename mobile/openapi/doc/{AssetCountByUserIdResponseDto.md => AssetStatsResponseDto.md} (58%) delete mode 100644 mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart create mode 100644 mobile/openapi/lib/model/asset_stats_response_dto.dart rename mobile/openapi/test/{asset_count_by_user_id_response_dto_test.dart => asset_stats_response_dto_test.dart} (50%) create mode 100644 server/src/domain/asset/dto/asset-statistics.dto.ts delete mode 100644 server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 4f36e2a941..68552add30 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -486,43 +486,6 @@ export interface AssetCountByTimeBucketResponseDto { */ 'buckets': Array<AssetCountByTimeBucket>; } -/** - * - * @export - * @interface AssetCountByUserIdResponseDto - */ -export interface AssetCountByUserIdResponseDto { - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'audio': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'photos': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'videos': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'other': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'total': number; -} /** * * @export @@ -724,6 +687,31 @@ export interface AssetResponseDto { } +/** + * + * @export + * @interface AssetStatsResponseDto + */ +export interface AssetStatsResponseDto { + /** + * + * @type {number} + * @memberof AssetStatsResponseDto + */ + 'images': number; + /** + * + * @type {number} + * @memberof AssetStatsResponseDto + */ + 'videos': number; + /** + * + * @type {number} + * @memberof AssetStatsResponseDto + */ + 'total': number; +} /** * * @export @@ -4892,44 +4880,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getArchivedAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { - const localVarPath = `/asset/stat/archive`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -5079,8 +5029,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { - const localVarPath = `/asset/count-by-user-id`; + getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { + const localVarPath = `/asset/search-terms`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5114,11 +5064,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * + * @param {boolean} [isArchived] + * @param {boolean} [isFavorite] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { - const localVarPath = `/asset/search-terms`; + getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { + const localVarPath = `/asset/statistics`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5139,6 +5091,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -5887,15 +5847,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getArchivedAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getArchivedAssetCountByUserId(options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * Get a single asset\'s information * @param {string} id @@ -5932,17 +5883,19 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByUserId(options); + async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * + * @param {boolean} [isArchived] + * @param {boolean} [isFavorite] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options); + async getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetStatsResponseDto>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetStats(isArchived, isFavorite, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6160,14 +6113,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getArchivedAssetCountByUserId(options?: AxiosRequestConfig): AxiosPromise<AssetCountByUserIdResponseDto> { - return localVarFp.getArchivedAssetCountByUserId(options).then((request) => request(axios, basePath)); - }, /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -6200,16 +6145,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetCountByUserId(options?: AxiosRequestConfig): AxiosPromise<AssetCountByUserIdResponseDto> { - return localVarFp.getAssetCountByUserId(options).then((request) => request(axios, basePath)); + getAssetSearchTerms(options?: AxiosRequestConfig): AxiosPromise<Array<string>> { + return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath)); }, /** * + * @param {AssetApiGetAssetStatsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetSearchTerms(options?: AxiosRequestConfig): AxiosPromise<Array<string>> { - return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath)); + getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<AssetStatsResponseDto> { + return localVarFp.getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(axios, basePath)); }, /** * @@ -6523,6 +6469,27 @@ export interface AssetApiGetAssetCountByTimeBucketRequest { readonly getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto } +/** + * Request parameters for getAssetStats operation in AssetApi. + * @export + * @interface AssetApiGetAssetStatsRequest + */ +export interface AssetApiGetAssetStatsRequest { + /** + * + * @type {boolean} + * @memberof AssetApiGetAssetStats + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiGetAssetStats + */ + readonly isFavorite?: boolean +} + /** * Request parameters for getAssetThumbnail operation in AssetApi. * @export @@ -6915,16 +6882,6 @@ export class AssetApi extends BaseAPI { 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)); } - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public getArchivedAssetCountByUserId(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getArchivedAssetCountByUserId(options).then((request) => request(this.axios, this.basePath)); - } - /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -6964,18 +6921,19 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public getAssetCountByUserId(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAssetCountByUserId(options).then((request) => request(this.axios, this.basePath)); + public getAssetSearchTerms(options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath)); } /** * + * @param {AssetApiGetAssetStatsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public getAssetSearchTerms(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath)); + public getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 86742e468c..f098bf4ffe 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -23,11 +23,11 @@ doc/AssetBulkUploadCheckResponseDto.md doc/AssetBulkUploadCheckResult.md doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucketResponseDto.md -doc/AssetCountByUserIdResponseDto.md doc/AssetFileUploadResponseDto.md doc/AssetIdsDto.md doc/AssetIdsResponseDto.md doc/AssetResponseDto.md +doc/AssetStatsResponseDto.md doc/AssetTypeEnum.md doc/AudioCodec.md doc/AuthDeviceResponseDto.md @@ -163,11 +163,11 @@ lib/model/asset_bulk_upload_check_response_dto.dart lib/model/asset_bulk_upload_check_result.dart lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart -lib/model/asset_count_by_user_id_response_dto.dart lib/model/asset_file_upload_response_dto.dart lib/model/asset_ids_dto.dart lib/model/asset_ids_response_dto.dart lib/model/asset_response_dto.dart +lib/model/asset_stats_response_dto.dart lib/model/asset_type_enum.dart lib/model/audio_codec.dart lib/model/auth_device_response_dto.dart @@ -272,11 +272,11 @@ test/asset_bulk_upload_check_response_dto_test.dart test/asset_bulk_upload_check_result_test.dart test/asset_count_by_time_bucket_response_dto_test.dart test/asset_count_by_time_bucket_test.dart -test/asset_count_by_user_id_response_dto_test.dart test/asset_file_upload_response_dto_test.dart test/asset_ids_dto_test.dart test/asset_ids_response_dto_test.dart test/asset_response_dto_test.dart +test/asset_stats_response_dto_test.dart test/asset_type_enum_test.dart test/audio_codec_test.dart test/auth_device_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index ef92b6a0f3..7f3e9db5e6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -94,12 +94,11 @@ Class | Method | HTTP request | Description *AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download | *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | -*AssetApi* | [**getArchivedAssetCountByUserId**](doc//AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | *AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | *AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket | -*AssetApi* | [**getAssetCountByUserId**](doc//AssetApi.md#getassetcountbyuserid) | **GET** /asset/count-by-user-id | *AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | +*AssetApi* | [**getAssetStats**](doc//AssetApi.md#getassetstats) | **GET** /asset/statistics | *AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | @@ -194,11 +193,11 @@ Class | Method | HTTP request | Description - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md) - [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md) - - [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md) - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetResponseDto](doc//AssetResponseDto.md) + - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) - [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 998836d09f..644907d1e5 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -16,12 +16,11 @@ Method | HTTP request | Description [**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download | [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | -[**getArchivedAssetCountByUserId**](AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | [**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | [**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket | -[**getAssetCountByUserId**](AssetApi.md#getassetcountbyuserid) | **GET** /asset/count-by-user-id | [**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | +[**getAssetStats**](AssetApi.md#getassetstats) | **GET** /asset/statistics | [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | @@ -445,57 +444,6 @@ 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) -# **getArchivedAssetCountByUserId** -> AssetCountByUserIdResponseDto getArchivedAssetCountByUserId() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); - -try { - final result = api_instance.getArchivedAssetCountByUserId(); - print(result); -} catch (e) { - print('Exception when calling AssetApi->getArchivedAssetCountByUserId: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -[**AssetCountByUserIdResponseDto**](AssetCountByUserIdResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[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) - # **getAssetById** > AssetResponseDto getAssetById(id, key) @@ -665,57 +613,6 @@ 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) -# **getAssetCountByUserId** -> AssetCountByUserIdResponseDto getAssetCountByUserId() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); - -try { - final result = api_instance.getAssetCountByUserId(); - print(result); -} catch (e) { - print('Exception when calling AssetApi->getAssetCountByUserId: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -[**AssetCountByUserIdResponseDto**](AssetCountByUserIdResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[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) - # **getAssetSearchTerms** > List<String> getAssetSearchTerms() @@ -767,6 +664,63 @@ This endpoint does not need any parameter. [[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) +# **getAssetStats** +> AssetStatsResponseDto getAssetStats(isArchived, isFavorite) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = AssetApi(); +final isArchived = true; // bool | +final isFavorite = true; // bool | + +try { + final result = api_instance.getAssetStats(isArchived, isFavorite); + print(result); +} catch (e) { + print('Exception when calling AssetApi->getAssetStats: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **isArchived** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + +### Return type + +[**AssetStatsResponseDto**](AssetStatsResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[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) + # **getAssetThumbnail** > MultipartFile getAssetThumbnail(id, format, key) diff --git a/mobile/openapi/doc/AssetCountByUserIdResponseDto.md b/mobile/openapi/doc/AssetStatsResponseDto.md similarity index 58% rename from mobile/openapi/doc/AssetCountByUserIdResponseDto.md rename to mobile/openapi/doc/AssetStatsResponseDto.md index b6271c3f78..d7937a7eda 100644 --- a/mobile/openapi/doc/AssetCountByUserIdResponseDto.md +++ b/mobile/openapi/doc/AssetStatsResponseDto.md @@ -1,4 +1,4 @@ -# openapi.model.AssetCountByUserIdResponseDto +# openapi.model.AssetStatsResponseDto ## Load the model package ```dart @@ -8,11 +8,9 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**audio** | **int** | | [default to 0] -**photos** | **int** | | [default to 0] -**videos** | **int** | | [default to 0] -**other** | **int** | | [default to 0] -**total** | **int** | | [default to 0] +**images** | **int** | | +**videos** | **int** | | +**total** | **int** | | [[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.dart b/mobile/openapi/lib/api.dart index 099f5615c5..ef9544c856 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -60,11 +60,11 @@ part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_count_by_time_bucket.dart'; part 'model/asset_count_by_time_bucket_response_dto.dart'; -part 'model/asset_count_by_user_id_response_dto.dart'; part 'model/asset_file_upload_response_dto.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; part 'model/asset_response_dto.dart'; +part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; part 'model/auth_device_response_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 1c609a7277..c570229aaf 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -440,47 +440,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'GET /asset/stat/archive' operation and returns the [Response]. - Future<Response> getArchivedAssetCountByUserIdWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/asset/stat/archive'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = <QueryParam>[]; - final headerParams = <String, String>{}; - final formParams = <String, String>{}; - - const contentTypes = <String>[]; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future<AssetCountByUserIdResponseDto?> getArchivedAssetCountByUserId() async { - final response = await getArchivedAssetCountByUserIdWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetCountByUserIdResponseDto',) as AssetCountByUserIdResponseDto; - - } - return null; - } - /// Get a single asset's information /// /// Note: This method returns the HTTP [Response]. @@ -639,47 +598,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'GET /asset/count-by-user-id' operation and returns the [Response]. - Future<Response> getAssetCountByUserIdWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/asset/count-by-user-id'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = <QueryParam>[]; - final headerParams = <String, String>{}; - final formParams = <String, String>{}; - - const contentTypes = <String>[]; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future<AssetCountByUserIdResponseDto?> getAssetCountByUserId() async { - final response = await getAssetCountByUserIdWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetCountByUserIdResponseDto',) as AssetCountByUserIdResponseDto; - - } - return null; - } - /// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response]. Future<Response> getAssetSearchTermsWithHttpInfo() async { // ignore: prefer_const_declarations @@ -724,6 +642,64 @@ class AssetApi { return null; } + /// Performs an HTTP 'GET /asset/statistics' operation and returns the [Response]. + /// Parameters: + /// + /// * [bool] isArchived: + /// + /// * [bool] isFavorite: + Future<Response> getAssetStatsWithHttpInfo({ bool? isArchived, bool? isFavorite, }) async { + // ignore: prefer_const_declarations + final path = r'/asset/statistics'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = <QueryParam>[]; + final headerParams = <String, String>{}; + final formParams = <String, String>{}; + + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + + const contentTypes = <String>[]; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [bool] isArchived: + /// + /// * [bool] isFavorite: + Future<AssetStatsResponseDto?> getAssetStats({ bool? isArchived, bool? isFavorite, }) async { + final response = await getAssetStatsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetStatsResponseDto',) as AssetStatsResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /asset/thumbnail/{id}' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 5855da8e82..824d4c9eb4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -215,8 +215,6 @@ class ApiClient { return AssetCountByTimeBucket.fromJson(value); case 'AssetCountByTimeBucketResponseDto': return AssetCountByTimeBucketResponseDto.fromJson(value); - case 'AssetCountByUserIdResponseDto': - return AssetCountByUserIdResponseDto.fromJson(value); case 'AssetFileUploadResponseDto': return AssetFileUploadResponseDto.fromJson(value); case 'AssetIdsDto': @@ -225,6 +223,8 @@ class ApiClient { return AssetIdsResponseDto.fromJson(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); + case 'AssetStatsResponseDto': + return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': return AssetTypeEnumTypeTransformer().decode(value); case 'AudioCodec': diff --git a/mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart b/mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart deleted file mode 100644 index 0e2b1cefc4..0000000000 --- a/mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart +++ /dev/null @@ -1,130 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetCountByUserIdResponseDto { - /// Returns a new [AssetCountByUserIdResponseDto] instance. - AssetCountByUserIdResponseDto({ - this.audio = 0, - this.photos = 0, - this.videos = 0, - this.other = 0, - this.total = 0, - }); - - int audio; - - int photos; - - int videos; - - int other; - - int total; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetCountByUserIdResponseDto && - other.audio == audio && - other.photos == photos && - other.videos == videos && - other.other == other && - other.total == total; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (audio.hashCode) + - (photos.hashCode) + - (videos.hashCode) + - (other.hashCode) + - (total.hashCode); - - @override - String toString() => 'AssetCountByUserIdResponseDto[audio=$audio, photos=$photos, videos=$videos, other=$other, total=$total]'; - - Map<String, dynamic> toJson() { - final json = <String, dynamic>{}; - json[r'audio'] = this.audio; - json[r'photos'] = this.photos; - json[r'videos'] = this.videos; - json[r'other'] = this.other; - json[r'total'] = this.total; - return json; - } - - /// Returns a new [AssetCountByUserIdResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetCountByUserIdResponseDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast<String, dynamic>(); - - return AssetCountByUserIdResponseDto( - audio: mapValueOfType<int>(json, r'audio')!, - photos: mapValueOfType<int>(json, r'photos')!, - videos: mapValueOfType<int>(json, r'videos')!, - other: mapValueOfType<int>(json, r'other')!, - total: mapValueOfType<int>(json, r'total')!, - ); - } - return null; - } - - static List<AssetCountByUserIdResponseDto> listFromJson(dynamic json, {bool growable = false,}) { - final result = <AssetCountByUserIdResponseDto>[]; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetCountByUserIdResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map<String, AssetCountByUserIdResponseDto> mapFromJson(dynamic json) { - final map = <String, AssetCountByUserIdResponseDto>{}; - if (json is Map && json.isNotEmpty) { - json = json.cast<String, dynamic>(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetCountByUserIdResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetCountByUserIdResponseDto-objects as value to a dart map - static Map<String, List<AssetCountByUserIdResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = <String, List<AssetCountByUserIdResponseDto>>{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast<String, dynamic>(); - for (final entry in json.entries) { - map[entry.key] = AssetCountByUserIdResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = <String>{ - 'audio', - 'photos', - 'videos', - 'other', - 'total', - }; -} - diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart new file mode 100644 index 0000000000..1221712d89 --- /dev/null +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetStatsResponseDto { + /// Returns a new [AssetStatsResponseDto] instance. + AssetStatsResponseDto({ + required this.images, + required this.videos, + required this.total, + }); + + int images; + + int videos; + + int total; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetStatsResponseDto && + other.images == images && + other.videos == videos && + other.total == total; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (images.hashCode) + + (videos.hashCode) + + (total.hashCode); + + @override + String toString() => 'AssetStatsResponseDto[images=$images, videos=$videos, total=$total]'; + + Map<String, dynamic> toJson() { + final json = <String, dynamic>{}; + json[r'images'] = this.images; + json[r'videos'] = this.videos; + json[r'total'] = this.total; + return json; + } + + /// Returns a new [AssetStatsResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetStatsResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast<String, dynamic>(); + + return AssetStatsResponseDto( + images: mapValueOfType<int>(json, r'images')!, + videos: mapValueOfType<int>(json, r'videos')!, + total: mapValueOfType<int>(json, r'total')!, + ); + } + return null; + } + + static List<AssetStatsResponseDto> listFromJson(dynamic json, {bool growable = false,}) { + final result = <AssetStatsResponseDto>[]; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetStatsResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map<String, AssetStatsResponseDto> mapFromJson(dynamic json) { + final map = <String, AssetStatsResponseDto>{}; + if (json is Map && json.isNotEmpty) { + json = json.cast<String, dynamic>(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetStatsResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetStatsResponseDto-objects as value to a dart map + static Map<String, List<AssetStatsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = <String, List<AssetStatsResponseDto>>{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast<String, dynamic>(); + for (final entry in json.entries) { + map[entry.key] = AssetStatsResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = <String>{ + 'images', + 'videos', + 'total', + }; +} + diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 97fa9c3f05..426e5e79a2 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -60,11 +60,6 @@ void main() { // TODO }); - //Future<AssetCountByUserIdResponseDto> getArchivedAssetCountByUserId() async - test('test getArchivedAssetCountByUserId', () async { - // TODO - }); - // Get a single asset's information // //Future<AssetResponseDto> getAssetById(String id, { String key }) async @@ -82,13 +77,13 @@ void main() { // TODO }); - //Future<AssetCountByUserIdResponseDto> getAssetCountByUserId() async - test('test getAssetCountByUserId', () async { + //Future<List<String>> getAssetSearchTerms() async + test('test getAssetSearchTerms', () async { // TODO }); - //Future<List<String>> getAssetSearchTerms() async - test('test getAssetSearchTerms', () async { + //Future<AssetStatsResponseDto> getAssetStats({ bool isArchived, bool isFavorite }) async + test('test getAssetStats', () async { // TODO }); diff --git a/mobile/openapi/test/asset_count_by_user_id_response_dto_test.dart b/mobile/openapi/test/asset_stats_response_dto_test.dart similarity index 50% rename from mobile/openapi/test/asset_count_by_user_id_response_dto_test.dart rename to mobile/openapi/test/asset_stats_response_dto_test.dart index 6d0b97b6ed..3e5d8b548f 100644 --- a/mobile/openapi/test/asset_count_by_user_id_response_dto_test.dart +++ b/mobile/openapi/test/asset_stats_response_dto_test.dart @@ -11,32 +11,22 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for AssetCountByUserIdResponseDto +// tests for AssetStatsResponseDto void main() { - // final instance = AssetCountByUserIdResponseDto(); + // final instance = AssetStatsResponseDto(); - group('test AssetCountByUserIdResponseDto', () { - // int audio (default value: 0) - test('to test the property `audio`', () async { + group('test AssetStatsResponseDto', () { + // int images + test('to test the property `images`', () async { // TODO }); - // int photos (default value: 0) - test('to test the property `photos`', () async { - // TODO - }); - - // int videos (default value: 0) + // int videos test('to test the property `videos`', () async { // TODO }); - // int other (default value: 0) - test('to test the property `other`', () async { - // TODO - }); - - // int total (default value: 0) + // int total test('to test the property `total`', () async { // TODO }); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index a3ce03df09..739d001861 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -984,38 +984,6 @@ ] } }, - "/asset/count-by-user-id": { - "get": { - "operationId": "getAssetCountByUserId", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetCountByUserIdResponseDto" - } - } - } - } - }, - "tags": [ - "Asset" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, "/asset/curated-locations": { "get": { "operationId": "getCuratedLocations", @@ -1608,17 +1576,34 @@ ] } }, - "/asset/stat/archive": { + "/asset/statistics": { "get": { - "operationId": "getArchivedAssetCountByUserId", - "parameters": [], + "operationId": "getAssetStats", + "parameters": [ + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetCountByUserIdResponseDto" + "$ref": "#/components/schemas/AssetStatsResponseDto" } } } @@ -4786,38 +4771,6 @@ "buckets" ] }, - "AssetCountByUserIdResponseDto": { - "type": "object", - "properties": { - "audio": { - "type": "integer", - "default": 0 - }, - "photos": { - "type": "integer", - "default": 0 - }, - "videos": { - "type": "integer", - "default": 0 - }, - "other": { - "type": "integer", - "default": 0 - }, - "total": { - "type": "integer", - "default": 0 - } - }, - "required": [ - "audio", - "photos", - "videos", - "other", - "total" - ] - }, "AssetFileUploadResponseDto": { "type": "object", "properties": { @@ -4970,6 +4923,25 @@ "checksum" ] }, + "AssetStatsResponseDto": { + "type": "object", + "properties": { + "images": { + "type": "integer" + }, + "videos": { + "type": "integer" + }, + "total": { + "type": "integer" + } + }, + "required": [ + "images", + "videos", + "total" + ] + }, "AssetTypeEnum": { "type": "string", "enum": [ diff --git a/server/src/domain/asset/asset.repository.ts b/server/src/domain/asset/asset.repository.ts index 9bd9c687aa..ae8f64e64e 100644 --- a/server/src/domain/asset/asset.repository.ts +++ b/server/src/domain/asset/asset.repository.ts @@ -1,6 +1,13 @@ import { AssetEntity, AssetType } from '@app/infra/entities'; import { Paginated, PaginationOptions } from '../domain.util'; +export type AssetStats = Record<AssetType, number>; + +export interface AssetStatsOptions { + isFavorite?: boolean; + isArchived?: boolean; +} + export interface AssetSearchOptions { isVisible?: boolean; type?: AssetType; @@ -55,4 +62,5 @@ export interface IAssetRepository { save(asset: Partial<AssetEntity>): Promise<AssetEntity>; findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>; getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>; + getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>; } diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index ef51c8831c..986e7d0b38 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -1,3 +1,4 @@ +import { AssetType } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; import { assetEntityStub, @@ -10,9 +11,9 @@ import { import { when } from 'jest-when'; import { Readable } from 'stream'; import { IStorageRepository } from '../storage'; -import { IAssetRepository } from './asset.repository'; +import { AssetStats, IAssetRepository } from './asset.repository'; import { AssetService } from './asset.service'; -import { DownloadResponseDto } from './index'; +import { AssetStatsResponseDto, DownloadResponseDto } from './dto'; import { mapAsset } from './response-dto'; const downloadResponse: DownloadResponseDto = { @@ -25,6 +26,19 @@ const downloadResponse: DownloadResponseDto = { ], }; +const stats: AssetStats = { + [AssetType.IMAGE]: 10, + [AssetType.VIDEO]: 23, + [AssetType.AUDIO]: 0, + [AssetType.OTHER]: 0, +}; + +const statResponse: AssetStatsResponseDto = { + images: 10, + videos: 23, + total: 33, +}; + describe(AssetService.name, () => { let sut: AssetService; let accessMock: IAccessRepositoryMock; @@ -287,4 +301,30 @@ describe(AssetService.name, () => { }); }); }); + + describe('getStatistics', () => { + it('should get the statistics for a user, excluding archived assets', async () => { + assetMock.getStatistics.mockResolvedValue(stats); + await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: false }); + }); + + it('should get the statistics for a user for archived assets', async () => { + assetMock.getStatistics.mockResolvedValue(stats); + await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: true }); + }); + + it('should get the statistics for a user for favorite assets', async () => { + assetMock.getStatistics.mockResolvedValue(stats); + await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isFavorite: true }); + }); + + it('should get the statistics for a user for all assets', async () => { + assetMock.getStatistics.mockResolvedValue(stats); + await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {}); + }); + }); }); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 5a84a4a351..90595de69b 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -9,6 +9,7 @@ import { HumanReadableSize, usePagination } from '../domain.util'; import { ImmichReadStream, IStorageRepository } from '../storage'; import { IAssetRepository } from './asset.repository'; import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto'; +import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto'; import { MapMarkerDto } from './dto/map-marker.dto'; import { mapAsset, MapMarkerResponseDto } from './response-dto'; import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto'; @@ -155,4 +156,9 @@ export class AssetService { throw new BadRequestException('assetIds, albumId, or userId is required'); } + + async getStatistics(authUser: AuthUserDto, dto: AssetStatsDto) { + const stats = await this.assetRepository.getStatistics(authUser.id, dto); + return mapStats(stats); + } } diff --git a/server/src/domain/asset/dto/asset-statistics.dto.ts b/server/src/domain/asset/dto/asset-statistics.dto.ts new file mode 100644 index 0000000000..ef9c0606fd --- /dev/null +++ b/server/src/domain/asset/dto/asset-statistics.dto.ts @@ -0,0 +1,37 @@ +import { AssetType } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional } from 'class-validator'; +import { toBoolean } from '../../domain.util'; +import { AssetStats } from '../asset.repository'; + +export class AssetStatsDto { + @IsBoolean() + @Transform(toBoolean) + @IsOptional() + isArchived?: boolean; + + @IsBoolean() + @Transform(toBoolean) + @IsOptional() + isFavorite?: boolean; +} + +export class AssetStatsResponseDto { + @ApiProperty({ type: 'integer' }) + images!: number; + + @ApiProperty({ type: 'integer' }) + videos!: number; + + @ApiProperty({ type: 'integer' }) + total!: number; +} + +export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { + return { + images: stats[AssetType.IMAGE], + videos: stats[AssetType.VIDEO], + total: Object.values(stats).reduce((total, value) => total + value, 0), + }; +}; diff --git a/server/src/domain/asset/dto/index.ts b/server/src/domain/asset/dto/index.ts index 9778a91221..f22534d35e 100644 --- a/server/src/domain/asset/dto/index.ts +++ b/server/src/domain/asset/dto/index.ts @@ -1,4 +1,5 @@ export * from './asset-ids.dto'; +export * from './asset-statistics.dto'; export * from './download.dto'; export * from './map-marker.dto'; export * from './memory-lane.dto'; diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index 7b3dfea4df..22c25d6ef4 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -1,4 +1,4 @@ -import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; +import { AssetEntity, ExifEntity } from '@app/infra/entities'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not } from 'typeorm'; @@ -11,7 +11,6 @@ import { GetAssetCountByTimeBucketDto, TimeGroupEnum } from './dto/get-asset-cou import { SearchPropertiesDto } from './dto/search-properties.dto'; import { UpdateAssetDto } from './dto/update-asset.dto'; import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto'; -import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; @@ -38,8 +37,6 @@ export interface IAssetRepository { getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>; getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>; getAssetCountByTimeBucket(userId: string, dto: GetAssetCountByTimeBucketDto): Promise<AssetCountByTimeBucket[]>; - getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>; - getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>; getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>; getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>; @@ -55,35 +52,6 @@ export class AssetRepository implements IAssetRepository { @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>, ) {} - async getAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> { - // Get asset count by AssetType - const items = await this.assetRepository - .createQueryBuilder('asset') - .select(`COUNT(asset.id)`, 'count') - .addSelect(`asset.type`, 'type') - .where('"ownerId" = :ownerId', { ownerId: ownerId }) - .andWhere('asset.isVisible = true') - .groupBy('asset.type') - .getRawMany(); - - return this.getAssetCount(items); - } - - async getArchivedAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> { - // Get archived asset count by AssetType - const items = await this.assetRepository - .createQueryBuilder('asset') - .select(`COUNT(asset.id)`, 'count') - .addSelect(`asset.type`, 'type') - .where('"ownerId" = :ownerId', { ownerId: ownerId }) - .andWhere('asset.isVisible = true') - .andWhere('asset.isArchived = true') - .groupBy('asset.type') - .getRawMany(); - - return this.getAssetCount(items); - } - async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> { // Get asset entity from a list of time buckets let builder = this.assetRepository @@ -337,29 +305,6 @@ export class AssetRepository implements IAssetRepository { return assets.map((asset) => asset.deviceAssetId); } - private getAssetCount(items: any): AssetCountByUserIdResponseDto { - const assetCountByUserId = new AssetCountByUserIdResponseDto(); - - // asset type to dto property mapping - const map: Record<AssetType, keyof AssetCountByUserIdResponseDto> = { - [AssetType.AUDIO]: 'audio', - [AssetType.IMAGE]: 'photos', - [AssetType.VIDEO]: 'videos', - [AssetType.OTHER]: 'other', - }; - - for (const item of items) { - const count = Number(item.count) || 0; - const assetType = item.type as AssetType; - const type = map[assetType]; - - assetCountByUserId[type] = count; - assetCountByUserId.total += count; - } - - return assetCountByUserId; - } - getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> { return this.assetRepository.findOne({ select: { diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index b59c971999..1e22bf3baa 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -38,7 +38,6 @@ import { ServeFileDto } from './dto/serve-file.dto'; import { UpdateAssetDto } from './dto/update-asset.dto'; import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto'; import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-response.dto'; -import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto'; import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; @@ -173,15 +172,6 @@ export class AssetController { return this.assetService.getAssetCountByTimeBucket(authUser, dto); } - @Get('/count-by-user-id') - getAssetCountByUserId(@AuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> { - return this.assetService.getAssetCountByUserId(authUser); - } - - @Get('/stat/archive') - getArchivedAssetCountByUserId(@AuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> { - return this.assetService.getArchivedAssetCountByUserId(authUser); - } /** * Get all AssetEntity belong to the user */ diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index 97725f149e..110d63f50c 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -26,7 +26,6 @@ import { CreateAssetDto } from './dto/create-asset.dto'; import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto'; import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto'; -import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; const _getCreateAssetDto = (): CreateAssetDto => { const createAssetDto = new CreateAssetDto(); @@ -103,24 +102,6 @@ const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => { return [result1, result2]; }; -const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => { - const result = new AssetCountByUserIdResponseDto(); - - result.videos = 2; - result.photos = 2; - - return result; -}; - -const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => { - const result = new AssetCountByUserIdResponseDto(); - - result.videos = 1; - result.photos = 2; - - return result; -}; - const uploadFile = { nullAuth: { authUser: null, @@ -197,8 +178,6 @@ describe('AssetService', () => { getSearchPropertiesByUserId: jest.fn(), getAssetByTimeBucket: jest.fn(), getAssetsByChecksums: jest.fn(), - getAssetCountByUserId: jest.fn(), - getArchivedAssetCountByUserId: jest.fn(), getExistingAssets: jest.fn(), getByOriginalPath: jest.fn(), }; @@ -467,20 +446,6 @@ describe('AssetService', () => { expect(result.buckets.length).toEqual(2); }); - it('get asset count by user id', async () => { - const assetCount = _getAssetCountByUserId(); - assetRepositoryMock.getAssetCountByUserId.mockResolvedValue(assetCount); - - await expect(sut.getAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount); - }); - - it('get archived asset count by user id', async () => { - const assetCount = _getArchivedAssetsCountByUserId(); - assetRepositoryMock.getArchivedAssetCountByUserId.mockResolvedValue(assetCount); - - await expect(sut.getArchivedAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount); - }); - describe('deleteAll', () => { it('should return failed status when an asset is missing', async () => { assetRepositoryMock.get.mockResolvedValue(null); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index c08a24fe0c..1aeac5ac0d 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -58,7 +58,6 @@ import { AssetCountByTimeBucketResponseDto, mapAssetCountByTimeBucket, } from './response-dto/asset-count-by-time-group-response.dto'; -import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto'; import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; @@ -536,14 +535,6 @@ export class AssetService { return mapAssetCountByTimeBucket(result); } - getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> { - return this._assetRepository.getAssetCountByUserId(authUser.id); - } - - getArchivedAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> { - return this._assetRepository.getArchivedAssetCountByUserId(authUser.id); - } - getExifPermission(authUser: AuthUserDto) { return !authUser.isPublicUser || authUser.isShowExif; } diff --git a/server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts deleted file mode 100644 index cbee0eed5c..0000000000 --- a/server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class AssetCountByUserIdResponseDto { - @ApiProperty({ type: 'integer' }) - audio = 0; - - @ApiProperty({ type: 'integer' }) - photos = 0; - - @ApiProperty({ type: 'integer' }) - videos = 0; - - @ApiProperty({ type: 'integer' }) - other = 0; - - @ApiProperty({ type: 'integer' }) - total = 0; -} diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 28e23c98e2..5c4e19ccf2 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -1,6 +1,8 @@ import { AssetIdsDto, AssetService, + AssetStatsDto, + AssetStatsResponseDto, AuthUserDto, DownloadDto, DownloadResponseDto, @@ -53,4 +55,9 @@ export class AssetController { downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { return this.service.downloadFile(authUser, id).then(asStreamableFile); } + + @Get('statistics') + getAssetStats(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> { + return this.service.getStatistics(authUser, dto); + } } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index a237872521..09fb3a17ec 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -1,5 +1,7 @@ import { AssetSearchOptions, + AssetStats, + AssetStatsOptions, IAssetRepository, LivePhotoSearchOptions, MapMarker, @@ -321,4 +323,38 @@ export class AssetRepository implements IAssetRepository { lon: asset.exifInfo!.longitude!, })); } + + async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> { + let builder = await this.repository + .createQueryBuilder('asset') + .select(`COUNT(asset.id)`, 'count') + .addSelect(`asset.type`, 'type') + .where('"ownerId" = :ownerId', { ownerId }) + .andWhere('asset.isVisible = true') + .groupBy('asset.type'); + + const { isArchived, isFavorite } = options; + if (isArchived !== undefined) { + builder = builder.andWhere(`asset.isArchived = :isArchived`, { isArchived }); + } + + if (isFavorite !== undefined) { + builder = builder.andWhere(`asset.isFavorite = :isFavorite`, { isFavorite }); + } + + const items = await builder.getRawMany(); + + const result: AssetStats = { + [AssetType.AUDIO]: 0, + [AssetType.IMAGE]: 0, + [AssetType.VIDEO]: 0, + [AssetType.OTHER]: 0, + }; + + for (const item of items) { + result[item.type as AssetType] = Number(item.count) || 0; + } + + return result; + } } diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 7e8a522626..5eb69a9e5a 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -18,5 +18,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => { save: jest.fn(), findLivePhotoMatch: jest.fn(), getMapMarkers: jest.fn(), + getStatistics: jest.fn(), }; }; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 484dc4e4d8..67434662de 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -486,43 +486,6 @@ export interface AssetCountByTimeBucketResponseDto { */ 'buckets': Array<AssetCountByTimeBucket>; } -/** - * - * @export - * @interface AssetCountByUserIdResponseDto - */ -export interface AssetCountByUserIdResponseDto { - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'audio': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'photos': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'videos': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'other': number; - /** - * - * @type {number} - * @memberof AssetCountByUserIdResponseDto - */ - 'total': number; -} /** * * @export @@ -724,6 +687,31 @@ export interface AssetResponseDto { } +/** + * + * @export + * @interface AssetStatsResponseDto + */ +export interface AssetStatsResponseDto { + /** + * + * @type {number} + * @memberof AssetStatsResponseDto + */ + 'images': number; + /** + * + * @type {number} + * @memberof AssetStatsResponseDto + */ + 'videos': number; + /** + * + * @type {number} + * @memberof AssetStatsResponseDto + */ + 'total': number; +} /** * * @export @@ -4901,44 +4889,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getArchivedAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { - const localVarPath = `/asset/stat/archive`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -5088,8 +5038,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { - const localVarPath = `/asset/count-by-user-id`; + getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { + const localVarPath = `/asset/search-terms`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5123,11 +5073,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * + * @param {boolean} [isArchived] + * @param {boolean} [isFavorite] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { - const localVarPath = `/asset/search-terms`; + getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { + const localVarPath = `/asset/statistics`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5148,6 +5100,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -5896,15 +5856,6 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getArchivedAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getArchivedAssetCountByUserId(options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * Get a single asset\'s information * @param {string} id @@ -5941,17 +5892,19 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByUserId(options); + async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * + * @param {boolean} [isArchived] + * @param {boolean} [isFavorite] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options); + async getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetStatsResponseDto>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetStats(isArchived, isFavorite, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6177,14 +6130,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { return localVarFp.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getArchivedAssetCountByUserId(options?: any): AxiosPromise<AssetCountByUserIdResponseDto> { - return localVarFp.getArchivedAssetCountByUserId(options).then((request) => request(axios, basePath)); - }, /** * Get a single asset\'s information * @param {string} id @@ -6218,16 +6163,18 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetCountByUserId(options?: any): AxiosPromise<AssetCountByUserIdResponseDto> { - return localVarFp.getAssetCountByUserId(options).then((request) => request(axios, basePath)); + getAssetSearchTerms(options?: any): AxiosPromise<Array<string>> { + return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath)); }, /** * + * @param {boolean} [isArchived] + * @param {boolean} [isFavorite] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAssetSearchTerms(options?: any): AxiosPromise<Array<string>> { - return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath)); + getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: any): AxiosPromise<AssetStatsResponseDto> { + return localVarFp.getAssetStats(isArchived, isFavorite, options).then((request) => request(axios, basePath)); }, /** * @@ -6565,6 +6512,27 @@ export interface AssetApiGetAssetCountByTimeBucketRequest { readonly getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto } +/** + * Request parameters for getAssetStats operation in AssetApi. + * @export + * @interface AssetApiGetAssetStatsRequest + */ +export interface AssetApiGetAssetStatsRequest { + /** + * + * @type {boolean} + * @memberof AssetApiGetAssetStats + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiGetAssetStats + */ + readonly isFavorite?: boolean +} + /** * Request parameters for getAssetThumbnail operation in AssetApi. * @export @@ -6957,16 +6925,6 @@ export class AssetApi extends BaseAPI { 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)); } - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public getArchivedAssetCountByUserId(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getArchivedAssetCountByUserId(options).then((request) => request(this.axios, this.basePath)); - } - /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -7006,18 +6964,19 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public getAssetCountByUserId(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAssetCountByUserId(options).then((request) => request(this.axios, this.basePath)); + public getAssetSearchTerms(options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath)); } /** * + * @param {AssetApiGetAssetStatsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public getAssetSearchTerms(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath)); + public getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(this.axios, this.basePath)); } /** 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 3d985661ad..9cc966b46a 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 @@ -1,6 +1,6 @@ <script lang="ts"> import { page } from '$app/stores'; - import { api } from '@api'; + import { AssetApiGetAssetStatsRequest, api } from '@api'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; import AccountMultiple from 'svelte-material-icons/AccountMultiple.svelte'; import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte'; @@ -18,31 +18,9 @@ import { locale } from '$lib/stores/preferences.store'; import SideBarSection from './side-bar-section.svelte'; - const getAssetCount = async () => { - const { data: allAssetCount } = await api.assetApi.getAssetCountByUserId(); - const { data: archivedCount } = await api.assetApi.getArchivedAssetCountByUserId(); - - return { - videos: allAssetCount.videos - archivedCount.videos, - photos: allAssetCount.photos - archivedCount.photos, - }; - }; - - const getFavoriteCount = async () => { - try { - const { data: assets } = await api.assetApi.getAllAssets({ - isFavorite: true, - withoutThumbs: true, - }); - - return { - favorites: assets.length, - }; - } catch { - return { - favorites: 0, - }; - } + const getStats = async (dto: AssetApiGetAssetStatsRequest) => { + const { data: stats } = await api.assetApi.getAssetStats(dto); + return stats; }; const getAlbumCount = async () => { @@ -54,22 +32,6 @@ } }; - const getArchivedAssetsCount = async () => { - try { - const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId(); - - return { - videos: assetCount.videos, - photos: assetCount.photos, - }; - } catch { - return { - videos: 0, - photos: 0, - }; - } - }; - const isFavoritesSelected = $page.route.id === '/(user)/favorites'; const isPhotosSelected = $page.route.id === '/(user)/photos'; const isSharingSelected = $page.route.id === '/(user)/sharing'; @@ -83,12 +45,12 @@ isSelected={isPhotosSelected} > <svelte:fragment slot="moreInformation"> - {#await getAssetCount()} + {#await getStats({ isArchived: false })} <LoadingSpinner /> {:then data} <div> <p>{data.videos.toLocaleString($locale)} Videos</p> - <p>{data.photos.toLocaleString($locale)} Photos</p> + <p>{data.images.toLocaleString($locale)} Photos</p> </div> {/await} </svelte:fragment> @@ -129,11 +91,12 @@ isSelected={isFavoritesSelected} > <svelte:fragment slot="moreInformation"> - {#await getFavoriteCount()} + {#await getStats({ isFavorite: true })} <LoadingSpinner /> {:then data} <div> - <p>{data.favorites} Favorites</p> + <p>{data.videos.toLocaleString($locale)} Videos</p> + <p>{data.images.toLocaleString($locale)} Photos</p> </div> {/await} </svelte:fragment> @@ -155,12 +118,12 @@ <a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false"> <SideBarButton title="Archive" logo={ArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}> <svelte:fragment slot="moreInformation"> - {#await getArchivedAssetsCount()} + {#await getStats({ isArchived: true })} <LoadingSpinner /> {:then data} <div> <p>{data.videos.toLocaleString($locale)} Videos</p> - <p>{data.photos.toLocaleString($locale)} Photos</p> + <p>{data.images.toLocaleString($locale)} Photos</p> </div> {/await} </svelte:fragment> diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte index 677e171020..cbabc89cf1 100644 --- a/web/src/routes/(user)/photos/+page.svelte +++ b/web/src/routes/(user)/photos/+page.svelte @@ -10,22 +10,22 @@ import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; + import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store'; import { assetStore } from '$lib/stores/assets.store'; + import { openFileUploadDialog } from '$lib/utils/file-uploader'; + import { api } from '@api'; import { onDestroy, onMount } from 'svelte'; import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; import Plus from 'svelte-material-icons/Plus.svelte'; import type { PageData } from './$types'; - import { api } from '@api'; - import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; - import { openFileUploadDialog } from '$lib/utils/file-uploader'; export let data: PageData; let assetCount = 1; onMount(async () => { - const { data: allAssetCount } = await api.assetApi.getAssetCountByUserId(); - assetCount = allAssetCount.total; + const { data: stats } = await api.assetApi.getAssetStats(); + assetCount = stats.total; }); onDestroy(() => {