Bläddra i källkod

refactor(server): asset stats (#3253)

* refactor(server): asset stats

* chore: open api
Jason Rasmussen 2 år sedan
förälder
incheckning
f952bc0b64
29 ändrade filer med 537 tillägg och 780 borttagningar
  1. 76 118
      cli/src/api/open-api/api.ts
  2. 3 3
      mobile/openapi/.openapi-generator/FILES
  3. 2 3
      mobile/openapi/README.md
  4. 18 64
      mobile/openapi/doc/AssetApi.md
  5. 4 6
      mobile/openapi/doc/AssetStatsResponseDto.md
  6. 1 1
      mobile/openapi/lib/api.dart
  7. 34 58
      mobile/openapi/lib/api/asset_api.dart
  8. 2 2
      mobile/openapi/lib/api_client.dart
  9. 0 130
      mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart
  10. 114 0
      mobile/openapi/lib/model/asset_stats_response_dto.dart
  11. 4 9
      mobile/openapi/test/asset_api_test.dart
  12. 7 17
      mobile/openapi/test/asset_stats_response_dto_test.dart
  13. 40 68
      server/immich-openapi-specs.json
  14. 8 0
      server/src/domain/asset/asset.repository.ts
  15. 42 2
      server/src/domain/asset/asset.service.spec.ts
  16. 6 0
      server/src/domain/asset/asset.service.ts
  17. 37 0
      server/src/domain/asset/dto/asset-statistics.dto.ts
  18. 1 0
      server/src/domain/asset/dto/index.ts
  19. 1 56
      server/src/immich/api-v1/asset/asset-repository.ts
  20. 0 10
      server/src/immich/api-v1/asset/asset.controller.ts
  21. 0 35
      server/src/immich/api-v1/asset/asset.service.spec.ts
  22. 0 9
      server/src/immich/api-v1/asset/asset.service.ts
  23. 0 18
      server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts
  24. 7 0
      server/src/immich/controllers/asset.controller.ts
  25. 36 0
      server/src/infra/repositories/asset.repository.ts
  26. 1 0
      server/test/repositories/asset.repository.mock.ts
  27. 77 118
      web/src/api/open-api/api.ts
  28. 11 48
      web/src/lib/components/shared-components/side-bar/side-bar.svelte
  29. 5 5
      web/src/routes/(user)/photos/+page.svelte

+ 76 - 118
cli/src/api/open-api/api.ts

@@ -486,43 +486,6 @@ export interface AssetCountByTimeBucketResponseDto {
      */
      */
     'buckets': Array<AssetCountByTimeBucket>;
     '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
  * @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
  * @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);
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -5079,8 +5029,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
             let baseOptions;
@@ -5114,11 +5064,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         },
         /**
         /**
          * 
          * 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
             let baseOptions;
@@ -5139,6 +5091,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
 
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
+
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
+
 
 
     
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             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);
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
             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
          * Get a single asset\'s information
          * @param {string} id 
          * @param {string} id 
@@ -5932,17 +5883,19 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         },
         /**
         /**
          * 
          * 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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);
             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>> {
         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));
             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
          * Get a single asset\'s information
          * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
          * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@@ -6200,16 +6145,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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
     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.
  * Request parameters for getAssetThumbnail operation in AssetApi.
  * @export
  * @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));
         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
      * Get a single asset\'s information
      * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
      * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@@ -6964,18 +6921,19 @@ export class AssetApi extends BaseAPI {
      * @throws {RequiredError}
      * @throws {RequiredError}
      * @memberof AssetApi
      * @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.
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @throws {RequiredError}
      * @memberof AssetApi
      * @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));
     }
     }
 
 
     /**
     /**

+ 3 - 3
mobile/openapi/.openapi-generator/FILES

@@ -23,11 +23,11 @@ doc/AssetBulkUploadCheckResponseDto.md
 doc/AssetBulkUploadCheckResult.md
 doc/AssetBulkUploadCheckResult.md
 doc/AssetCountByTimeBucket.md
 doc/AssetCountByTimeBucket.md
 doc/AssetCountByTimeBucketResponseDto.md
 doc/AssetCountByTimeBucketResponseDto.md
-doc/AssetCountByUserIdResponseDto.md
 doc/AssetFileUploadResponseDto.md
 doc/AssetFileUploadResponseDto.md
 doc/AssetIdsDto.md
 doc/AssetIdsDto.md
 doc/AssetIdsResponseDto.md
 doc/AssetIdsResponseDto.md
 doc/AssetResponseDto.md
 doc/AssetResponseDto.md
+doc/AssetStatsResponseDto.md
 doc/AssetTypeEnum.md
 doc/AssetTypeEnum.md
 doc/AudioCodec.md
 doc/AudioCodec.md
 doc/AuthDeviceResponseDto.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_bulk_upload_check_result.dart
 lib/model/asset_count_by_time_bucket.dart
 lib/model/asset_count_by_time_bucket.dart
 lib/model/asset_count_by_time_bucket_response_dto.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_file_upload_response_dto.dart
 lib/model/asset_ids_dto.dart
 lib/model/asset_ids_dto.dart
 lib/model/asset_ids_response_dto.dart
 lib/model/asset_ids_response_dto.dart
 lib/model/asset_response_dto.dart
 lib/model/asset_response_dto.dart
+lib/model/asset_stats_response_dto.dart
 lib/model/asset_type_enum.dart
 lib/model/asset_type_enum.dart
 lib/model/audio_codec.dart
 lib/model/audio_codec.dart
 lib/model/auth_device_response_dto.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_bulk_upload_check_result_test.dart
 test/asset_count_by_time_bucket_response_dto_test.dart
 test/asset_count_by_time_bucket_response_dto_test.dart
 test/asset_count_by_time_bucket_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_file_upload_response_dto_test.dart
 test/asset_ids_dto_test.dart
 test/asset_ids_dto_test.dart
 test/asset_ids_response_dto_test.dart
 test/asset_ids_response_dto_test.dart
 test/asset_response_dto_test.dart
 test/asset_response_dto_test.dart
+test/asset_stats_response_dto_test.dart
 test/asset_type_enum_test.dart
 test/asset_type_enum_test.dart
 test/audio_codec_test.dart
 test/audio_codec_test.dart
 test/auth_device_response_dto_test.dart
 test/auth_device_response_dto_test.dart

+ 2 - 3
mobile/openapi/README.md

@@ -94,12 +94,11 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download | 
 *AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download | 
 *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | 
 *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* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 *AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | 
 *AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | 
 *AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-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* | [**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* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
@@ -194,11 +193,11 @@ Class | Method | HTTP request | Description
  - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
  - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
  - [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
  - [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
  - [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
  - [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
- - [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
  - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
  - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
  - [AssetIdsDto](doc//AssetIdsDto.md)
  - [AssetIdsDto](doc//AssetIdsDto.md)
  - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
  - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
  - [AssetResponseDto](doc//AssetResponseDto.md)
  - [AssetResponseDto](doc//AssetResponseDto.md)
+ - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
  - [AssetTypeEnum](doc//AssetTypeEnum.md)
  - [AssetTypeEnum](doc//AssetTypeEnum.md)
  - [AudioCodec](doc//AudioCodec.md)
  - [AudioCodec](doc//AudioCodec.md)
  - [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)
  - [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)

+ 18 - 64
mobile/openapi/doc/AssetApi.md

@@ -16,12 +16,11 @@ Method | HTTP request | Description
 [**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download | 
 [**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download | 
 [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | 
 [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | 
-[**getArchivedAssetCountByUserId**](AssetApi.md#getarchivedassetcountbyuserid) | **GET** /asset/stat/archive | 
 [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
 [**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | 
 [**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | 
 [**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-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 | 
 [**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | 
+[**getAssetStats**](AssetApi.md#getassetstats) | **GET** /asset/statistics | 
 [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
 [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 [**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)
 [[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**
 # **getAssetById**
 > AssetResponseDto getAssetById(id, key)
 > AssetResponseDto getAssetById(id, key)
 
 
@@ -665,8 +613,8 @@ 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)
 [[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()
+# **getAssetSearchTerms**
+> List<String> getAssetSearchTerms()
 
 
 
 
 
 
@@ -691,10 +639,10 @@ import 'package:openapi/api.dart';
 final api_instance = AssetApi();
 final api_instance = AssetApi();
 
 
 try {
 try {
-    final result = api_instance.getAssetCountByUserId();
+    final result = api_instance.getAssetSearchTerms();
     print(result);
     print(result);
 } catch (e) {
 } catch (e) {
-    print('Exception when calling AssetApi->getAssetCountByUserId: $e\n');
+    print('Exception when calling AssetApi->getAssetSearchTerms: $e\n');
 }
 }
 ```
 ```
 
 
@@ -703,7 +651,7 @@ This endpoint does not need any parameter.
 
 
 ### Return type
 ### Return type
 
 
-[**AssetCountByUserIdResponseDto**](AssetCountByUserIdResponseDto.md)
+**List<String>**
 
 
 ### Authorization
 ### Authorization
 
 
@@ -716,8 +664,8 @@ 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)
 [[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()
+# **getAssetStats**
+> AssetStatsResponseDto getAssetStats(isArchived, isFavorite)
 
 
 
 
 
 
@@ -740,21 +688,27 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 
 final api_instance = AssetApi();
 final api_instance = AssetApi();
+final isArchived = true; // bool | 
+final isFavorite = true; // bool | 
 
 
 try {
 try {
-    final result = api_instance.getAssetSearchTerms();
+    final result = api_instance.getAssetStats(isArchived, isFavorite);
     print(result);
     print(result);
 } catch (e) {
 } catch (e) {
-    print('Exception when calling AssetApi->getAssetSearchTerms: $e\n');
+    print('Exception when calling AssetApi->getAssetStats: $e\n');
 }
 }
 ```
 ```
 
 
 ### Parameters
 ### Parameters
-This endpoint does not need any parameter.
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **isArchived** | **bool**|  | [optional] 
+ **isFavorite** | **bool**|  | [optional] 
 
 
 ### Return type
 ### Return type
 
 
-**List<String>**
+[**AssetStatsResponseDto**](AssetStatsResponseDto.md)
 
 
 ### Authorization
 ### Authorization
 
 

+ 4 - 6
mobile/openapi/doc/AssetCountByUserIdResponseDto.md → mobile/openapi/doc/AssetStatsResponseDto.md

@@ -1,4 +1,4 @@
-# openapi.model.AssetCountByUserIdResponseDto
+# openapi.model.AssetStatsResponseDto
 
 
 ## Load the model package
 ## Load the model package
 ```dart
 ```dart
@@ -8,11 +8,9 @@ import 'package:openapi/api.dart';
 ## Properties
 ## Properties
 Name | Type | Description | Notes
 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)
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 
 

+ 1 - 1
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_bulk_upload_check_result.dart';
 part 'model/asset_count_by_time_bucket.dart';
 part 'model/asset_count_by_time_bucket.dart';
 part 'model/asset_count_by_time_bucket_response_dto.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_file_upload_response_dto.dart';
 part 'model/asset_ids_dto.dart';
 part 'model/asset_ids_dto.dart';
 part 'model/asset_ids_response_dto.dart';
 part 'model/asset_ids_response_dto.dart';
 part 'model/asset_response_dto.dart';
 part 'model/asset_response_dto.dart';
+part 'model/asset_stats_response_dto.dart';
 part 'model/asset_type_enum.dart';
 part 'model/asset_type_enum.dart';
 part 'model/audio_codec.dart';
 part 'model/audio_codec.dart';
 part 'model/auth_device_response_dto.dart';
 part 'model/auth_device_response_dto.dart';

+ 34 - 58
mobile/openapi/lib/api/asset_api.dart

@@ -440,47 +440,6 @@ class AssetApi {
     return null;
     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
   /// Get a single asset's information
   ///
   ///
   /// Note: This method returns the HTTP [Response].
   /// Note: This method returns the HTTP [Response].
@@ -639,10 +598,10 @@ class AssetApi {
     return null;
     return null;
   }
   }
 
 
-  /// Performs an HTTP 'GET /asset/count-by-user-id' operation and returns the [Response].
-  Future<Response> getAssetCountByUserIdWithHttpInfo() async {
+  /// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
+  Future<Response> getAssetSearchTermsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     // ignore: prefer_const_declarations
-    final path = r'/asset/count-by-user-id';
+    final path = r'/asset/search-terms';
 
 
     // ignore: prefer_final_locals
     // ignore: prefer_final_locals
     Object? postBody;
     Object? postBody;
@@ -665,8 +624,8 @@ class AssetApi {
     );
     );
   }
   }
 
 
-  Future<AssetCountByUserIdResponseDto?> getAssetCountByUserId() async {
-    final response = await getAssetCountByUserIdWithHttpInfo();
+  Future<List<String>?> getAssetSearchTerms() async {
+    final response = await getAssetSearchTermsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
     }
@@ -674,16 +633,24 @@ class AssetApi {
     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
     // FormatException when trying to decode an empty string.
     // FormatException when trying to decode an empty string.
     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
-      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetCountByUserIdResponseDto',) as AssetCountByUserIdResponseDto;
-    
+      final responseBody = await _decodeBodyBytes(response);
+      return (await apiClient.deserializeAsync(responseBody, 'List<String>') as List)
+        .cast<String>()
+        .toList();
+
     }
     }
     return null;
     return null;
   }
   }
 
 
-  /// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
-  Future<Response> getAssetSearchTermsWithHttpInfo() async {
+  /// 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
     // ignore: prefer_const_declarations
-    final path = r'/asset/search-terms';
+    final path = r'/asset/statistics';
 
 
     // ignore: prefer_final_locals
     // ignore: prefer_final_locals
     Object? postBody;
     Object? postBody;
@@ -692,6 +659,13 @@ class AssetApi {
     final headerParams = <String, String>{};
     final headerParams = <String, String>{};
     final formParams = <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>[];
     const contentTypes = <String>[];
 
 
 
 
@@ -706,8 +680,13 @@ class AssetApi {
     );
     );
   }
   }
 
 
-  Future<List<String>?> getAssetSearchTerms() async {
-    final response = await getAssetSearchTermsWithHttpInfo();
+  /// 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) {
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
     }
@@ -715,11 +694,8 @@ class AssetApi {
     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
     // FormatException when trying to decode an empty string.
     // FormatException when trying to decode an empty string.
     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
-      final responseBody = await _decodeBodyBytes(response);
-      return (await apiClient.deserializeAsync(responseBody, 'List<String>') as List)
-        .cast<String>()
-        .toList();
-
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetStatsResponseDto',) as AssetStatsResponseDto;
+    
     }
     }
     return null;
     return null;
   }
   }

+ 2 - 2
mobile/openapi/lib/api_client.dart

@@ -215,8 +215,6 @@ class ApiClient {
           return AssetCountByTimeBucket.fromJson(value);
           return AssetCountByTimeBucket.fromJson(value);
         case 'AssetCountByTimeBucketResponseDto':
         case 'AssetCountByTimeBucketResponseDto':
           return AssetCountByTimeBucketResponseDto.fromJson(value);
           return AssetCountByTimeBucketResponseDto.fromJson(value);
-        case 'AssetCountByUserIdResponseDto':
-          return AssetCountByUserIdResponseDto.fromJson(value);
         case 'AssetFileUploadResponseDto':
         case 'AssetFileUploadResponseDto':
           return AssetFileUploadResponseDto.fromJson(value);
           return AssetFileUploadResponseDto.fromJson(value);
         case 'AssetIdsDto':
         case 'AssetIdsDto':
@@ -225,6 +223,8 @@ class ApiClient {
           return AssetIdsResponseDto.fromJson(value);
           return AssetIdsResponseDto.fromJson(value);
         case 'AssetResponseDto':
         case 'AssetResponseDto':
           return AssetResponseDto.fromJson(value);
           return AssetResponseDto.fromJson(value);
+        case 'AssetStatsResponseDto':
+          return AssetStatsResponseDto.fromJson(value);
         case 'AssetTypeEnum':
         case 'AssetTypeEnum':
           return AssetTypeEnumTypeTransformer().decode(value);
           return AssetTypeEnumTypeTransformer().decode(value);
         case 'AudioCodec':
         case 'AudioCodec':

+ 0 - 130
mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart

@@ -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',
-  };
-}
-

+ 114 - 0
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',
+  };
+}
+

+ 4 - 9
mobile/openapi/test/asset_api_test.dart

@@ -60,11 +60,6 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
-    //Future<AssetCountByUserIdResponseDto> getArchivedAssetCountByUserId() async
-    test('test getArchivedAssetCountByUserId', () async {
-      // TODO
-    });
-
     // Get a single asset's information
     // Get a single asset's information
     //
     //
     //Future<AssetResponseDto> getAssetById(String id, { String key }) async
     //Future<AssetResponseDto> getAssetById(String id, { String key }) async
@@ -82,13 +77,13 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
-    //Future<AssetCountByUserIdResponseDto> getAssetCountByUserId() async
-    test('test getAssetCountByUserId', () async {
+    //Future<List<String>> getAssetSearchTerms() async
+    test('test getAssetSearchTerms', () async {
       // TODO
       // TODO
     });
     });
 
 
-    //Future<List<String>> getAssetSearchTerms() async
-    test('test getAssetSearchTerms', () async {
+    //Future<AssetStatsResponseDto> getAssetStats({ bool isArchived, bool isFavorite }) async
+    test('test getAssetStats', () async {
       // TODO
       // TODO
     });
     });
 
 

+ 7 - 17
mobile/openapi/test/asset_count_by_user_id_response_dto_test.dart → mobile/openapi/test/asset_stats_response_dto_test.dart

@@ -11,32 +11,22 @@
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 import 'package:test/test.dart';
 import 'package:test/test.dart';
 
 
-// tests for AssetCountByUserIdResponseDto
+// tests for AssetStatsResponseDto
 void main() {
 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
       // 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 {
     test('to test the property `videos`', () async {
       // TODO
       // 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 {
     test('to test the property `total`', () async {
       // TODO
       // TODO
     });
     });

+ 40 - 68
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": {
     "/asset/curated-locations": {
       "get": {
       "get": {
         "operationId": "getCuratedLocations",
         "operationId": "getCuratedLocations",
@@ -1608,17 +1576,34 @@
         ]
         ]
       }
       }
     },
     },
-    "/asset/stat/archive": {
+    "/asset/statistics": {
       "get": {
       "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": {
         "responses": {
           "200": {
           "200": {
             "description": "",
             "description": "",
             "content": {
             "content": {
               "application/json": {
               "application/json": {
                 "schema": {
                 "schema": {
-                  "$ref": "#/components/schemas/AssetCountByUserIdResponseDto"
+                  "$ref": "#/components/schemas/AssetStatsResponseDto"
                 }
                 }
               }
               }
             }
             }
@@ -4786,38 +4771,6 @@
           "buckets"
           "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": {
       "AssetFileUploadResponseDto": {
         "type": "object",
         "type": "object",
         "properties": {
         "properties": {
@@ -4970,6 +4923,25 @@
           "checksum"
           "checksum"
         ]
         ]
       },
       },
+      "AssetStatsResponseDto": {
+        "type": "object",
+        "properties": {
+          "images": {
+            "type": "integer"
+          },
+          "videos": {
+            "type": "integer"
+          },
+          "total": {
+            "type": "integer"
+          }
+        },
+        "required": [
+          "images",
+          "videos",
+          "total"
+        ]
+      },
       "AssetTypeEnum": {
       "AssetTypeEnum": {
         "type": "string",
         "type": "string",
         "enum": [
         "enum": [

+ 8 - 0
server/src/domain/asset/asset.repository.ts

@@ -1,6 +1,13 @@
 import { AssetEntity, AssetType } from '@app/infra/entities';
 import { AssetEntity, AssetType } from '@app/infra/entities';
 import { Paginated, PaginationOptions } from '../domain.util';
 import { Paginated, PaginationOptions } from '../domain.util';
 
 
+export type AssetStats = Record<AssetType, number>;
+
+export interface AssetStatsOptions {
+  isFavorite?: boolean;
+  isArchived?: boolean;
+}
+
 export interface AssetSearchOptions {
 export interface AssetSearchOptions {
   isVisible?: boolean;
   isVisible?: boolean;
   type?: AssetType;
   type?: AssetType;
@@ -55,4 +62,5 @@ export interface IAssetRepository {
   save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
   save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
   findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
   findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
   getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
   getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
+  getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
 }
 }

+ 42 - 2
server/src/domain/asset/asset.service.spec.ts

@@ -1,3 +1,4 @@
+import { AssetType } from '@app/infra/entities';
 import { BadRequestException } from '@nestjs/common';
 import { BadRequestException } from '@nestjs/common';
 import {
 import {
   assetEntityStub,
   assetEntityStub,
@@ -10,9 +11,9 @@ import {
 import { when } from 'jest-when';
 import { when } from 'jest-when';
 import { Readable } from 'stream';
 import { Readable } from 'stream';
 import { IStorageRepository } from '../storage';
 import { IStorageRepository } from '../storage';
-import { IAssetRepository } from './asset.repository';
+import { AssetStats, IAssetRepository } from './asset.repository';
 import { AssetService } from './asset.service';
 import { AssetService } from './asset.service';
-import { DownloadResponseDto } from './index';
+import { AssetStatsResponseDto, DownloadResponseDto } from './dto';
 import { mapAsset } from './response-dto';
 import { mapAsset } from './response-dto';
 
 
 const downloadResponse: DownloadResponseDto = {
 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, () => {
 describe(AssetService.name, () => {
   let sut: AssetService;
   let sut: AssetService;
   let accessMock: IAccessRepositoryMock;
   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, {});
+    });
+  });
 });
 });

+ 6 - 0
server/src/domain/asset/asset.service.ts

@@ -9,6 +9,7 @@ import { HumanReadableSize, usePagination } from '../domain.util';
 import { ImmichReadStream, IStorageRepository } from '../storage';
 import { ImmichReadStream, IStorageRepository } from '../storage';
 import { IAssetRepository } from './asset.repository';
 import { IAssetRepository } from './asset.repository';
 import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
 import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
+import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
 import { MapMarkerDto } from './dto/map-marker.dto';
 import { MapMarkerDto } from './dto/map-marker.dto';
 import { mapAsset, MapMarkerResponseDto } from './response-dto';
 import { mapAsset, MapMarkerResponseDto } from './response-dto';
 import { MemoryLaneResponseDto } from './response-dto/memory-lane-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');
     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);
+  }
 }
 }

+ 37 - 0
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),
+  };
+};

+ 1 - 0
server/src/domain/asset/dto/index.ts

@@ -1,4 +1,5 @@
 export * from './asset-ids.dto';
 export * from './asset-ids.dto';
+export * from './asset-statistics.dto';
 export * from './download.dto';
 export * from './download.dto';
 export * from './map-marker.dto';
 export * from './map-marker.dto';
 export * from './memory-lane.dto';
 export * from './memory-lane.dto';

+ 1 - 56
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 { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { InjectRepository } from '@nestjs/typeorm';
 import { IsNull, Not } from '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 { SearchPropertiesDto } from './dto/search-properties.dto';
 import { UpdateAssetDto } from './dto/update-asset.dto';
 import { UpdateAssetDto } from './dto/update-asset.dto';
 import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-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';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 
 
@@ -38,8 +37,6 @@ export interface IAssetRepository {
   getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
   getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
   getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
   getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
   getAssetCountByTimeBucket(userId: string, dto: GetAssetCountByTimeBucketDto): Promise<AssetCountByTimeBucket[]>;
   getAssetCountByTimeBucket(userId: string, dto: GetAssetCountByTimeBucketDto): Promise<AssetCountByTimeBucket[]>;
-  getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
-  getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
   getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
   getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
   getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
   getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
   getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
   getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
@@ -55,35 +52,6 @@ export class AssetRepository implements IAssetRepository {
     @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
     @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[]> {
   async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
     // Get asset entity from a list of time buckets
     // Get asset entity from a list of time buckets
     let builder = this.assetRepository
     let builder = this.assetRepository
@@ -337,29 +305,6 @@ export class AssetRepository implements IAssetRepository {
     return assets.map((asset) => asset.deviceAssetId);
     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> {
   getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> {
     return this.assetRepository.findOne({
     return this.assetRepository.findOne({
       select: {
       select: {

+ 0 - 10
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 { UpdateAssetDto } from './dto/update-asset.dto';
 import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
 import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
 import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-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 { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-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);
     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
    * Get all AssetEntity belong to the user
    */
    */

