From f952bc0b64c9e6f7f2a8320bc12a31576040be4e Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jrasm91@gmail.com>
Date: Fri, 14 Jul 2023 09:30:17 -0400
Subject: [PATCH] refactor(server): asset stats (#3253)

* refactor(server): asset stats

* chore: open api
---
 cli/src/api/open-api/api.ts                   | 194 +++++++----------
 mobile/openapi/.openapi-generator/FILES       |   6 +-
 mobile/openapi/README.md                      |   5 +-
 mobile/openapi/doc/AssetApi.md                | 162 ++++++---------
 ...esponseDto.md => AssetStatsResponseDto.md} |  10 +-
 mobile/openapi/lib/api.dart                   |   2 +-
 mobile/openapi/lib/api/asset_api.dart         | 140 ++++++-------
 mobile/openapi/lib/api_client.dart            |   4 +-
 .../asset_count_by_user_id_response_dto.dart  | 130 ------------
 .../lib/model/asset_stats_response_dto.dart   | 114 ++++++++++
 mobile/openapi/test/asset_api_test.dart       |  13 +-
 ...art => asset_stats_response_dto_test.dart} |  24 +--
 server/immich-openapi-specs.json              | 108 ++++------
 server/src/domain/asset/asset.repository.ts   |   8 +
 server/src/domain/asset/asset.service.spec.ts |  44 +++-
 server/src/domain/asset/asset.service.ts      |   6 +
 .../domain/asset/dto/asset-statistics.dto.ts  |  37 ++++
 server/src/domain/asset/dto/index.ts          |   1 +
 .../immich/api-v1/asset/asset-repository.ts   |  57 +----
 .../immich/api-v1/asset/asset.controller.ts   |  10 -
 .../immich/api-v1/asset/asset.service.spec.ts |  35 ----
 .../src/immich/api-v1/asset/asset.service.ts  |   9 -
 .../asset-count-by-user-id-response.dto.ts    |  18 --
 .../immich/controllers/asset.controller.ts    |   7 +
 .../infra/repositories/asset.repository.ts    |  36 ++++
 .../repositories/asset.repository.mock.ts     |   1 +
 web/src/api/open-api/api.ts                   | 195 +++++++-----------
 .../side-bar/side-bar.svelte                  |  59 +-----
 web/src/routes/(user)/photos/+page.svelte     |  10 +-
 29 files changed, 601 insertions(+), 844 deletions(-)
 rename mobile/openapi/doc/{AssetCountByUserIdResponseDto.md => AssetStatsResponseDto.md} (58%)
 delete mode 100644 mobile/openapi/lib/model/asset_count_by_user_id_response_dto.dart
 create mode 100644 mobile/openapi/lib/model/asset_stats_response_dto.dart
 rename mobile/openapi/test/{asset_count_by_user_id_response_dto_test.dart => asset_stats_response_dto_test.dart} (50%)
 create mode 100644 server/src/domain/asset/dto/asset-statistics.dto.ts
 delete mode 100644 server/src/immich/api-v1/asset/response-dto/asset-count-by-user-id-response.dto.ts

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