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>