From f980a2f27a0ae1cb397b93a41c79ee5a88a19762 Mon Sep 17 00:00:00 2001
From: Alex <alex.tran1502@gmail.com>
Date: Fri, 26 Aug 2022 22:53:37 -0700
Subject: [PATCH] Add asset repository and refactor asset service (#540)

* build endpoint to get asset count by month

* Added asset repository

* Added create asset

* get asset by device ID

* Added test for existing methods

* Refactor additional endpoint

* Refactor database api to get curated locations and curated objects

* Refactor get search properties

* Fixed cookies parsing for websocket

* Added API to get asset count by time group

* Remove unused code
---
 mobile/openapi/.openapi-generator/FILES       |   8 +
 mobile/openapi/README.md                      |  11 +-
 mobile/openapi/doc/AssetApi.md                |  54 ++++-
 .../openapi/doc/AssetCountByTimeGroupDto.md   |  16 ++
 .../doc/AssetCountByTimeGroupResponseDto.md   |  16 ++
 .../doc/GetAssetCountByTimeGroupDto.md        |  15 ++
 mobile/openapi/doc/TimeGroupEnum.md           |  14 ++
 mobile/openapi/lib/api.dart                   |   4 +
 mobile/openapi/lib/api/asset_api.dart         |  59 +++++-
 mobile/openapi/lib/api_client.dart            |   8 +
 mobile/openapi/lib/api_helper.dart            |   3 +
 .../model/asset_count_by_time_group_dto.dart  | 119 +++++++++++
 ...sset_count_by_time_group_response_dto.dart | 119 +++++++++++
 .../get_asset_count_by_time_group_dto.dart    | 111 +++++++++++
 mobile/openapi/lib/model/time_group_enum.dart |  85 ++++++++
 .../asset_count_by_time_group_dto_test.dart   |  32 +++
 ...count_by_time_group_response_dto_test.dart |  32 +++
 ...et_asset_count_by_time_group_dto_test.dart |  27 +++
 mobile/openapi/test/time_group_enum_test.dart |  21 ++
 .../src/api-v1/asset/asset-repository.ts      | 187 ++++++++++++++++++
 .../src/api-v1/asset/asset.controller.ts      |  17 +-
 .../immich/src/api-v1/asset/asset.module.ts   |  10 +-
 .../src/api-v1/asset/asset.service.spec.ts    |  92 +++++++++
 .../immich/src/api-v1/asset/asset.service.ts  | 162 +++++----------
 .../dto/get-asset-count-by-time-group.dto.ts  |  16 ++
 .../api-v1/asset/dto/search-properties.dto.ts |  12 ++
 .../asset-count-by-time-group-response.dto.ts |  23 +++
 .../communication/communication.gateway.ts    |  20 +-
 server/immich-openapi-specs.json              |   2 +-
 web/src/api/open-api/api.ts                   | 140 ++++++++++++-
 web/src/lib/stores/websocket.ts               |   1 -
 web/src/routes/+layout.server.ts              |   7 +-
 web/src/routes/photos/+page.svelte            |  16 +-
 33 files changed, 1321 insertions(+), 138 deletions(-)
 create mode 100644 mobile/openapi/doc/AssetCountByTimeGroupDto.md
 create mode 100644 mobile/openapi/doc/AssetCountByTimeGroupResponseDto.md
 create mode 100644 mobile/openapi/doc/GetAssetCountByTimeGroupDto.md
 create mode 100644 mobile/openapi/doc/TimeGroupEnum.md
 create mode 100644 mobile/openapi/lib/model/asset_count_by_time_group_dto.dart
 create mode 100644 mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart
 create mode 100644 mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart
 create mode 100644 mobile/openapi/lib/model/time_group_enum.dart
 create mode 100644 mobile/openapi/test/asset_count_by_time_group_dto_test.dart
 create mode 100644 mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart
 create mode 100644 mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart
 create mode 100644 mobile/openapi/test/time_group_enum_test.dart
 create mode 100644 server/apps/immich/src/api-v1/asset/asset-repository.ts
 create mode 100644 server/apps/immich/src/api-v1/asset/asset.service.spec.ts
 create mode 100644 server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts
 create mode 100644 server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts
 create mode 100644 server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts

diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index 0b891e36af..250e641ccd 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -8,6 +8,8 @@ doc/AdminSignupResponseDto.md
 doc/AlbumApi.md
 doc/AlbumResponseDto.md
 doc/AssetApi.md
+doc/AssetCountByTimeGroupDto.md
+doc/AssetCountByTimeGroupResponseDto.md
 doc/AssetFileUploadResponseDto.md
 doc/AssetResponseDto.md
 doc/AssetTypeEnum.md
@@ -27,6 +29,7 @@ doc/DeviceInfoApi.md
 doc/DeviceInfoResponseDto.md
 doc/DeviceTypeEnum.md
 doc/ExifResponseDto.md
+doc/GetAssetCountByTimeGroupDto.md
 doc/LoginCredentialDto.md
 doc/LoginResponseDto.md
 doc/LogoutResponseDto.md
@@ -39,6 +42,7 @@ doc/ServerVersionReponseDto.md
 doc/SignUpDto.md
 doc/SmartInfoResponseDto.md
 doc/ThumbnailFormat.md
+doc/TimeGroupEnum.md
 doc/UpdateAlbumDto.md
 doc/UpdateDeviceInfoDto.md
 doc/UpdateUserDto.md
@@ -66,6 +70,8 @@ lib/model/add_assets_dto.dart
 lib/model/add_users_dto.dart
 lib/model/admin_signup_response_dto.dart
 lib/model/album_response_dto.dart
+lib/model/asset_count_by_time_group_dto.dart
+lib/model/asset_count_by_time_group_response_dto.dart
 lib/model/asset_file_upload_response_dto.dart
 lib/model/asset_response_dto.dart
 lib/model/asset_type_enum.dart
@@ -83,6 +89,7 @@ lib/model/delete_asset_status.dart
 lib/model/device_info_response_dto.dart
 lib/model/device_type_enum.dart
 lib/model/exif_response_dto.dart
+lib/model/get_asset_count_by_time_group_dto.dart
 lib/model/login_credential_dto.dart
 lib/model/login_response_dto.dart
 lib/model/logout_response_dto.dart
@@ -94,6 +101,7 @@ lib/model/server_version_reponse_dto.dart
 lib/model/sign_up_dto.dart
 lib/model/smart_info_response_dto.dart
 lib/model/thumbnail_format.dart
+lib/model/time_group_enum.dart
 lib/model/update_album_dto.dart
 lib/model/update_device_info_dto.dart
 lib/model/update_user_dto.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 03f08f98e3..f14d52e6d2 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -79,10 +79,11 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download | 
 *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | 
 *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} | 
-*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/searchTerm | 
+*AssetApi* | [**getAssetCountByTimeGroup**](doc//AssetApi.md#getassetcountbytimegroup) | **GET** /asset/count-by-date | 
+*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | 
 *AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} | 
-*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/allLocation | 
-*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/allObjects | 
+*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
+*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 *AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | 
 *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file | 