+ 0 - 35
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 { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
 import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
 import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-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 _getCreateAssetDto = (): CreateAssetDto => {
   const createAssetDto = new CreateAssetDto();
   const createAssetDto = new CreateAssetDto();
@@ -103,24 +102,6 @@ const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
   return [result1, result2];
   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 = {
 const uploadFile = {
   nullAuth: {
   nullAuth: {
     authUser: null,
     authUser: null,
@@ -197,8 +178,6 @@ describe('AssetService', () => {
       getSearchPropertiesByUserId: jest.fn(),
       getSearchPropertiesByUserId: jest.fn(),
       getAssetByTimeBucket: jest.fn(),
       getAssetByTimeBucket: jest.fn(),
       getAssetsByChecksums: jest.fn(),
       getAssetsByChecksums: jest.fn(),
-      getAssetCountByUserId: jest.fn(),
-      getArchivedAssetCountByUserId: jest.fn(),
       getExistingAssets: jest.fn(),
       getExistingAssets: jest.fn(),
       getByOriginalPath: jest.fn(),
       getByOriginalPath: jest.fn(),
     };
     };
@@ -467,20 +446,6 @@ describe('AssetService', () => {
     expect(result.buckets.length).toEqual(2);
     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', () => {
   describe('deleteAll', () => {
     it('should return failed status when an asset is missing', async () => {
     it('should return failed status when an asset is missing', async () => {
       assetRepositoryMock.get.mockResolvedValue(null);
       assetRepositoryMock.get.mockResolvedValue(null);

+ 0 - 9
server/src/immich/api-v1/asset/asset.service.ts

@@ -58,7 +58,6 @@ import {
   AssetCountByTimeBucketResponseDto,
   AssetCountByTimeBucketResponseDto,
   mapAssetCountByTimeBucket,
   mapAssetCountByTimeBucket,
 } from './response-dto/asset-count-by-time-group-response.dto';
 } 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 { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
 import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
@@ -536,14 +535,6 @@ export class AssetService {
     return mapAssetCountByTimeBucket(result);
     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) {
   getExifPermission(authUser: AuthUserDto) {
     return !authUser.isPublicUser || authUser.isShowExif;
     return !authUser.isPublicUser || authUser.isShowExif;
   }
   }

+ 0 - 18
server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts

@@ -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;
-}

+ 7 - 0
server/src/immich/controllers/asset.controller.ts

@@ -1,6 +1,8 @@
 import {
 import {
   AssetIdsDto,
   AssetIdsDto,
   AssetService,
   AssetService,
+  AssetStatsDto,
+  AssetStatsResponseDto,
   AuthUserDto,
   AuthUserDto,
   DownloadDto,
   DownloadDto,
   DownloadResponseDto,
   DownloadResponseDto,
@@ -53,4 +55,9 @@ export class AssetController {
   downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
   downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
     return this.service.downloadFile(authUser, id).then(asStreamableFile);
     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);
+  }
 }
 }

+ 36 - 0
server/src/infra/repositories/asset.repository.ts

@@ -1,5 +1,7 @@
 import {
 import {
   AssetSearchOptions,
   AssetSearchOptions,
+  AssetStats,
+  AssetStatsOptions,
   IAssetRepository,
   IAssetRepository,
   LivePhotoSearchOptions,
   LivePhotoSearchOptions,
   MapMarker,
   MapMarker,
@@ -321,4 +323,38 @@ export class AssetRepository implements IAssetRepository {
       lon: asset.exifInfo!.longitude!,
       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;
+  }
 }
 }

+ 1 - 0
server/test/repositories/asset.repository.mock.ts

@@ -18,5 +18,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
     save: jest.fn(),
     save: jest.fn(),
     findLivePhotoMatch: jest.fn(),
     findLivePhotoMatch: jest.fn(),
     getMapMarkers: jest.fn(),
     getMapMarkers: jest.fn(),
+    getStatistics: jest.fn(),
   };
   };
 };
 };

+ 77 - 118
web/src/api/open-api/api.ts

@@ -486,43 +486,6 @@ export interface AssetCountByTimeBucketResponseDto {
      */
      */
     'buckets': Array<AssetCountByTimeBucket>;
     '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
  * @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
  * @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);
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -5088,8 +5038,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
             let baseOptions;
@@ -5123,11 +5073,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         },
         /**
         /**
          * 
          * 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
             let baseOptions;
@@ -5148,6 +5100,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
 
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
+
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
+
 
 
     
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             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);
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
             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
          * Get a single asset\'s information
          * @param {string} id 
          * @param {string} id 
@@ -5941,17 +5892,19 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         },
         /**
         /**
          * 
          * 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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);
             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>> {
         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));
             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
          * Get a single asset\'s information
          * @param {string} id 
          * @param {string} id 
@@ -6218,16 +6163,18 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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
     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.
  * Request parameters for getAssetThumbnail operation in AssetApi.
  * @export
  * @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));
         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
      * Get a single asset\'s information
      * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
      * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@@ -7006,18 +6964,19 @@ export class AssetApi extends BaseAPI {
      * @throws {RequiredError}
      * @throws {RequiredError}
      * @memberof AssetApi
      * @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.
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @throws {RequiredError}
      * @memberof AssetApi
      * @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));
     }
     }
 
 
     /**
     /**

+ 11 - 48
web/src/lib/components/shared-components/side-bar/side-bar.svelte

@@ -1,6 +1,6 @@
 <script lang="ts">
 <script lang="ts">
   import { page } from '$app/stores';
   import { page } from '$app/stores';
-  import { api } from '@api';
+  import { AssetApiGetAssetStatsRequest, api } from '@api';
   import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
   import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
   import AccountMultiple from 'svelte-material-icons/AccountMultiple.svelte';
   import AccountMultiple from 'svelte-material-icons/AccountMultiple.svelte';
   import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
   import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
@@ -18,31 +18,9 @@
   import { locale } from '$lib/stores/preferences.store';
   import { locale } from '$lib/stores/preferences.store';
   import SideBarSection from './side-bar-section.svelte';
   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 () => {
   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 isFavoritesSelected = $page.route.id === '/(user)/favorites';
   const isPhotosSelected = $page.route.id === '/(user)/photos';
   const isPhotosSelected = $page.route.id === '/(user)/photos';
   const isSharingSelected = $page.route.id === '/(user)/sharing';
   const isSharingSelected = $page.route.id === '/(user)/sharing';
@@ -83,12 +45,12 @@
       isSelected={isPhotosSelected}
       isSelected={isPhotosSelected}
     >
     >
       <svelte:fragment slot="moreInformation">
       <svelte:fragment slot="moreInformation">
-        {#await getAssetCount()}
+        {#await getStats({ isArchived: false })}
           <LoadingSpinner />
           <LoadingSpinner />
         {:then data}
         {:then data}
           <div>
           <div>
             <p>{data.videos.toLocaleString($locale)} Videos</p>
             <p>{data.videos.toLocaleString($locale)} Videos</p>
-            <p>{data.photos.toLocaleString($locale)} Photos</p>
+            <p>{data.images.toLocaleString($locale)} Photos</p>
           </div>
           </div>
         {/await}
         {/await}
       </svelte:fragment>
       </svelte:fragment>
@@ -129,11 +91,12 @@
       isSelected={isFavoritesSelected}
       isSelected={isFavoritesSelected}
     >
     >
       <svelte:fragment slot="moreInformation">
       <svelte:fragment slot="moreInformation">
-        {#await getFavoriteCount()}
+        {#await getStats({ isFavorite: true })}
           <LoadingSpinner />
           <LoadingSpinner />
         {:then data}
         {:then data}
           <div>
           <div>
-            <p>{data.favorites} Favorites</p>
+            <p>{data.videos.toLocaleString($locale)} Videos</p>
+            <p>{data.images.toLocaleString($locale)} Photos</p>
           </div>
           </div>
         {/await}
         {/await}
       </svelte:fragment>
       </svelte:fragment>
@@ -155,12 +118,12 @@
   <a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
   <a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
     <SideBarButton title="Archive" logo={ArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}>
     <SideBarButton title="Archive" logo={ArchiveArrowDownOutline} isSelected={$page.route.id === '/(user)/archive'}>
       <svelte:fragment slot="moreInformation">
       <svelte:fragment slot="moreInformation">
-        {#await getArchivedAssetsCount()}
+        {#await getStats({ isArchived: true })}
           <LoadingSpinner />
           <LoadingSpinner />
         {:then data}
         {:then data}
           <div>
           <div>
             <p>{data.videos.toLocaleString($locale)} Videos</p>
             <p>{data.videos.toLocaleString($locale)} Videos</p>
-            <p>{data.photos.toLocaleString($locale)} Photos</p>
+            <p>{data.images.toLocaleString($locale)} Photos</p>
           </div>
           </div>
         {/await}
         {/await}
       </svelte:fragment>
       </svelte:fragment>

+ 5 - 5
web/src/routes/(user)/photos/+page.svelte

@@ -10,22 +10,22 @@
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.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 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 { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
   import { assetStore } from '$lib/stores/assets.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 { onDestroy, onMount } from 'svelte';
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
   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;
   export let data: PageData;
   let assetCount = 1;
   let assetCount = 1;
 
 
   onMount(async () => {
   onMount(async () => {
-    const { data: allAssetCount } = await api.assetApi.getAssetCountByUserId();
-    assetCount = allAssetCount.total;
+    const { data: stats } = await api.assetApi.getAssetStats();
+    assetCount = stats.total;
   });
   });
 
 
   onDestroy(() => {
   onDestroy(() => {