@@ -112,6 +113,8 @@ Class | Method | HTTP request | Description
  - [AddUsersDto](doc//AddUsersDto.md)
  - [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
  - [AlbumResponseDto](doc//AlbumResponseDto.md)
+ - [AssetCountByTimeGroupDto](doc//AssetCountByTimeGroupDto.md)
+ - [AssetCountByTimeGroupResponseDto](doc//AssetCountByTimeGroupResponseDto.md)
  - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
  - [AssetResponseDto](doc//AssetResponseDto.md)
  - [AssetTypeEnum](doc//AssetTypeEnum.md)
@@ -129,6 +132,7 @@ Class | Method | HTTP request | Description
  - [DeviceInfoResponseDto](doc//DeviceInfoResponseDto.md)
  - [DeviceTypeEnum](doc//DeviceTypeEnum.md)
  - [ExifResponseDto](doc//ExifResponseDto.md)
+ - [GetAssetCountByTimeGroupDto](doc//GetAssetCountByTimeGroupDto.md)
  - [LoginCredentialDto](doc//LoginCredentialDto.md)
  - [LoginResponseDto](doc//LoginResponseDto.md)
  - [LogoutResponseDto](doc//LogoutResponseDto.md)
@@ -140,6 +144,7 @@ Class | Method | HTTP request | Description
  - [SignUpDto](doc//SignUpDto.md)
  - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
  - [ThumbnailFormat](doc//ThumbnailFormat.md)
+ - [TimeGroupEnum](doc//TimeGroupEnum.md)
  - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
  - [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
  - [UpdateUserDto](doc//UpdateUserDto.md)
diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md
index dc01b3282e..91268af324 100644
--- a/mobile/openapi/doc/AssetApi.md
+++ b/mobile/openapi/doc/AssetApi.md
@@ -14,10 +14,11 @@ Method | HTTP request | Description
 [**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download | 
 [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | 
 [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} | 
-[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/searchTerm | 
+[**getAssetCountByTimeGroup**](AssetApi.md#getassetcountbytimegroup) | **GET** /asset/count-by-date | 
+[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | 
 [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} | 
-[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/allLocation | 
-[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/allObjects | 
+[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
+[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 [**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | 
 [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file | 
@@ -267,6 +268,53 @@ 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)
 
+# **getAssetCountByTimeGroup**
+> AssetCountByTimeGroupResponseDto getAssetCountByTimeGroup(getAssetCountByTimeGroupDto)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// 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 getAssetCountByTimeGroupDto = GetAssetCountByTimeGroupDto(); // GetAssetCountByTimeGroupDto | 
+
+try {
+    final result = api_instance.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto);
+    print(result);
+} catch (e) {
+    print('Exception when calling AssetApi->getAssetCountByTimeGroup: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **getAssetCountByTimeGroupDto** | [**GetAssetCountByTimeGroupDto**](GetAssetCountByTimeGroupDto.md)|  | 
+
+### Return type
+
+[**AssetCountByTimeGroupResponseDto**](AssetCountByTimeGroupResponseDto.md)
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **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()
 
diff --git a/mobile/openapi/doc/AssetCountByTimeGroupDto.md b/mobile/openapi/doc/AssetCountByTimeGroupDto.md
new file mode 100644
index 0000000000..f75884e16b
--- /dev/null
+++ b/mobile/openapi/doc/AssetCountByTimeGroupDto.md
@@ -0,0 +1,16 @@
+# openapi.model.AssetCountByTimeGroupDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**timeGroup** | **String** |  | 
+**count** | **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/doc/AssetCountByTimeGroupResponseDto.md b/mobile/openapi/doc/AssetCountByTimeGroupResponseDto.md
new file mode 100644
index 0000000000..86990303a5
--- /dev/null
+++ b/mobile/openapi/doc/AssetCountByTimeGroupResponseDto.md
@@ -0,0 +1,16 @@
+# openapi.model.AssetCountByTimeGroupResponseDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**totalAssets** | **int** |  | 
+**groups** | [**List<AssetCountByTimeGroupDto>**](AssetCountByTimeGroupDto.md) |  | [default to const []]
+
+[[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/doc/GetAssetCountByTimeGroupDto.md b/mobile/openapi/doc/GetAssetCountByTimeGroupDto.md
new file mode 100644
index 0000000000..12dccc261d
--- /dev/null
+++ b/mobile/openapi/doc/GetAssetCountByTimeGroupDto.md
@@ -0,0 +1,15 @@
+# openapi.model.GetAssetCountByTimeGroupDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**timeGroup** | [**TimeGroupEnum**](TimeGroupEnum.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)
+
+
diff --git a/mobile/openapi/doc/TimeGroupEnum.md b/mobile/openapi/doc/TimeGroupEnum.md
new file mode 100644
index 0000000000..d037b9f356
--- /dev/null
+++ b/mobile/openapi/doc/TimeGroupEnum.md
@@ -0,0 +1,14 @@
+# openapi.model.TimeGroupEnum
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[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 afb9e664c3..8a65cf7f34 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -38,6 +38,8 @@ part 'model/add_assets_dto.dart';
 part 'model/add_users_dto.dart';
 part 'model/admin_signup_response_dto.dart';
 part 'model/album_response_dto.dart';
+part 'model/asset_count_by_time_group_dto.dart';
+part 'model/asset_count_by_time_group_response_dto.dart';
 part 'model/asset_file_upload_response_dto.dart';
 part 'model/asset_response_dto.dart';
 part 'model/asset_type_enum.dart';
@@ -55,6 +57,7 @@ part 'model/delete_asset_status.dart';
 part 'model/device_info_response_dto.dart';
 part 'model/device_type_enum.dart';
 part 'model/exif_response_dto.dart';
+part 'model/get_asset_count_by_time_group_dto.dart';
 part 'model/login_credential_dto.dart';
 part 'model/login_response_dto.dart';
 part 'model/logout_response_dto.dart';
@@ -66,6 +69,7 @@ part 'model/server_version_reponse_dto.dart';
 part 'model/sign_up_dto.dart';
 part 'model/smart_info_response_dto.dart';
 part 'model/thumbnail_format.dart';
+part 'model/time_group_enum.dart';
 part 'model/update_album_dto.dart';
 part 'model/update_device_info_dto.dart';
 part 'model/update_user_dto.dart';
diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart
index f83ef32fb0..5c09259062 100644
--- a/mobile/openapi/lib/api/asset_api.dart
+++ b/mobile/openapi/lib/api/asset_api.dart
@@ -298,10 +298,57 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/searchTerm' operation and returns the [Response].
+  /// Performs an HTTP 'GET /asset/count-by-date' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [GetAssetCountByTimeGroupDto] getAssetCountByTimeGroupDto (required):
+  Future<Response> getAssetCountByTimeGroupWithHttpInfo(GetAssetCountByTimeGroupDto getAssetCountByTimeGroupDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/asset/count-by-date';
+
+    // ignore: prefer_final_locals
+    Object? postBody = getAssetCountByTimeGroupDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [GetAssetCountByTimeGroupDto] getAssetCountByTimeGroupDto (required):
+  Future<AssetCountByTimeGroupResponseDto?> getAssetCountByTimeGroup(GetAssetCountByTimeGroupDto getAssetCountByTimeGroupDto,) async {
+    final response = await getAssetCountByTimeGroupWithHttpInfo(getAssetCountByTimeGroupDto,);
+    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), 'AssetCountByTimeGroupResponseDto',) as AssetCountByTimeGroupResponseDto;
+    
+    }
+    return null;
+  }
+
+  /// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
   Future<Response> getAssetSearchTermsWithHttpInfo() async {
     // ignore: prefer_const_declarations
-    final path = r'/asset/searchTerm';
+    final path = r'/asset/search-terms';
 
     // ignore: prefer_final_locals
     Object? postBody;
@@ -398,10 +445,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/allLocation' operation and returns the [Response].
+  /// Performs an HTTP 'GET /asset/curated-locations' operation and returns the [Response].
   Future<Response> getCuratedLocationsWithHttpInfo() async {
     // ignore: prefer_const_declarations
-    final path = r'/asset/allLocation';
+    final path = r'/asset/curated-locations';
 
     // ignore: prefer_final_locals
     Object? postBody;
@@ -442,10 +489,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/allObjects' operation and returns the [Response].
+  /// Performs an HTTP 'GET /asset/curated-objects' operation and returns the [Response].
   Future<Response> getCuratedObjectsWithHttpInfo() async {
     // ignore: prefer_const_declarations
-    final path = r'/asset/allObjects';
+    final path = r'/asset/curated-objects';
 
     // ignore: prefer_final_locals
     Object? postBody;
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 262445371e..6e9050da96 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -200,6 +200,10 @@ class ApiClient {
           return AdminSignupResponseDto.fromJson(value);
         case 'AlbumResponseDto':
           return AlbumResponseDto.fromJson(value);
+        case 'AssetCountByTimeGroupDto':
+          return AssetCountByTimeGroupDto.fromJson(value);
+        case 'AssetCountByTimeGroupResponseDto':
+          return AssetCountByTimeGroupResponseDto.fromJson(value);
         case 'AssetFileUploadResponseDto':
           return AssetFileUploadResponseDto.fromJson(value);
         case 'AssetResponseDto':
@@ -234,6 +238,8 @@ class ApiClient {
           return DeviceTypeEnumTypeTransformer().decode(value);
         case 'ExifResponseDto':
           return ExifResponseDto.fromJson(value);
+        case 'GetAssetCountByTimeGroupDto':
+          return GetAssetCountByTimeGroupDto.fromJson(value);
         case 'LoginCredentialDto':
           return LoginCredentialDto.fromJson(value);
         case 'LoginResponseDto':
@@ -256,6 +262,8 @@ class ApiClient {
           return SmartInfoResponseDto.fromJson(value);
         case 'ThumbnailFormat':
           return ThumbnailFormatTypeTransformer().decode(value);
+        case 'TimeGroupEnum':
+          return TimeGroupEnumTypeTransformer().decode(value);
         case 'UpdateAlbumDto':
           return UpdateAlbumDto.fromJson(value);
         case 'UpdateDeviceInfoDto':
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index 4e0c1e1572..26c90fd0c7 100644
--- a/mobile/openapi/lib/api_helper.dart
+++ b/mobile/openapi/lib/api_helper.dart
@@ -67,6 +67,9 @@ String parameterToString(dynamic value) {
   if (value is ThumbnailFormat) {
     return ThumbnailFormatTypeTransformer().encode(value).toString();
   }
+  if (value is TimeGroupEnum) {
+    return TimeGroupEnumTypeTransformer().encode(value).toString();
+  }
   return value.toString();
 }
 
diff --git a/mobile/openapi/lib/model/asset_count_by_time_group_dto.dart b/mobile/openapi/lib/model/asset_count_by_time_group_dto.dart
new file mode 100644
index 0000000000..c3d805612b
--- /dev/null
+++ b/mobile/openapi/lib/model/asset_count_by_time_group_dto.dart
@@ -0,0 +1,119 @@
+//
+// 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 AssetCountByTimeGroupDto {
+  /// Returns a new [AssetCountByTimeGroupDto] instance.
+  AssetCountByTimeGroupDto({
+    required this.timeGroup,
+    required this.count,
+  });
+
+  String timeGroup;
+
+  int count;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeGroupDto &&
+     other.timeGroup == timeGroup &&
+     other.count == count;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (timeGroup.hashCode) +
+    (count.hashCode);
+
+  @override
+  String toString() => 'AssetCountByTimeGroupDto[timeGroup=$timeGroup, count=$count]';
+
+  Map<String, dynamic> toJson() {
+    final _json = <String, dynamic>{};
+      _json[r'timeGroup'] = timeGroup;
+      _json[r'count'] = count;
+    return _json;
+  }
+
+  /// Returns a new [AssetCountByTimeGroupDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static AssetCountByTimeGroupDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "AssetCountByTimeGroupDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "AssetCountByTimeGroupDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return AssetCountByTimeGroupDto(
+        timeGroup: mapValueOfType<String>(json, r'timeGroup')!,
+        count: mapValueOfType<int>(json, r'count')!,
+      );
+    }
+    return null;
+  }
+
+  static List<AssetCountByTimeGroupDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <AssetCountByTimeGroupDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = AssetCountByTimeGroupDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, AssetCountByTimeGroupDto> mapFromJson(dynamic json) {
+    final map = <String, AssetCountByTimeGroupDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = AssetCountByTimeGroupDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of AssetCountByTimeGroupDto-objects as value to a dart map
+  static Map<String, List<AssetCountByTimeGroupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<AssetCountByTimeGroupDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = AssetCountByTimeGroupDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'timeGroup',
+    'count',
+  };
+}
+
diff --git a/mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart b/mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart
new file mode 100644
index 0000000000..ddd771f518
--- /dev/null
+++ b/mobile/openapi/lib/model/asset_count_by_time_group_response_dto.dart
@@ -0,0 +1,119 @@
+//
+// 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 AssetCountByTimeGroupResponseDto {
+  /// Returns a new [AssetCountByTimeGroupResponseDto] instance.
+  AssetCountByTimeGroupResponseDto({
+    required this.totalAssets,
+    this.groups = const [],
+  });
+
+  int totalAssets;
+
+  List<AssetCountByTimeGroupDto> groups;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeGroupResponseDto &&
+     other.totalAssets == totalAssets &&
+     other.groups == groups;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (totalAssets.hashCode) +
+    (groups.hashCode);
+
+  @override
+  String toString() => 'AssetCountByTimeGroupResponseDto[totalAssets=$totalAssets, groups=$groups]';
+
+  Map<String, dynamic> toJson() {
+    final _json = <String, dynamic>{};
+      _json[r'totalAssets'] = totalAssets;
+      _json[r'groups'] = groups;
+    return _json;
+  }
+
+  /// Returns a new [AssetCountByTimeGroupResponseDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static AssetCountByTimeGroupResponseDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "AssetCountByTimeGroupResponseDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "AssetCountByTimeGroupResponseDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return AssetCountByTimeGroupResponseDto(
+        totalAssets: mapValueOfType<int>(json, r'totalAssets')!,
+        groups: AssetCountByTimeGroupDto.listFromJson(json[r'groups'])!,
+      );
+    }
+    return null;
+  }
+
+  static List<AssetCountByTimeGroupResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <AssetCountByTimeGroupResponseDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = AssetCountByTimeGroupResponseDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, AssetCountByTimeGroupResponseDto> mapFromJson(dynamic json) {
+    final map = <String, AssetCountByTimeGroupResponseDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = AssetCountByTimeGroupResponseDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of AssetCountByTimeGroupResponseDto-objects as value to a dart map
+  static Map<String, List<AssetCountByTimeGroupResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<AssetCountByTimeGroupResponseDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = AssetCountByTimeGroupResponseDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'totalAssets',
+    'groups',
+  };
+}
+
diff --git a/mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart b/mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart
new file mode 100644
index 0000000000..1e296d3770
--- /dev/null
+++ b/mobile/openapi/lib/model/get_asset_count_by_time_group_dto.dart
@@ -0,0 +1,111 @@
+//
+// 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 GetAssetCountByTimeGroupDto {
+  /// Returns a new [GetAssetCountByTimeGroupDto] instance.
+  GetAssetCountByTimeGroupDto({
+    required this.timeGroup,
+  });
+
+  TimeGroupEnum timeGroup;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is GetAssetCountByTimeGroupDto &&
+     other.timeGroup == timeGroup;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (timeGroup.hashCode);
+
+  @override
+  String toString() => 'GetAssetCountByTimeGroupDto[timeGroup=$timeGroup]';
+
+  Map<String, dynamic> toJson() {
+    final _json = <String, dynamic>{};
+      _json[r'timeGroup'] = timeGroup;
+    return _json;
+  }
+
+  /// Returns a new [GetAssetCountByTimeGroupDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static GetAssetCountByTimeGroupDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "GetAssetCountByTimeGroupDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "GetAssetCountByTimeGroupDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return GetAssetCountByTimeGroupDto(
+        timeGroup: TimeGroupEnum.fromJson(json[r'timeGroup'])!,
+      );
+    }
+    return null;
+  }
+
+  static List<GetAssetCountByTimeGroupDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <GetAssetCountByTimeGroupDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = GetAssetCountByTimeGroupDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, GetAssetCountByTimeGroupDto> mapFromJson(dynamic json) {
+    final map = <String, GetAssetCountByTimeGroupDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = GetAssetCountByTimeGroupDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of GetAssetCountByTimeGroupDto-objects as value to a dart map
+  static Map<String, List<GetAssetCountByTimeGroupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<GetAssetCountByTimeGroupDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = GetAssetCountByTimeGroupDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'timeGroup',
+  };
+}
+
diff --git a/mobile/openapi/lib/model/time_group_enum.dart b/mobile/openapi/lib/model/time_group_enum.dart
new file mode 100644
index 0000000000..b6b443a8b8
--- /dev/null
+++ b/mobile/openapi/lib/model/time_group_enum.dart
@@ -0,0 +1,85 @@
+//
+// 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 TimeGroupEnum {
+  /// Instantiate a new enum with the provided [value].
+  const TimeGroupEnum._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const day = TimeGroupEnum._(r'day');
+  static const month = TimeGroupEnum._(r'month');
+
+  /// List of all possible values in this [enum][TimeGroupEnum].
+  static const values = <TimeGroupEnum>[
+    day,
+    month,
+  ];
+
+  static TimeGroupEnum? fromJson(dynamic value) => TimeGroupEnumTypeTransformer().decode(value);
+
+  static List<TimeGroupEnum>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <TimeGroupEnum>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = TimeGroupEnum.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [TimeGroupEnum] to String,
+/// and [decode] dynamic data back to [TimeGroupEnum].
+class TimeGroupEnumTypeTransformer {
+  factory TimeGroupEnumTypeTransformer() => _instance ??= const TimeGroupEnumTypeTransformer._();
+
+  const TimeGroupEnumTypeTransformer._();
+
+  String encode(TimeGroupEnum data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a TimeGroupEnum.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  TimeGroupEnum? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data.toString()) {
+        case r'day': return TimeGroupEnum.day;
+        case r'month': return TimeGroupEnum.month;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [TimeGroupEnumTypeTransformer] instance.
+  static TimeGroupEnumTypeTransformer? _instance;
+}
+
diff --git a/mobile/openapi/test/asset_count_by_time_group_dto_test.dart b/mobile/openapi/test/asset_count_by_time_group_dto_test.dart
new file mode 100644
index 0000000000..3012dde35a
--- /dev/null
+++ b/mobile/openapi/test/asset_count_by_time_group_dto_test.dart
@@ -0,0 +1,32 @@
+//
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for AssetCountByTimeGroupDto
+void main() {
+  // final instance = AssetCountByTimeGroupDto();
+
+  group('test AssetCountByTimeGroupDto', () {
+    // String timeGroup
+    test('to test the property `timeGroup`', () async {
+      // TODO
+    });
+
+    // int count
+    test('to test the property `count`', () async {
+      // TODO
+    });
+
+
+  });
+
+}
diff --git a/mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart b/mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart
new file mode 100644
index 0000000000..1320fb4bbc
--- /dev/null
+++ b/mobile/openapi/test/asset_count_by_time_group_response_dto_test.dart
@@ -0,0 +1,32 @@
+//
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for AssetCountByTimeGroupResponseDto
+void main() {
+  // final instance = AssetCountByTimeGroupResponseDto();
+
+  group('test AssetCountByTimeGroupResponseDto', () {
+    // int totalAssets
+    test('to test the property `totalAssets`', () async {
+      // TODO
+    });
+
+    // List<AssetCountByTimeGroupDto> groups (default value: const [])
+    test('to test the property `groups`', () async {
+      // TODO
+    });
+
+
+  });
+
+}
diff --git a/mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart b/mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart
new file mode 100644
index 0000000000..993c7ce5c9
--- /dev/null
+++ b/mobile/openapi/test/get_asset_count_by_time_group_dto_test.dart
@@ -0,0 +1,27 @@
+//
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for GetAssetCountByTimeGroupDto
+void main() {
+  // final instance = GetAssetCountByTimeGroupDto();
+
+  group('test GetAssetCountByTimeGroupDto', () {
+    // String timeGroup (default value: 'month')
+    test('to test the property `timeGroup`', () async {
+      // TODO
+    });
+
+
+  });
+
+}
diff --git a/mobile/openapi/test/time_group_enum_test.dart b/mobile/openapi/test/time_group_enum_test.dart
new file mode 100644
index 0000000000..3c23d32333
--- /dev/null
+++ b/mobile/openapi/test/time_group_enum_test.dart
@@ -0,0 +1,21 @@
+//
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for TimeGroupEnum
+void main() {
+
+  group('test TimeGroupEnum', () {
+
+  });
+
+}
diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts
new file mode 100644
index 0000000000..9424750776
--- /dev/null
+++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts
@@ -0,0 +1,187 @@
+import { SearchPropertiesDto } from './dto/search-properties.dto';
+import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
+import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm/repository/Repository';
+import { CreateAssetDto } from './dto/create-asset.dto';
+import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
+import { AssetCountByTimeGroupDto } from './response-dto/asset-count-by-time-group-response.dto';
+import { TimeGroupEnum } from './dto/get-asset-count-by-time-group.dto';
+
+export interface IAssetRepository {
+  create(createAssetDto: CreateAssetDto, ownerId: string, originalPath: string, mimeType: string): Promise<AssetEntity>;
+  getAllByUserId(userId: string): Promise<AssetEntity[]>;
+  getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
+  getById(assetId: string): Promise<AssetEntity>;
+  getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
+  getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
+  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
+  getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum): Promise<AssetCountByTimeGroupDto[]>;
+}
+
+export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
+
+@Injectable()
+export class AssetRepository implements IAssetRepository {
+  constructor(
+    @InjectRepository(AssetEntity)
+    private assetRepository: Repository<AssetEntity>,
+  ) {}
+  async getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum) {
+    let result: AssetCountByTimeGroupDto[] = [];
+
+    if (timeGroup === TimeGroupEnum.Month) {
+      result = await this.assetRepository
+        .createQueryBuilder('asset')
+        .select(`COUNT(asset.id)::int`, 'count')
+        .addSelect(`to_char(date_trunc('month', "createdAt"::timestamptz), 'YYYY_MM')`, 'timeGroup')
+        .where('"userId" = :userId', { userId: userId })
+        .groupBy(`date_trunc('month', "createdAt"::timestamptz)`)
+        .orderBy(`date_trunc('month', "createdAt"::timestamptz)`, 'DESC')
+        .getRawMany();
+    } else if (timeGroup === TimeGroupEnum.Day) {
+      result = await this.assetRepository
+        .createQueryBuilder('asset')
+        .select(`COUNT(asset.id)::int`, 'count')
+        .addSelect(`to_char(date_trunc('day', "createdAt"::timestamptz), 'YYYY_MM_DD')`, 'timeGroup')
+        .where('"userId" = :userId', { userId: userId })
+        .groupBy(`date_trunc('day', "createdAt"::timestamptz)`)
+        .orderBy(`date_trunc('day', "createdAt"::timestamptz)`, 'DESC')
+        .getRawMany();
+    }
+
+    return result;
+  }
+
+  async getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
+    return await this.assetRepository
+      .createQueryBuilder('asset')
+      .where('asset.userId = :userId', { userId: userId })
+      .leftJoin('asset.exifInfo', 'ei')
+      .leftJoin('asset.smartInfo', 'si')
+      .select('si.tags', 'tags')
+      .addSelect('si.objects', 'objects')
+      .addSelect('asset.type', 'assetType')
+      .addSelect('ei.orientation', 'orientation')
+      .addSelect('ei."lensModel"', 'lensModel')
+      .addSelect('ei.make', 'make')
+      .addSelect('ei.model', 'model')
+      .addSelect('ei.city', 'city')
+      .addSelect('ei.state', 'state')
+      .addSelect('ei.country', 'country')
+      .distinctOn(['si.tags'])
+      .getRawMany();
+  }
+
+  async getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> {
+    return await this.assetRepository.query(
+      `
+        SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
+        FROM assets a
+        LEFT JOIN smart_info si ON a.id = si."assetId"
+        WHERE a."userId" = $1
+        AND si.objects IS NOT NULL
+      `,
+      [userId],
+    );
+  }
+
+  async getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]> {
+    return await this.assetRepository.query(
+      `
+        SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
+        FROM assets a
+        LEFT JOIN exif e ON a.id = e."assetId"
+        WHERE a."userId" = $1
+        AND e.city IS NOT NULL
+        AND a.type = 'IMAGE';
+      `,
+      [userId],
+    );
+  }
+
+  /**
+   * Get a single asset information by its ID
+   * - include exif info
+   * @param assetId
+   */
+  async getById(assetId: string): Promise<AssetEntity> {
+    return await this.assetRepository.findOneOrFail({
+      where: {
+        id: assetId,
+      },
+      relations: ['exifInfo'],
+    });
+  }
+
+  /**
+   * Get all assets belong to the user on the database
+   * @param userId
+   */
+  async getAllByUserId(userId: string): Promise<AssetEntity[]> {
+    const query = this.assetRepository
+      .createQueryBuilder('asset')
+      .where('asset.userId = :userId', { userId: userId })
+      .andWhere('asset.resizePath is not NULL')
+      .leftJoinAndSelect('asset.exifInfo', 'exifInfo')
+      .orderBy('asset.createdAt', 'DESC');
+
+    return await query.getMany();
+  }
+
+  /**
+   * Create new asset information in database
+   * @param createAssetDto
+   * @param ownerId
+   * @param originalPath
+   * @param mimeType
+   * @returns Promise<AssetEntity>
+   */
+  async create(
+    createAssetDto: CreateAssetDto,
+    ownerId: string,
+    originalPath: string,
+    mimeType: string,
+  ): Promise<AssetEntity> {
+    const asset = new AssetEntity();
+    asset.deviceAssetId = createAssetDto.deviceAssetId;
+    asset.userId = ownerId;
+    asset.deviceId = createAssetDto.deviceId;
+    asset.type = createAssetDto.assetType || AssetType.OTHER;
+    asset.originalPath = originalPath;
+    asset.createdAt = createAssetDto.createdAt;
+    asset.modifiedAt = createAssetDto.modifiedAt;
+    asset.isFavorite = createAssetDto.isFavorite;
+    asset.mimeType = mimeType;
+    asset.duration = createAssetDto.duration || null;
+
+    const createdAsset = await this.assetRepository.save(asset);
+
+    if (!createdAsset) {
+      throw new BadRequestException('Asset not created');
+    }
+    return createdAsset;
+  }
+
+  /**
+   * Get assets by device's Id on the database
+   * @param userId
+   * @param deviceId
+   *
+   * @returns Promise<string[]> - Array of assetIds belong to the device
+   */
+  async getAllByDeviceId(userId: string, deviceId: string): Promise<string[]> {
+    const rows = await this.assetRepository.find({
+      where: {
+        userId: userId,
+        deviceId: deviceId,
+      },
+      select: ['deviceAssetId'],
+    });
+    const res: string[] = [];
+    rows.forEach((v) => res.push(v.deviceAssetId));
+
+    return res;
+  }
+}
diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts
index 18447de8d1..7009f1e11a 100644
--- a/server/apps/immich/src/api-v1/asset/asset.controller.ts
+++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts
@@ -2,7 +2,6 @@ import {
   Controller,
   Post,
   UseInterceptors,
-  UploadedFiles,
   Body,
   UseGuards,
   Get,
@@ -44,6 +43,8 @@ import { CreateAssetDto } from './dto/create-asset.dto';
 import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
 import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
+import { AssetCountByTimeGroupResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
+import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto';
 
 @UseGuards(JwtAuthGuard)
 @ApiBearerAuth()
@@ -117,17 +118,17 @@ export class AssetController {
     return this.assetService.getAssetThumbnail(assetId, query);
   }
 
-  @Get('/allObjects')
+  @Get('/curated-objects')
   async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
     return this.assetService.getCuratedObject(authUser);
   }
 
-  @Get('/allLocation')
+  @Get('/curated-locations')
   async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
     return this.assetService.getCuratedLocation(authUser);
   }
 
-  @Get('/searchTerm')
+  @Get('/search-terms')
   async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> {
     return this.assetService.getAssetSearchTerm(authUser);
   }
@@ -140,6 +141,14 @@ export class AssetController {
     return this.assetService.searchAsset(authUser, searchAssetDto);
   }
 
+  @Get('/count-by-date')
+  async getAssetCountByTimeGroup(
+    @GetAuthUser() authUser: AuthUserDto,
+    @Body(ValidationPipe) getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto,
+  ): Promise<AssetCountByTimeGroupResponseDto> {
+    return this.assetService.getAssetCountByTimeGroup(authUser, getAssetCountByTimeGroupDto);
+  }
+
   /**
    * Get all AssetEntity belong to the user
    */
diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts
index a09831391f..13df9997f9 100644
--- a/server/apps/immich/src/api-v1/asset/asset.module.ts
+++ b/server/apps/immich/src/api-v1/asset/asset.module.ts
@@ -8,6 +8,7 @@ import { BackgroundTaskModule } from '../../modules/background-task/background-t
 import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { CommunicationModule } from '../communication/communication.module';
 import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
+import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
 
 @Module({
   imports: [
@@ -24,7 +25,14 @@ import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
     }),
   ],
   controllers: [AssetController],
-  providers: [AssetService, BackgroundTaskService],
+  providers: [
+    AssetService,
+    BackgroundTaskService,
+    {
+      provide: ASSET_REPOSITORY,
+      useClass: AssetRepository,
+    },
+  ],
   exports: [AssetService],
 })
 export class AssetModule {}
diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts
new file mode 100644
index 0000000000..c16050ea85
--- /dev/null
+++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts
@@ -0,0 +1,92 @@
+import { AssetRepository, IAssetRepository } from './asset-repository';
+import { AuthUserDto } from '../../decorators/auth-user.decorator';
+import { AssetService } from './asset.service';
+import { Repository } from 'typeorm';
+import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
+import { CreateAssetDto } from './dto/create-asset.dto';
+
+describe('AssetService', () => {
+  let sui: AssetService;
+  let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
+  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
+
+  const authUser: AuthUserDto = Object.freeze({
+    id: '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd',
+    email: 'auth@test.com',
+  });
+
+  const _getCreateAssetDto = (): CreateAssetDto => {
+    const createAssetDto = new CreateAssetDto();
+    createAssetDto.deviceAssetId = 'deviceAssetId';
+    createAssetDto.deviceId = 'deviceId';
+    createAssetDto.assetType = AssetType.OTHER;
+    createAssetDto.createdAt = '2022-06-19T23:41:36.910Z';
+    createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z';
+    createAssetDto.isFavorite = false;
+    createAssetDto.duration = '0:00:00.000000';
+
+    return createAssetDto;
+  };
+  const _getAsset = () => {
+    const assetEntity = new AssetEntity();
+
+    assetEntity.id = 'e8edabfd-7d8a-45d0-9d61-7c7ca60f2c67';
+    assetEntity.userId = '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd';
+    assetEntity.deviceAssetId = '4967046344801';
+    assetEntity.deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
+    assetEntity.type = AssetType.VIDEO;
+    assetEntity.originalPath =
+      'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
+    assetEntity.resizePath = '';
+    assetEntity.createdAt = '2022-06-19T23:41:36.910Z';
+    assetEntity.modifiedAt = '2022-06-19T23:41:36.910Z';
+    assetEntity.isFavorite = false;
+    assetEntity.mimeType = 'image/jpeg';
+    assetEntity.webpPath = '';
+    assetEntity.encodedVideoPath = '';
+    assetEntity.duration = '0:00:00.000000';
+
+    return assetEntity;
+  };
+
+  beforeAll(() => {
+    assetRepositoryMock = {
+      create: jest.fn(),
+      getAllByUserId: jest.fn(),
+      getAllByDeviceId: jest.fn(),
+      getAssetCountByTimeGroup: jest.fn(),
+      getById: jest.fn(),
+      getDetectedObjectsByUserId: jest.fn(),
+      getLocationsByUserId: jest.fn(),
+      getSearchPropertiesByUserId: jest.fn(),
+    };
+
+    sui = new AssetService(assetRepositoryMock, a);
+  });
+
+  it('create an asset', async () => {
+    const assetEntity = _getAsset();
+
+    assetRepositoryMock.create.mockImplementation(() => Promise.resolve<AssetEntity>(assetEntity));
+
+    const originalPath =
+      'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
+    const mimeType = 'image/jpeg';
+    const createAssetDto = _getCreateAssetDto();
+    const result = await sui.createUserAsset(authUser, createAssetDto, originalPath, mimeType);
+
+    expect(result.userId).toEqual(authUser.id);
+    expect(result.resizePath).toEqual('');
+    expect(result.webpPath).toEqual('');
+  });
+
+  it('get assets by device id', async () => {
+    assetRepositoryMock.getAllByDeviceId.mockImplementation(() => Promise.resolve<string[]>(['4967046344801']));
+
+    const deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
+    const result = await sui.getUserAssetsByDeviceId(authUser, deviceId);
+
+    expect(result.length).toEqual(1);
+    expect(result[0]).toEqual('4967046344801');
+  });
+});
diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts
index 9156950a5e..4d32212671 100644
--- a/server/apps/immich/src/api-v1/asset/asset.service.ts
+++ b/server/apps/immich/src/api-v1/asset/asset.service.ts
@@ -1,5 +1,7 @@
+import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import {
   BadRequestException,
+  Inject,
   Injectable,
   InternalServerErrorException,
   Logger,
@@ -7,7 +9,7 @@ import {
   StreamableFile,
 } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
-import { IsNull, Not, Repository } from 'typeorm';
+import { Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
 import { constants, createReadStream, ReadStream, stat } from 'fs';
@@ -25,83 +27,49 @@ import { CreateAssetDto } from './dto/create-asset.dto';
 import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
 import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
+import { ASSET_REPOSITORY, IAssetRepository } from './asset-repository';
+import { SearchPropertiesDto } from './dto/search-properties.dto';
+import {
+  AssetCountByTimeGroupResponseDto,
+  mapAssetCountByTimeGroupResponse,
+} from './response-dto/asset-count-by-time-group-response.dto';
+import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto';
 
 const fileInfo = promisify(stat);
 
 @Injectable()
 export class AssetService {
   constructor(
+    @Inject(ASSET_REPOSITORY)
+    private _assetRepository: IAssetRepository,
+
     @InjectRepository(AssetEntity)
     private assetRepository: Repository<AssetEntity>,
   ) {}
 
-  public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
-    const updatedAsset = await this.assetRepository
-      .createQueryBuilder('assets')
-      .update<AssetEntity>(AssetEntity, { ...asset, resizePath: thumbnailPath })
-      .where('assets.id = :id', { id: asset.id })
-      .returning('*')
-      .updateEntity(true)
-      .execute();
-
-    return updatedAsset.raw[0];
-  }
-
   public async createUserAsset(
     authUser: AuthUserDto,
-    assetInfo: CreateAssetDto,
-    path: string,
+    createAssetDto: CreateAssetDto,
+    originalPath: string,
     mimeType: string,
-  ): Promise<AssetEntity | undefined> {
-    const asset = new AssetEntity();
-    asset.deviceAssetId = assetInfo.deviceAssetId;
-    asset.userId = authUser.id;
-    asset.deviceId = assetInfo.deviceId;
-    asset.type = assetInfo.assetType || AssetType.OTHER;
-    asset.originalPath = path;
-    asset.createdAt = assetInfo.createdAt;
-    asset.modifiedAt = assetInfo.modifiedAt;
-    asset.isFavorite = assetInfo.isFavorite;
-    asset.mimeType = mimeType;
-    asset.duration = assetInfo.duration || null;
+  ): Promise<AssetEntity> {
+    const assetEntity = await this._assetRepository.create(createAssetDto, authUser.id, originalPath, mimeType);
 
-    const createdAsset = await this.assetRepository.save(asset);
-    if (!createdAsset) {
-      throw new Error('Asset not created');
-    }
-    return createdAsset;
+    return assetEntity;
   }
 
   public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
-    const rows = await this.assetRepository.find({
-      where: {
-        userId: authUser.id,
-        deviceId: deviceId,
-      },
-      select: ['deviceAssetId'],
-    });
-
-    const res: string[] = [];
-    rows.forEach((v) => res.push(v.deviceAssetId));
-    return res;
+    return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
   }
 
   public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> {
-    const assets = await this.assetRepository.find({
-      where: {
-        userId: authUser.id,
-        resizePath: Not(IsNull()),
-      },
-      relations: ['exifInfo'],
-      order: {
-        createdAt: 'DESC',
-      },
-    });
+    const assets = await this._assetRepository.getAllByUserId(authUser.id);
 
     return assets.map((asset) => mapAsset(asset));
   }
 
-  public async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
+  // TODO - Refactor this to get asset by its own id
+  private async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
     const rows = await this.assetRepository.query(
       'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2',
       [assetId, deviceId],
@@ -117,16 +85,7 @@ export class AssetService {
   }
 
   public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
-    const asset = await this.assetRepository.findOne({
-      where: {
-        id: assetId,
-      },
-      relations: ['exifInfo'],
-    });
-
-    if (!asset) {
-      throw new NotFoundException('Asset not found');
-    }
+    const asset = await this._assetRepository.getById(assetId);
 
     return mapAsset(asset);
   }
@@ -394,45 +353,35 @@ export class AssetService {
 
   async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
     const possibleSearchTerm = new Set<string>();
-    // TODO: should use query builder
-    const rows = await this.assetRepository.query(
-      `
-      SELECT DISTINCT si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
-      FROM assets a
-      LEFT JOIN exif e ON a.id = e."assetId"
-      LEFT JOIN smart_info si ON a.id = si."assetId"
-      WHERE a."userId" = $1;
-      `,
-      [authUser.id],
-    );
 
-    rows.forEach((row: { [x: string]: any }) => {
+    const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id);
+    rows.forEach((row: SearchPropertiesDto) => {
       // tags
-      row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
+      row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
 
       // objects
-      row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
+      row.objects?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
 
       // asset's tyoe
-      possibleSearchTerm.add(row['type']?.toLowerCase());
+      possibleSearchTerm.add(row.assetType?.toLowerCase() || '');
 
       // image orientation
-      possibleSearchTerm.add(row['orientation']?.toLowerCase());
+      possibleSearchTerm.add(row.orientation?.toLowerCase() || '');
 
       // Lens model
-      possibleSearchTerm.add(row['lensModel']?.toLowerCase());
+      possibleSearchTerm.add(row.lensModel?.toLowerCase() || '');
 
       // Make and model
-      possibleSearchTerm.add(row['make']?.toLowerCase());
-      possibleSearchTerm.add(row['model']?.toLowerCase());
+      possibleSearchTerm.add(row.make?.toLowerCase() || '');
+      possibleSearchTerm.add(row.model?.toLowerCase() || '');
 
       // Location
-      possibleSearchTerm.add(row['city']?.toLowerCase());
-      possibleSearchTerm.add(row['state']?.toLowerCase());
-      possibleSearchTerm.add(row['country']?.toLowerCase());
+      possibleSearchTerm.add(row.city?.toLowerCase() || '');
+      possibleSearchTerm.add(row.state?.toLowerCase() || '');
+      possibleSearchTerm.add(row.country?.toLowerCase() || '');
     });
 
-    return Array.from(possibleSearchTerm).filter((x) => x != null);
+    return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
   }
 
   async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise<AssetResponseDto[]> {
@@ -459,33 +408,12 @@ export class AssetService {
     return searchResults.map((asset) => mapAsset(asset));
   }
 
-  async getCuratedLocation(authUser: AuthUserDto) {
-    return await this.assetRepository.query(
-      `
-        SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
-        FROM assets a
-        LEFT JOIN exif e ON a.id = e."assetId"
-        WHERE a."userId" = $1
-        AND e.city IS NOT NULL
-        AND a.type = 'IMAGE';
-      `,
-      [authUser.id],
-    );
+  async getCuratedLocation(authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
+    return this._assetRepository.getLocationsByUserId(authUser.id);
   }
 
   async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
-    const curatedObjects: CuratedObjectsResponseDto[] = await this.assetRepository.query(
-      `
-        SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
-        FROM assets a
-        LEFT JOIN smart_info si ON a.id = si."assetId"
-        WHERE a."userId" = $1
-        AND si.objects IS NOT NULL
-      `,
-      [authUser.id],
-    );
-
-    return curatedObjects;
+    return this._assetRepository.getDetectedObjectsByUserId(authUser.id);
   }
 
   async checkDuplicatedAsset(
@@ -504,4 +432,16 @@ export class AssetService {
 
     return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id);
   }
+
+  async getAssetCountByTimeGroup(
+    authUser: AuthUserDto,
+    getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto,
+  ): Promise<AssetCountByTimeGroupResponseDto> {
+    const result = await this._assetRepository.getAssetCountByTimeGroup(
+      authUser.id,
+      getAssetCountByTimeGroupDto.timeGroup,
+    );
+
+    return mapAssetCountByTimeGroupResponse(result);
+  }
 }
diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts
new file mode 100644
index 0000000000..2862d3c945
--- /dev/null
+++ b/server/apps/immich/src/api-v1/asset/dto/get-asset-count-by-time-group.dto.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty } from 'class-validator';
+
+export enum TimeGroupEnum {
+  Day = 'day',
+  Month = 'month',
+}
+export class GetAssetCountByTimeGroupDto {
+  @IsNotEmpty()
+  @ApiProperty({
+    type: String,
+    enum: TimeGroupEnum,
+    enumName: 'TimeGroupEnum',
+  })
+  timeGroup!: TimeGroupEnum;
+}
diff --git a/server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts b/server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts
new file mode 100644
index 0000000000..669b29b2e3
--- /dev/null
+++ b/server/apps/immich/src/api-v1/asset/dto/search-properties.dto.ts
@@ -0,0 +1,12 @@
+export class SearchPropertiesDto {
+  tags?: string[];
+  objects?: string[];
+  assetType?: string;
+  orientation?: string;
+  lensModel?: string;
+  make?: string;
+  model?: string;
+  city?: string;
+  state?: string;
+  country?: string;
+}
diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts
new file mode 100644
index 0000000000..2d1d60974e
--- /dev/null
+++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts
@@ -0,0 +1,23 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class AssetCountByTimeGroupDto {
+  @ApiProperty({ type: 'string' })
+  timeGroup!: string;
+
+  @ApiProperty({ type: 'integer' })
+  count!: number;
+}
+
+export class AssetCountByTimeGroupResponseDto {
+  groups!: AssetCountByTimeGroupDto[];
+
+  @ApiProperty({ type: 'integer' })
+  totalAssets!: number;
+}
+
+export function mapAssetCountByTimeGroupResponse(result: AssetCountByTimeGroupDto[]): AssetCountByTimeGroupResponseDto {
+  return {
+    groups: result,
+    totalAssets: result.map((group) => group.count).reduce((a, b) => a + b, 0),
+  };
+}
diff --git a/server/apps/immich/src/api-v1/communication/communication.gateway.ts b/server/apps/immich/src/api-v1/communication/communication.gateway.ts
index ba36bbcf7d..8cb7928b9e 100644
--- a/server/apps/immich/src/api-v1/communication/communication.gateway.ts
+++ b/server/apps/immich/src/api-v1/communication/communication.gateway.ts
@@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { UserEntity } from '@app/database/entities/user.entity';
 import { Repository } from 'typeorm';
-
+import cookieParser from 'cookie';
 @WebSocketGateway({ cors: true })
 export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
   constructor(
@@ -26,8 +26,24 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
   async handleConnection(client: Socket) {
     try {
       Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent');
+      let accessToken = '';
 
-      const accessToken = client.handshake.headers.authorization?.split(' ')[1];
+      if (client.handshake.headers.cookie != undefined) {
+        const cookies = cookieParser.parse(client.handshake.headers.cookie);
+        if (cookies.immich_access_token) {
+          accessToken = cookies.immich_access_token;
+        } else {
+          client.emit('error', 'unauthorized');
+          client.disconnect();
+          return;
+        }
+      } else if (client.handshake.headers.authorization != undefined) {
+        accessToken = client.handshake.headers.authorization.split(' ')[1];
+      } else {
+        client.emit('error', 'unauthorized');
+        client.disconnect();
+        return;
+      }
 
       const res: JwtValidationResult = accessToken
         ? await this.immichJwtService.validateToken(accessToken)
diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index 9cc6f3f468..4d4bf4dad6 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -1 +1 @@
-{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/allObjects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/allLocation":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/searchTerm":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"string","nullable":true,"default":null},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"fileSizeInByte":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer"},"diskUseRaw":{"type":"integer"},"diskAvailableRaw":{"type":"integer"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}}}}}
\ No newline at end of file
+{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-date":{"get":{"operationId":"getAssetCountByTimeGroup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeGroupResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"string","nullable":true,"default":null},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"fileSizeInByte":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeGroupDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeGroupDto":{"type":"object","properties":{"timeGroup":{"type":"string"},"count":{"type":"integer"}},"required":["timeGroup","count"]},"AssetCountByTimeGroupResponseDto":{"type":"object","properties":{"totalAssets":{"type":"integer"},"groups":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeGroupDto"}}},"required":["totalAssets","groups"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer"},"diskUseRaw":{"type":"integer"},"diskAvailableRaw":{"type":"integer"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}}}}}
\ No newline at end of file
diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts
index 79ea7f4ad8..2ccc6010c4 100644
--- a/web/src/api/open-api/api.ts
+++ b/web/src/api/open-api/api.ts
@@ -145,6 +145,44 @@ export interface AlbumResponseDto {
      */
     'assets': Array<AssetResponseDto>;
 }
+/**
+ * 
+ * @export
+ * @interface AssetCountByTimeGroupDto
+ */
+export interface AssetCountByTimeGroupDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetCountByTimeGroupDto
+     */
+    'timeGroup': string;
+    /**
+     * 
+     * @type {number}
+     * @memberof AssetCountByTimeGroupDto
+     */
+    'count': number;
+}
+/**
+ * 
+ * @export
+ * @interface AssetCountByTimeGroupResponseDto
+ */
+export interface AssetCountByTimeGroupResponseDto {
+    /**
+     * 
+     * @type {number}
+     * @memberof AssetCountByTimeGroupResponseDto
+     */
+    'totalAssets': number;
+    /**
+     * 
+     * @type {Array<AssetCountByTimeGroupDto>}
+     * @memberof AssetCountByTimeGroupResponseDto
+     */
+    'groups': Array<AssetCountByTimeGroupDto>;
+}
 /**
  * 
  * @export
@@ -720,6 +758,19 @@ export interface ExifResponseDto {
      */
     'country'?: string | null;
 }
+/**
+ * 
+ * @export
+ * @interface GetAssetCountByTimeGroupDto
+ */
+export interface GetAssetCountByTimeGroupDto {
+    /**
+     * 
+     * @type {TimeGroupEnum}
+     * @memberof GetAssetCountByTimeGroupDto
+     */
+    'timeGroup': TimeGroupEnum;
+}
 /**
  * 
  * @export
@@ -996,6 +1047,20 @@ export const ThumbnailFormat = {
 export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat];
 
 
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const TimeGroupEnum = {
+    Day: 'day',
+    Month: 'month'
+} as const;
+
+export type TimeGroupEnum = typeof TimeGroupEnum[keyof typeof TimeGroupEnum];
+
+
 /**
  * 
  * @export
@@ -2072,13 +2137,52 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getAssetCountByTimeGroup: async (getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'getAssetCountByTimeGroupDto' is not null or undefined
+            assertParamExists('getAssetCountByTimeGroup', 'getAssetCountByTimeGroupDto', getAssetCountByTimeGroupDto)
+            const localVarPath = `/asset/count-by-date`;
+            // 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 bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(getAssetCountByTimeGroupDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
         getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/searchTerm`;
+            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;
@@ -2153,7 +2257,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * @throws {RequiredError}
          */
         getCuratedLocations: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/allLocation`;
+            const localVarPath = `/asset/curated-locations`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -2186,7 +2290,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * @throws {RequiredError}
          */
         getCuratedObjects: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/allObjects`;
+            const localVarPath = `/asset/curated-objects`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -2456,6 +2560,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(assetId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByTimeGroupResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -2598,6 +2712,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getAssetById(assetId: string, options?: any): AxiosPromise<AssetResponseDto> {
             return localVarFp.getAssetById(assetId, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: any): AxiosPromise<AssetCountByTimeGroupResponseDto> {
+            return localVarFp.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -2742,6 +2865,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getAssetById(assetId, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {*} [options] Override http request option.
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts
index 569ed5673d..db2bb6472b 100644
--- a/web/src/lib/stores/websocket.ts
+++ b/web/src/lib/stores/websocket.ts
@@ -1,5 +1,4 @@
 import { Socket, io } from 'socket.io-client';
-import { writable } from 'svelte/store';
 
 let websocket: Socket;
 
diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/+layout.server.ts
index ad51683594..d3ae614590 100644
--- a/web/src/routes/+layout.server.ts
+++ b/web/src/routes/+layout.server.ts
@@ -1,4 +1,4 @@
-import { serverApi } from '@api';
+import { serverApi, TimeGroupEnum } from '@api';
 import * as cookieParser from 'cookie';
 
 import type { LayoutServerLoad } from './$types';
@@ -21,6 +21,9 @@ export const load: LayoutServerLoad = async ({ request }) => {
 			user: userInfo
 		};
 	} catch (e) {
-		console.log('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
+		console.error('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
+		return {
+			user: undefined
+		};
 	}
 };
diff --git a/web/src/routes/photos/+page.svelte b/web/src/routes/photos/+page.svelte
index 38bf463e52..aca0447edc 100644
--- a/web/src/routes/photos/+page.svelte
+++ b/web/src/routes/photos/+page.svelte
@@ -20,11 +20,13 @@
 	import Close from 'svelte-material-icons/Close.svelte';
 	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 	import type { PageData } from './$types';
-	import { onMount } from 'svelte';
+
+	import { onMount, onDestroy } from 'svelte';
 	import {
 		notificationController,
 		NotificationType
 	} from '$lib/components/shared-components/notification/notification';
+	import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
 
 	export let data: PageData;
 
@@ -193,6 +195,18 @@
 			console.error('Error deleteSelectedAssetHandler', e);
 		}
 	};
+
+	onMount(async () => {
+		openWebsocketConnection();
+
+		const { data: assets } = await api.assetApi.getAllAssets();
+
+		setAssetInfo(assets);
+	});
+
+	onDestroy(() => {
+		closeWebsocketConnection();
+	});
 </script>
 
 <svelte:head>