diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 3c662af41..1657aa69c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -162,7 +162,9 @@ Class | Method | HTTP request | Description *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | +*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **GET** /search/metadata | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | +*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **GET** /search/smart | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index a7ea1c07c..623d88b38 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -1034,7 +1034,7 @@ void (empty response body) [[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) # **searchAssets** -> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked) +> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) @@ -1093,13 +1093,14 @@ final type = ; // AssetTypeEnum | final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final webpPath = webpPath_example; // String | +final withArchived = true; // bool | final withDeleted = true; // bool | final withExif = true; // bool | final withPeople = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked); + final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->searchAssets: $e\n'); @@ -1146,6 +1147,7 @@ Name | Type | Description | Notes **updatedAfter** | **DateTime**| | [optional] **updatedBefore** | **DateTime**| | [optional] **webpPath** | **String**| | [optional] + **withArchived** | **bool**| | [optional] **withDeleted** | **bool**| | [optional] **withExif** | **bool**| | [optional] **withPeople** | **bool**| | [optional] diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index dcf453b55..40b44fb01 100644 --- a/mobile/openapi/doc/SearchApi.md +++ b/mobile/openapi/doc/SearchApi.md @@ -11,7 +11,9 @@ Method | HTTP request | Description ------------- | ------------- | ------------- [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | [**search**](SearchApi.md#search) | **GET** /search | +[**searchMetadata**](SearchApi.md#searchmetadata) | **GET** /search/metadata | [**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person | +[**searchSmart**](SearchApi.md#searchsmart) | **GET** /search/smart | # **getExploreData** @@ -66,7 +68,7 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **search** -> SearchResponseDto search(clip, motion, q, query, recent, smart, type, withArchived) +> SearchResponseDto search(clip, motion, page, q, query, recent, size, smart, type, withArchived) @@ -91,15 +93,17 @@ import 'package:openapi/api.dart'; final api_instance = SearchApi(); final clip = true; // bool | @deprecated final motion = true; // bool | +final page = 8.14; // num | final q = q_example; // String | final query = query_example; // String | final recent = true; // bool | +final size = 8.14; // num | final smart = true; // bool | final type = type_example; // String | final withArchived = true; // bool | try { - final result = api_instance.search(clip, motion, q, query, recent, smart, type, withArchived); + final result = api_instance.search(clip, motion, page, q, query, recent, size, smart, type, withArchived); print(result); } catch (e) { print('Exception when calling SearchApi->search: $e\n'); @@ -112,9 +116,11 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **clip** | **bool**| @deprecated | [optional] **motion** | **bool**| | [optional] + **page** | **num**| | [optional] **q** | **String**| | [optional] **query** | **String**| | [optional] **recent** | **bool**| | [optional] + **size** | **num**| | [optional] **smart** | **bool**| | [optional] **type** | **String**| | [optional] **withArchived** | **bool**| | [optional] @@ -134,6 +140,141 @@ 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) +# **searchMetadata** +> SearchResponseDto searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final checksum = checksum_example; // String | +final city = city_example; // String | +final country = country_example; // String | +final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final deviceAssetId = deviceAssetId_example; // String | +final deviceId = deviceId_example; // String | +final encodedVideoPath = encodedVideoPath_example; // String | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final isArchived = true; // bool | +final isEncoded = true; // bool | +final isExternal = true; // bool | +final isFavorite = true; // bool | +final isMotion = true; // bool | +final isOffline = true; // bool | +final isReadOnly = true; // bool | +final isVisible = true; // bool | +final lensModel = lensModel_example; // String | +final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final make = make_example; // String | +final model = model_example; // String | +final order = ; // AssetOrder | +final originalFileName = originalFileName_example; // String | +final originalPath = originalPath_example; // String | +final page = 8.14; // num | +final resizePath = resizePath_example; // String | +final size = 8.14; // num | +final state = state_example; // String | +final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final type = ; // AssetTypeEnum | +final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final webpPath = webpPath_example; // String | +final withArchived = true; // bool | +final withDeleted = true; // bool | +final withExif = true; // bool | +final withPeople = true; // bool | +final withStacked = true; // bool | + +try { + final result = api_instance.searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); + print(result); +} catch (e) { + print('Exception when calling SearchApi->searchMetadata: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **checksum** | **String**| | [optional] + **city** | **String**| | [optional] + **country** | **String**| | [optional] + **createdAfter** | **DateTime**| | [optional] + **createdBefore** | **DateTime**| | [optional] + **deviceAssetId** | **String**| | [optional] + **deviceId** | **String**| | [optional] + **encodedVideoPath** | **String**| | [optional] + **id** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isEncoded** | **bool**| | [optional] + **isExternal** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isMotion** | **bool**| | [optional] + **isOffline** | **bool**| | [optional] + **isReadOnly** | **bool**| | [optional] + **isVisible** | **bool**| | [optional] + **lensModel** | **String**| | [optional] + **libraryId** | **String**| | [optional] + **make** | **String**| | [optional] + **model** | **String**| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] + **originalFileName** | **String**| | [optional] + **originalPath** | **String**| | [optional] + **page** | **num**| | [optional] + **resizePath** | **String**| | [optional] + **size** | **num**| | [optional] + **state** | **String**| | [optional] + **takenAfter** | **DateTime**| | [optional] + **takenBefore** | **DateTime**| | [optional] + **trashedAfter** | **DateTime**| | [optional] + **trashedBefore** | **DateTime**| | [optional] + **type** | [**AssetTypeEnum**](.md)| | [optional] + **updatedAfter** | **DateTime**| | [optional] + **updatedBefore** | **DateTime**| | [optional] + **webpPath** | **String**| | [optional] + **withArchived** | **bool**| | [optional] + **withDeleted** | **bool**| | [optional] + **withExif** | **bool**| | [optional] + **withPeople** | **bool**| | [optional] + **withStacked** | **bool**| | [optional] + +### Return type + +[**SearchResponseDto**](SearchResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **searchPerson** > List searchPerson(name, withHidden) @@ -191,3 +332,118 @@ 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) +# **searchSmart** +> SearchResponseDto searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final query = query_example; // String | +final city = city_example; // String | +final country = country_example; // String | +final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final deviceId = deviceId_example; // String | +final isArchived = true; // bool | +final isEncoded = true; // bool | +final isExternal = true; // bool | +final isFavorite = true; // bool | +final isMotion = true; // bool | +final isOffline = true; // bool | +final isReadOnly = true; // bool | +final isVisible = true; // bool | +final lensModel = lensModel_example; // String | +final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final make = make_example; // String | +final model = model_example; // String | +final page = 8.14; // num | +final size = 8.14; // num | +final state = state_example; // String | +final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final type = ; // AssetTypeEnum | +final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final withArchived = true; // bool | +final withDeleted = true; // bool | +final withExif = true; // bool | + +try { + final result = api_instance.searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif); + print(result); +} catch (e) { + print('Exception when calling SearchApi->searchSmart: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **query** | **String**| | + **city** | **String**| | [optional] + **country** | **String**| | [optional] + **createdAfter** | **DateTime**| | [optional] + **createdBefore** | **DateTime**| | [optional] + **deviceId** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isEncoded** | **bool**| | [optional] + **isExternal** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isMotion** | **bool**| | [optional] + **isOffline** | **bool**| | [optional] + **isReadOnly** | **bool**| | [optional] + **isVisible** | **bool**| | [optional] + **lensModel** | **String**| | [optional] + **libraryId** | **String**| | [optional] + **make** | **String**| | [optional] + **model** | **String**| | [optional] + **page** | **num**| | [optional] + **size** | **num**| | [optional] + **state** | **String**| | [optional] + **takenAfter** | **DateTime**| | [optional] + **takenBefore** | **DateTime**| | [optional] + **trashedAfter** | **DateTime**| | [optional] + **trashedBefore** | **DateTime**| | [optional] + **type** | [**AssetTypeEnum**](.md)| | [optional] + **updatedAfter** | **DateTime**| | [optional] + **updatedBefore** | **DateTime**| | [optional] + **withArchived** | **bool**| | [optional] + **withDeleted** | **bool**| | [optional] + **withExif** | **bool**| | [optional] + +### Return type + +[**SearchResponseDto**](SearchResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/SearchAssetResponseDto.md b/mobile/openapi/doc/SearchAssetResponseDto.md index 2fc33feb4..5b55a559d 100644 --- a/mobile/openapi/doc/SearchAssetResponseDto.md +++ b/mobile/openapi/doc/SearchAssetResponseDto.md @@ -11,6 +11,7 @@ Name | Type | Description | Notes **count** | **int** | | **facets** | [**List**](SearchFacetResponseDto.md) | | [default to const []] **items** | [**List**](AssetResponseDto.md) | | [default to const []] +**nextPage** | **String** | | **total** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 7f4528c12..4dec34d40 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -1177,6 +1177,8 @@ class AssetApi { /// /// * [String] webpPath: /// + /// * [bool] withArchived: + /// /// * [bool] withDeleted: /// /// * [bool] withExif: @@ -1184,7 +1186,7 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/assets'; @@ -1303,6 +1305,9 @@ class AssetApi { if (webpPath != null) { queryParams.addAll(_queryParams('', 'webpPath', webpPath)); } + if (withArchived != null) { + queryParams.addAll(_queryParams('', 'withArchived', withArchived)); + } if (withDeleted != null) { queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); } @@ -1404,6 +1409,8 @@ class AssetApi { /// /// * [String] webpPath: /// + /// * [bool] withArchived: + /// /// * [bool] withDeleted: /// /// * [bool] withExif: @@ -1411,8 +1418,8 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { - final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); + Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index e2bde2a17..55d737a8d 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -68,18 +68,22 @@ class SearchApi { /// /// * [bool] motion: /// + /// * [num] page: + /// /// * [String] q: /// /// * [String] query: /// /// * [bool] recent: /// + /// * [num] size: + /// /// * [bool] smart: /// /// * [String] type: /// /// * [bool] withArchived: - Future searchWithHttpInfo({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async { + Future searchWithHttpInfo({ bool? clip, bool? motion, num? page, String? q, String? query, bool? recent, num? size, bool? smart, String? type, bool? withArchived, }) async { // ignore: prefer_const_declarations final path = r'/search'; @@ -96,6 +100,9 @@ class SearchApi { if (motion != null) { queryParams.addAll(_queryParams('', 'motion', motion)); } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } if (q != null) { queryParams.addAll(_queryParams('', 'q', q)); } @@ -105,6 +112,9 @@ class SearchApi { if (recent != null) { queryParams.addAll(_queryParams('', 'recent', recent)); } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } if (smart != null) { queryParams.addAll(_queryParams('', 'smart', smart)); } @@ -136,19 +146,354 @@ class SearchApi { /// /// * [bool] motion: /// + /// * [num] page: + /// /// * [String] q: /// /// * [String] query: /// /// * [bool] recent: /// + /// * [num] size: + /// /// * [bool] smart: /// /// * [String] type: /// /// * [bool] withArchived: - Future search({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async { - final response = await searchWithHttpInfo( clip: clip, motion: motion, q: q, query: query, recent: recent, smart: smart, type: type, withArchived: withArchived, ); + Future search({ bool? clip, bool? motion, num? page, String? q, String? query, bool? recent, num? size, bool? smart, String? type, bool? withArchived, }) async { + final response = await searchWithHttpInfo( clip: clip, motion: motion, page: page, q: q, query: query, recent: recent, size: size, smart: smart, type: type, withArchived: withArchived, ); + 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), 'SearchResponseDto',) as SearchResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /search/metadata' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] checksum: + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceAssetId: + /// + /// * [String] deviceId: + /// + /// * [String] encodedVideoPath: + /// + /// * [String] id: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [AssetOrder] order: + /// + /// * [String] originalFileName: + /// + /// * [String] originalPath: + /// + /// * [num] page: + /// + /// * [String] resizePath: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [String] webpPath: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + /// + /// * [bool] withPeople: + /// + /// * [bool] withStacked: + Future searchMetadataWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + // ignore: prefer_const_declarations + final path = r'/search/metadata'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (checksum != null) { + queryParams.addAll(_queryParams('', 'checksum', checksum)); + } + if (city != null) { + queryParams.addAll(_queryParams('', 'city', city)); + } + if (country != null) { + queryParams.addAll(_queryParams('', 'country', country)); + } + if (createdAfter != null) { + queryParams.addAll(_queryParams('', 'createdAfter', createdAfter)); + } + if (createdBefore != null) { + queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); + } + if (deviceAssetId != null) { + queryParams.addAll(_queryParams('', 'deviceAssetId', deviceAssetId)); + } + if (deviceId != null) { + queryParams.addAll(_queryParams('', 'deviceId', deviceId)); + } + if (encodedVideoPath != null) { + queryParams.addAll(_queryParams('', 'encodedVideoPath', encodedVideoPath)); + } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isEncoded != null) { + queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); + } + if (isExternal != null) { + queryParams.addAll(_queryParams('', 'isExternal', isExternal)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isMotion != null) { + queryParams.addAll(_queryParams('', 'isMotion', isMotion)); + } + if (isOffline != null) { + queryParams.addAll(_queryParams('', 'isOffline', isOffline)); + } + if (isReadOnly != null) { + queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly)); + } + if (isVisible != null) { + queryParams.addAll(_queryParams('', 'isVisible', isVisible)); + } + if (lensModel != null) { + queryParams.addAll(_queryParams('', 'lensModel', lensModel)); + } + if (libraryId != null) { + queryParams.addAll(_queryParams('', 'libraryId', libraryId)); + } + if (make != null) { + queryParams.addAll(_queryParams('', 'make', make)); + } + if (model != null) { + queryParams.addAll(_queryParams('', 'model', model)); + } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (originalFileName != null) { + queryParams.addAll(_queryParams('', 'originalFileName', originalFileName)); + } + if (originalPath != null) { + queryParams.addAll(_queryParams('', 'originalPath', originalPath)); + } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } + if (resizePath != null) { + queryParams.addAll(_queryParams('', 'resizePath', resizePath)); + } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } + if (state != null) { + queryParams.addAll(_queryParams('', 'state', state)); + } + if (takenAfter != null) { + queryParams.addAll(_queryParams('', 'takenAfter', takenAfter)); + } + if (takenBefore != null) { + queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); + } + if (trashedAfter != null) { + queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); + } + if (trashedBefore != null) { + queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore)); + } + if (type != null) { + queryParams.addAll(_queryParams('', 'type', type)); + } + if (updatedAfter != null) { + queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); + } + if (updatedBefore != null) { + queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore)); + } + if (webpPath != null) { + queryParams.addAll(_queryParams('', 'webpPath', webpPath)); + } + if (withArchived != null) { + queryParams.addAll(_queryParams('', 'withArchived', withArchived)); + } + if (withDeleted != null) { + queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); + } + if (withExif != null) { + queryParams.addAll(_queryParams('', 'withExif', withExif)); + } + if (withPeople != null) { + queryParams.addAll(_queryParams('', 'withPeople', withPeople)); + } + if (withStacked != null) { + queryParams.addAll(_queryParams('', 'withStacked', withStacked)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] checksum: + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceAssetId: + /// + /// * [String] deviceId: + /// + /// * [String] encodedVideoPath: + /// + /// * [String] id: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [AssetOrder] order: + /// + /// * [String] originalFileName: + /// + /// * [String] originalPath: + /// + /// * [num] page: + /// + /// * [String] resizePath: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [String] webpPath: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + /// + /// * [bool] withPeople: + /// + /// * [bool] withStacked: + Future searchMetadata({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchMetadataWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -220,4 +565,263 @@ class SearchApi { } return null; } + + /// Performs an HTTP 'GET /search/smart' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] query (required): + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [num] page: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + Future searchSmartWithHttpInfo(String query, { String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, num? page, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, bool? withArchived, bool? withDeleted, bool? withExif, }) async { + // ignore: prefer_const_declarations + final path = r'/search/smart'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (city != null) { + queryParams.addAll(_queryParams('', 'city', city)); + } + if (country != null) { + queryParams.addAll(_queryParams('', 'country', country)); + } + if (createdAfter != null) { + queryParams.addAll(_queryParams('', 'createdAfter', createdAfter)); + } + if (createdBefore != null) { + queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); + } + if (deviceId != null) { + queryParams.addAll(_queryParams('', 'deviceId', deviceId)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isEncoded != null) { + queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); + } + if (isExternal != null) { + queryParams.addAll(_queryParams('', 'isExternal', isExternal)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isMotion != null) { + queryParams.addAll(_queryParams('', 'isMotion', isMotion)); + } + if (isOffline != null) { + queryParams.addAll(_queryParams('', 'isOffline', isOffline)); + } + if (isReadOnly != null) { + queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly)); + } + if (isVisible != null) { + queryParams.addAll(_queryParams('', 'isVisible', isVisible)); + } + if (lensModel != null) { + queryParams.addAll(_queryParams('', 'lensModel', lensModel)); + } + if (libraryId != null) { + queryParams.addAll(_queryParams('', 'libraryId', libraryId)); + } + if (make != null) { + queryParams.addAll(_queryParams('', 'make', make)); + } + if (model != null) { + queryParams.addAll(_queryParams('', 'model', model)); + } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } + queryParams.addAll(_queryParams('', 'query', query)); + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } + if (state != null) { + queryParams.addAll(_queryParams('', 'state', state)); + } + if (takenAfter != null) { + queryParams.addAll(_queryParams('', 'takenAfter', takenAfter)); + } + if (takenBefore != null) { + queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); + } + if (trashedAfter != null) { + queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); + } + if (trashedBefore != null) { + queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore)); + } + if (type != null) { + queryParams.addAll(_queryParams('', 'type', type)); + } + if (updatedAfter != null) { + queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); + } + if (updatedBefore != null) { + queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore)); + } + if (withArchived != null) { + queryParams.addAll(_queryParams('', 'withArchived', withArchived)); + } + if (withDeleted != null) { + queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); + } + if (withExif != null) { + queryParams.addAll(_queryParams('', 'withExif', withExif)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] query (required): + /// + /// * [String] city: + /// + /// * [String] country: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] createdBefore: + /// + /// * [String] deviceId: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [String] lensModel: + /// + /// * [String] libraryId: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [num] page: + /// + /// * [num] size: + /// + /// * [String] state: + /// + /// * [DateTime] takenAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [AssetTypeEnum] type: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [bool] withArchived: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withExif: + Future searchSmart(String query, { String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, num? page, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, bool? withArchived, bool? withDeleted, bool? withExif, }) async { + final response = await searchSmartWithHttpInfo(query, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, page: page, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, ); + 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), 'SearchResponseDto',) as SearchResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index 98291307d..abdbc5d4e 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -16,6 +16,7 @@ class SearchAssetResponseDto { required this.count, this.facets = const [], this.items = const [], + required this.nextPage, required this.total, }); @@ -25,6 +26,8 @@ class SearchAssetResponseDto { List items; + String? nextPage; + int total; @override @@ -32,6 +35,7 @@ class SearchAssetResponseDto { other.count == count && _deepEquality.equals(other.facets, facets) && _deepEquality.equals(other.items, items) && + other.nextPage == nextPage && other.total == total; @override @@ -40,16 +44,22 @@ class SearchAssetResponseDto { (count.hashCode) + (facets.hashCode) + (items.hashCode) + + (nextPage == null ? 0 : nextPage!.hashCode) + (total.hashCode); @override - String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, total=$total]'; + String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, nextPage=$nextPage, total=$total]'; Map toJson() { final json = {}; json[r'count'] = this.count; json[r'facets'] = this.facets; json[r'items'] = this.items; + if (this.nextPage != null) { + json[r'nextPage'] = this.nextPage; + } else { + // json[r'nextPage'] = null; + } json[r'total'] = this.total; return json; } @@ -65,6 +75,7 @@ class SearchAssetResponseDto { count: mapValueOfType(json, r'count')!, facets: SearchFacetResponseDto.listFromJson(json[r'facets']), items: AssetResponseDto.listFromJson(json[r'items']), + nextPage: mapValueOfType(json, r'nextPage'), total: mapValueOfType(json, r'total')!, ); } @@ -116,6 +127,7 @@ class SearchAssetResponseDto { 'count', 'facets', 'items', + 'nextPage', 'total', }; } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index c34c85466..a7a63f38e 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -110,7 +110,7 @@ void main() { // TODO }); - //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async test('test searchAssets', () async { // TODO }); diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 769ad3194..be12c7e1f 100644 --- a/mobile/openapi/test/search_api_test.dart +++ b/mobile/openapi/test/search_api_test.dart @@ -22,15 +22,25 @@ void main() { // TODO }); - //Future search({ bool clip, bool motion, String q, String query, bool recent, bool smart, String type, bool withArchived }) async + //Future search({ bool clip, bool motion, num page, String q, String query, bool recent, num size, bool smart, String type, bool withArchived }) async test('test search', () async { // TODO }); + //Future searchMetadata({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + test('test searchMetadata', () async { + // TODO + }); + //Future> searchPerson(String name, { bool withHidden }) async test('test searchPerson', () async { // TODO }); + //Future searchSmart(String query, { String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceId, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, num page, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, bool withArchived, bool withDeleted, bool withExif }) async + test('test searchSmart', () async { + // TODO + }); + }); } diff --git a/mobile/openapi/test/search_asset_response_dto_test.dart b/mobile/openapi/test/search_asset_response_dto_test.dart index 87a7a61e9..56e827617 100644 --- a/mobile/openapi/test/search_asset_response_dto_test.dart +++ b/mobile/openapi/test/search_asset_response_dto_test.dart @@ -31,6 +31,11 @@ void main() { // TODO }); + // String nextPage + test('to test the property `nextPage`', () async { + // TODO + }); + // int total test('to test the property `total`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4c6317c2c..501218fc8 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2130,6 +2130,7 @@ }, "/assets": { "get": { + "deprecated": true, "operationId": "searchAssets", "parameters": [ { @@ -2430,6 +2431,14 @@ "type": "string" } }, + { + "name": "withArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "withDeleted", "required": false, @@ -4354,6 +4363,7 @@ }, "/search": { "get": { + "deprecated": true, "operationId": "search", "parameters": [ { @@ -4374,6 +4384,14 @@ "type": "boolean" } }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, { "name": "q", "required": false, @@ -4398,6 +4416,14 @@ "type": "boolean" } }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, { "name": "smart", "required": false, @@ -4492,6 +4518,377 @@ ] } }, + "/search/metadata": { + "get": { + "operationId": "searchMetadata", + "parameters": [ + { + "name": "checksum", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "city", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "createdAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "createdBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "deviceAssetId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "encodedVideoPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isEncoded", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isExternal", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMotion", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isOffline", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isReadOnly", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isVisible", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "lensModel", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "libraryId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + { + "name": "originalFileName", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "originalPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "resizePath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "takenAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "takenBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetTypeEnum" + } + }, + { + "name": "updatedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "updatedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "webpPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "withArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withDeleted", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withExif", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withPeople", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withStacked", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/search/person": { "get": { "operationId": "searchPerson", @@ -4544,6 +4941,296 @@ ] } }, + "/search/smart": { + "get": { + "operationId": "searchSmart", + "parameters": [ + { + "name": "city", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "createdAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "createdBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "deviceId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isEncoded", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isExternal", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMotion", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isOffline", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isReadOnly", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isVisible", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "lensModel", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "libraryId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "query", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "takenAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "takenBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetTypeEnum" + } + }, + { + "name": "updatedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "updatedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "withArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withDeleted", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withExif", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/server-info": { "get": { "operationId": "getServerInfo", @@ -8458,6 +9145,10 @@ }, "type": "array" }, + "nextPage": { + "nullable": true, + "type": "string" + }, "total": { "type": "integer" } @@ -8466,6 +9157,7 @@ "count", "facets", "items", + "nextPage", "total" ], "type": "object" diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index 542fa0580..c231aa617 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -2887,6 +2887,12 @@ export interface SearchAssetResponseDto { * @memberof SearchAssetResponseDto */ 'items': Array; + /** + * + * @type {string} + * @memberof SearchAssetResponseDto + */ + 'nextPage': string | null; /** * * @type {number} @@ -7760,14 +7766,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} [updatedAfter] * @param {string} [updatedBefore] * @param {string} [webpPath] + * @param {boolean} [withArchived] * @param {boolean} [withDeleted] * @param {boolean} [withExif] * @param {boolean} [withPeople] * @param {boolean} [withStacked] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - searchAssets: async (checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + searchAssets: async (checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/assets`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -7949,6 +7957,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['webpPath'] = webpPath; } + if (withArchived !== undefined) { + localVarQueryParameter['withArchived'] = withArchived; + } + if (withDeleted !== undefined) { localVarQueryParameter['withDeleted'] = withDeleted; } @@ -8585,15 +8597,17 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} [updatedAfter] * @param {string} [updatedBefore] * @param {string} [webpPath] + * @param {boolean} [withArchived] * @param {boolean} [withDeleted] * @param {boolean} [withExif] * @param {boolean} [withPeople] * @param {boolean} [withStacked] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async searchAssets(checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked, options); + async searchAssets(checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked, options); const index = configuration?.serverIndex ?? 0; const operationBasePath = operationServerMap['AssetApi.searchAssets']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); @@ -8841,10 +8855,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise> { - return localVarFp.searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(axios, basePath)); + return localVarFp.searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(axios, basePath)); }, /** * @@ -9593,6 +9608,13 @@ export interface AssetApiSearchAssetsRequest { */ readonly webpPath?: string + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withArchived?: boolean + /** * * @type {boolean} @@ -10020,11 +10042,12 @@ export class AssetApi extends BaseAPI { * * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AssetApi */ public searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: RawAxiosRequestConfig) { - return AssetApiFp(this.configuration).searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).searchAssets(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); } /** @@ -14500,16 +14523,19 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio * * @param {boolean} [clip] @deprecated * @param {boolean} [motion] + * @param {number} [page] * @param {string} [q] * @param {string} [query] * @param {boolean} [recent] + * @param {number} [size] * @param {boolean} [smart] * @param {SearchTypeEnum} [type] * @param {boolean} [withArchived] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - search: async (clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + search: async (clip?: boolean, motion?: boolean, page?: number, q?: string, query?: string, recent?: boolean, size?: number, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/search`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -14539,6 +14565,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['motion'] = motion; } + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + if (q !== undefined) { localVarQueryParameter['q'] = q; } @@ -14551,6 +14581,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['recent'] = recent; } + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + if (smart !== undefined) { localVarQueryParameter['smart'] = smart; } @@ -14565,6 +14599,265 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [checksum] + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [encodedVideoPath] + * @param {string} [id] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {AssetOrder} [order] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {number} [page] + * @param {string} [resizePath] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {string} [webpPath] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {boolean} [withStacked] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchMetadata: async (checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/search/metadata`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (checksum !== undefined) { + localVarQueryParameter['checksum'] = checksum; + } + + if (city !== undefined) { + localVarQueryParameter['city'] = city; + } + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (createdAfter !== undefined) { + localVarQueryParameter['createdAfter'] = (createdAfter as any instanceof Date) ? + (createdAfter as any).toISOString() : + createdAfter; + } + + if (createdBefore !== undefined) { + localVarQueryParameter['createdBefore'] = (createdBefore as any instanceof Date) ? + (createdBefore as any).toISOString() : + createdBefore; + } + + if (deviceAssetId !== undefined) { + localVarQueryParameter['deviceAssetId'] = deviceAssetId; + } + + if (deviceId !== undefined) { + localVarQueryParameter['deviceId'] = deviceId; + } + + if (encodedVideoPath !== undefined) { + localVarQueryParameter['encodedVideoPath'] = encodedVideoPath; + } + + if (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isEncoded !== undefined) { + localVarQueryParameter['isEncoded'] = isEncoded; + } + + if (isExternal !== undefined) { + localVarQueryParameter['isExternal'] = isExternal; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (isMotion !== undefined) { + localVarQueryParameter['isMotion'] = isMotion; + } + + if (isOffline !== undefined) { + localVarQueryParameter['isOffline'] = isOffline; + } + + if (isReadOnly !== undefined) { + localVarQueryParameter['isReadOnly'] = isReadOnly; + } + + if (isVisible !== undefined) { + localVarQueryParameter['isVisible'] = isVisible; + } + + if (lensModel !== undefined) { + localVarQueryParameter['lensModel'] = lensModel; + } + + if (libraryId !== undefined) { + localVarQueryParameter['libraryId'] = libraryId; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (order !== undefined) { + localVarQueryParameter['order'] = order; + } + + if (originalFileName !== undefined) { + localVarQueryParameter['originalFileName'] = originalFileName; + } + + if (originalPath !== undefined) { + localVarQueryParameter['originalPath'] = originalPath; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (resizePath !== undefined) { + localVarQueryParameter['resizePath'] = resizePath; + } + + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (takenAfter !== undefined) { + localVarQueryParameter['takenAfter'] = (takenAfter as any instanceof Date) ? + (takenAfter as any).toISOString() : + takenAfter; + } + + if (takenBefore !== undefined) { + localVarQueryParameter['takenBefore'] = (takenBefore as any instanceof Date) ? + (takenBefore as any).toISOString() : + takenBefore; + } + + if (trashedAfter !== undefined) { + localVarQueryParameter['trashedAfter'] = (trashedAfter as any instanceof Date) ? + (trashedAfter as any).toISOString() : + trashedAfter; + } + + if (trashedBefore !== undefined) { + localVarQueryParameter['trashedBefore'] = (trashedBefore as any instanceof Date) ? + (trashedBefore as any).toISOString() : + trashedBefore; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + + if (updatedBefore !== undefined) { + localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ? + (updatedBefore as any).toISOString() : + updatedBefore; + } + + if (webpPath !== undefined) { + localVarQueryParameter['webpPath'] = webpPath; + } + + if (withArchived !== undefined) { + localVarQueryParameter['withArchived'] = withArchived; + } + + if (withDeleted !== undefined) { + localVarQueryParameter['withDeleted'] = withDeleted; + } + + if (withExif !== undefined) { + localVarQueryParameter['withExif'] = withExif; + } + + if (withPeople !== undefined) { + localVarQueryParameter['withPeople'] = withPeople; + } + + if (withStacked !== undefined) { + localVarQueryParameter['withStacked'] = withStacked; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -14615,6 +14908,217 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} query + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceId] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {number} [page] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchSmart: async (query: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceId?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, page?: number, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'query' is not null or undefined + assertParamExists('searchSmart', 'query', query) + const localVarPath = `/search/smart`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (city !== undefined) { + localVarQueryParameter['city'] = city; + } + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (createdAfter !== undefined) { + localVarQueryParameter['createdAfter'] = (createdAfter as any instanceof Date) ? + (createdAfter as any).toISOString() : + createdAfter; + } + + if (createdBefore !== undefined) { + localVarQueryParameter['createdBefore'] = (createdBefore as any instanceof Date) ? + (createdBefore as any).toISOString() : + createdBefore; + } + + if (deviceId !== undefined) { + localVarQueryParameter['deviceId'] = deviceId; + } + + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isEncoded !== undefined) { + localVarQueryParameter['isEncoded'] = isEncoded; + } + + if (isExternal !== undefined) { + localVarQueryParameter['isExternal'] = isExternal; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (isMotion !== undefined) { + localVarQueryParameter['isMotion'] = isMotion; + } + + if (isOffline !== undefined) { + localVarQueryParameter['isOffline'] = isOffline; + } + + if (isReadOnly !== undefined) { + localVarQueryParameter['isReadOnly'] = isReadOnly; + } + + if (isVisible !== undefined) { + localVarQueryParameter['isVisible'] = isVisible; + } + + if (lensModel !== undefined) { + localVarQueryParameter['lensModel'] = lensModel; + } + + if (libraryId !== undefined) { + localVarQueryParameter['libraryId'] = libraryId; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (query !== undefined) { + localVarQueryParameter['query'] = query; + } + + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (takenAfter !== undefined) { + localVarQueryParameter['takenAfter'] = (takenAfter as any instanceof Date) ? + (takenAfter as any).toISOString() : + takenAfter; + } + + if (takenBefore !== undefined) { + localVarQueryParameter['takenBefore'] = (takenBefore as any instanceof Date) ? + (takenBefore as any).toISOString() : + takenBefore; + } + + if (trashedAfter !== undefined) { + localVarQueryParameter['trashedAfter'] = (trashedAfter as any instanceof Date) ? + (trashedAfter as any).toISOString() : + trashedAfter; + } + + if (trashedBefore !== undefined) { + localVarQueryParameter['trashedBefore'] = (trashedBefore as any instanceof Date) ? + (trashedBefore as any).toISOString() : + trashedBefore; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + + if (updatedBefore !== undefined) { + localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ? + (updatedBefore as any).toISOString() : + updatedBefore; + } + + if (withArchived !== undefined) { + localVarQueryParameter['withArchived'] = withArchived; + } + + if (withDeleted !== undefined) { + localVarQueryParameter['withDeleted'] = withDeleted; + } + + if (withExif !== undefined) { + localVarQueryParameter['withExif'] = withExif; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -14649,21 +15153,76 @@ export const SearchApiFp = function(configuration?: Configuration) { * * @param {boolean} [clip] @deprecated * @param {boolean} [motion] + * @param {number} [page] * @param {string} [q] * @param {string} [query] * @param {boolean} [recent] + * @param {number} [size] * @param {boolean} [smart] * @param {SearchTypeEnum} [type] * @param {boolean} [withArchived] * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async search(clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, q, query, recent, smart, type, withArchived, options); + async search(clip?: boolean, motion?: boolean, page?: number, q?: string, query?: string, recent?: boolean, size?: number, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, page, q, query, recent, size, smart, type, withArchived, options); const index = configuration?.serverIndex ?? 0; const operationBasePath = operationServerMap['SearchApi.search']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @param {string} [checksum] + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [encodedVideoPath] + * @param {string} [id] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {AssetOrder} [order] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {number} [page] + * @param {string} [resizePath] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {string} [webpPath] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {boolean} [withStacked] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchMetadata(checksum?: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceAssetId?: string, deviceId?: string, encodedVideoPath?: string, id?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, order?: AssetOrder, originalFileName?: string, originalPath?: string, page?: number, resizePath?: string, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, webpPath?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, withPeople?: boolean, withStacked?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['SearchApi.searchMetadata']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @param {string} name @@ -14677,6 +15236,48 @@ export const SearchApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['SearchApi.searchPerson']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @param {string} query + * @param {string} [city] + * @param {string} [country] + * @param {string} [createdAfter] + * @param {string} [createdBefore] + * @param {string} [deviceId] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {string} [lensModel] + * @param {string} [libraryId] + * @param {string} [make] + * @param {string} [model] + * @param {number} [page] + * @param {number} [size] + * @param {string} [state] + * @param {string} [takenAfter] + * @param {string} [takenBefore] + * @param {string} [trashedAfter] + * @param {string} [trashedBefore] + * @param {AssetTypeEnum} [type] + * @param {string} [updatedAfter] + * @param {string} [updatedBefore] + * @param {boolean} [withArchived] + * @param {boolean} [withDeleted] + * @param {boolean} [withExif] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchSmart(query: string, city?: string, country?: string, createdAfter?: string, createdBefore?: string, deviceId?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, lensModel?: string, libraryId?: string, make?: string, model?: string, page?: number, size?: number, state?: string, takenAfter?: string, takenBefore?: string, trashedAfter?: string, trashedBefore?: string, type?: AssetTypeEnum, updatedAfter?: string, updatedBefore?: string, withArchived?: boolean, withDeleted?: boolean, withExif?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['SearchApi.searchSmart']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, } }; @@ -14699,10 +15300,20 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat * * @param {SearchApiSearchRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(axios, basePath)); + return localVarFp.search(requestParameters.clip, requestParameters.motion, requestParameters.page, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.size, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {SearchApiSearchMetadataRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchMetadata(requestParameters: SearchApiSearchMetadataRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.searchMetadata(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(axios, basePath)); }, /** * @@ -14713,6 +15324,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SearchApiSearchSmartRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchSmart(requestParameters: SearchApiSearchSmartRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.searchSmart(requestParameters.query, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceId, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.page, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, options).then((request) => request(axios, basePath)); + }, }; }; @@ -14736,6 +15356,13 @@ export interface SearchApiSearchRequest { */ readonly motion?: boolean + /** + * + * @type {number} + * @memberof SearchApiSearch + */ + readonly page?: number + /** * * @type {string} @@ -14757,6 +15384,13 @@ export interface SearchApiSearchRequest { */ readonly recent?: boolean + /** + * + * @type {number} + * @memberof SearchApiSearch + */ + readonly size?: number + /** * * @type {boolean} @@ -14779,6 +15413,300 @@ export interface SearchApiSearchRequest { readonly withArchived?: boolean } +/** + * Request parameters for searchMetadata operation in SearchApi. + * @export + * @interface SearchApiSearchMetadataRequest + */ +export interface SearchApiSearchMetadataRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly checksum?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly city?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly createdAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly createdBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly deviceAssetId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly deviceId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly encodedVideoPath?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly id?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isEncoded?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isExternal?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isFavorite?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isMotion?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isOffline?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isReadOnly?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly isVisible?: boolean + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly lensModel?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly libraryId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly model?: string + + /** + * + * @type {AssetOrder} + * @memberof SearchApiSearchMetadata + */ + readonly order?: AssetOrder + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly originalFileName?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly originalPath?: string + + /** + * + * @type {number} + * @memberof SearchApiSearchMetadata + */ + readonly page?: number + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly resizePath?: string + + /** + * + * @type {number} + * @memberof SearchApiSearchMetadata + */ + readonly size?: number + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly state?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly takenAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly takenBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly trashedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly trashedBefore?: string + + /** + * + * @type {AssetTypeEnum} + * @memberof SearchApiSearchMetadata + */ + readonly type?: AssetTypeEnum + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly updatedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly updatedBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchMetadata + */ + readonly webpPath?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withDeleted?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withExif?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withPeople?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchMetadata + */ + readonly withStacked?: boolean +} + /** * Request parameters for searchPerson operation in SearchApi. * @export @@ -14800,6 +15728,230 @@ export interface SearchApiSearchPersonRequest { readonly withHidden?: boolean } +/** + * Request parameters for searchSmart operation in SearchApi. + * @export + * @interface SearchApiSearchSmartRequest + */ +export interface SearchApiSearchSmartRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly query: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly city?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly createdAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly createdBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly deviceId?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isEncoded?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isExternal?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isFavorite?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isMotion?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isOffline?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isReadOnly?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly isVisible?: boolean + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly lensModel?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly libraryId?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly model?: string + + /** + * + * @type {number} + * @memberof SearchApiSearchSmart + */ + readonly page?: number + + /** + * + * @type {number} + * @memberof SearchApiSearchSmart + */ + readonly size?: number + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly state?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly takenAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly takenBefore?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly trashedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly trashedBefore?: string + + /** + * + * @type {AssetTypeEnum} + * @memberof SearchApiSearchSmart + */ + readonly type?: AssetTypeEnum + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly updatedAfter?: string + + /** + * + * @type {string} + * @memberof SearchApiSearchSmart + */ + readonly updatedBefore?: string + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly withArchived?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly withDeleted?: boolean + + /** + * + * @type {boolean} + * @memberof SearchApiSearchSmart + */ + readonly withExif?: boolean +} + /** * SearchApi - object-oriented interface * @export @@ -14821,11 +15973,23 @@ export class SearchApi extends BaseAPI { * * @param {SearchApiSearchRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof SearchApi */ public search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig) { - return SearchApiFp(this.configuration).search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(this.axios, this.basePath)); + return SearchApiFp(this.configuration).search(requestParameters.clip, requestParameters.motion, requestParameters.page, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.size, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {SearchApiSearchMetadataRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchMetadata(requestParameters: SearchApiSearchMetadataRequest = {}, options?: RawAxiosRequestConfig) { + return SearchApiFp(this.configuration).searchMetadata(requestParameters.checksum, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.encodedVideoPath, requestParameters.id, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.order, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.page, requestParameters.resizePath, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.webpPath, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, requestParameters.withPeople, requestParameters.withStacked, options).then((request) => request(this.axios, this.basePath)); } /** @@ -14838,6 +16002,17 @@ export class SearchApi extends BaseAPI { public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig) { return SearchApiFp(this.configuration).searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {SearchApiSearchSmartRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchSmart(requestParameters: SearchApiSearchSmartRequest, options?: RawAxiosRequestConfig) { + return SearchApiFp(this.configuration).searchSmart(requestParameters.query, requestParameters.city, requestParameters.country, requestParameters.createdAfter, requestParameters.createdBefore, requestParameters.deviceId, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.lensModel, requestParameters.libraryId, requestParameters.make, requestParameters.model, requestParameters.page, requestParameters.size, requestParameters.state, requestParameters.takenAfter, requestParameters.takenBefore, requestParameters.trashedAfter, requestParameters.trashedBefore, requestParameters.type, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.withArchived, requestParameters.withDeleted, requestParameters.withExif, options).then((request) => request(this.axios, this.basePath)); + } } /** diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index cb2c42f5d..afdb705c6 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -588,6 +588,7 @@ export type SearchAssetResponseDto = { count: number; facets: SearchFacetResponseDto[]; items: AssetResponseDto[]; + nextPage: string | null; total: number; }; export type SearchResponseDto = { @@ -1461,7 +1462,7 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } -export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked }: { +export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { checksum?: string; city?: string; country?: string; @@ -1498,6 +1499,7 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef updatedAfter?: string; updatedBefore?: string; webpPath?: string; + withArchived?: boolean; withDeleted?: boolean; withExif?: boolean; withPeople?: boolean; @@ -1543,6 +1545,7 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef updatedAfter, updatedBefore, webpPath, + withArchived, withDeleted, withExif, withPeople, @@ -2047,12 +2050,14 @@ export function getPersonThumbnail({ id }: { ...opts })); } -export function search({ clip, motion, q, query, recent, smart, $type, withArchived }: { +export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: { clip?: boolean; motion?: boolean; + page?: number; q?: string; query?: string; recent?: boolean; + size?: number; smart?: boolean; $type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER"; withArchived?: boolean; @@ -2063,9 +2068,11 @@ export function search({ clip, motion, q, query, recent, smart, $type, withArchi }>(`/search${QS.query(QS.explode({ clip, motion, + page, q, query, recent, + size, smart, "type": $type, withArchived @@ -2081,6 +2088,98 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function searchMetadata({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { + checksum?: string; + city?: string; + country?: string; + createdAfter?: string; + createdBefore?: string; + deviceAssetId?: string; + deviceId?: string; + encodedVideoPath?: string; + id?: string; + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + lensModel?: string; + libraryId?: string; + make?: string; + model?: string; + order?: AssetOrder; + originalFileName?: string; + originalPath?: string; + page?: number; + resizePath?: string; + size?: number; + state?: string; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + $type?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + webpPath?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; + withPeople?: boolean; + withStacked?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchResponseDto; + }>(`/search/metadata${QS.query(QS.explode({ + checksum, + city, + country, + createdAfter, + createdBefore, + deviceAssetId, + deviceId, + encodedVideoPath, + id, + isArchived, + isEncoded, + isExternal, + isFavorite, + isMotion, + isOffline, + isReadOnly, + isVisible, + lensModel, + libraryId, + make, + model, + order, + originalFileName, + originalPath, + page, + resizePath, + size, + state, + takenAfter, + takenBefore, + trashedAfter, + trashedBefore, + "type": $type, + updatedAfter, + updatedBefore, + webpPath, + withArchived, + withDeleted, + withExif, + withPeople, + withStacked + }))}`, { + ...opts + })); +} export function searchPerson({ name, withHidden }: { name: string; withHidden?: boolean; @@ -2095,6 +2194,78 @@ export function searchPerson({ name, withHidden }: { ...opts })); } +export function searchSmart({ city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, query, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif }: { + city?: string; + country?: string; + createdAfter?: string; + createdBefore?: string; + deviceId?: string; + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + lensModel?: string; + libraryId?: string; + make?: string; + model?: string; + page?: number; + query: string; + size?: number; + state?: string; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + $type?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchResponseDto; + }>(`/search/smart${QS.query(QS.explode({ + city, + country, + createdAfter, + createdBefore, + deviceId, + isArchived, + isEncoded, + isExternal, + isFavorite, + isMotion, + isOffline, + isReadOnly, + isVisible, + lensModel, + libraryId, + make, + model, + page, + query, + size, + state, + takenAfter, + takenBefore, + trashedAfter, + trashedBefore, + "type": $type, + updatedAfter, + updatedBefore, + withArchived, + withDeleted, + withExif + }))}`, { + ...opts + })); +} export function getServerInfo(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 6f6bc8890..5993a7040 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -169,7 +169,11 @@ describe(`${AssetController.name} (e2e)`, () => { { should: 'should reject size as a string', query: { size: 'abc' }, - expected: ['size must not be less than 1', 'size must be an integer number'], + expected: [ + 'size must not be greater than 1000', + 'size must not be less than 1', + 'size must be an integer number', + ], }, { should: 'should reject an invalid size', @@ -478,7 +482,7 @@ describe(`${AssetController.name} (e2e)`, () => { }), }, { - should: 'sohuld search by make', + should: 'should search by make', deferred: () => ({ query: { make: 'Cannon' }, assets: [asset3], diff --git a/server/e2e/api/specs/search.e2e-spec.ts b/server/e2e/api/specs/search.e2e-spec.ts index 8d17ee930..74988396d 100644 --- a/server/e2e/api/specs/search.e2e-spec.ts +++ b/server/e2e/api/specs/search.e2e-spec.ts @@ -1,7 +1,7 @@ import { AssetResponseDto, IAssetRepository, - ISmartInfoRepository, + ISearchRepository, LibraryResponseDto, LoginResponseDto, mapAsset, @@ -20,14 +20,14 @@ describe(`${SearchController.name}`, () => { let accessToken: string; let libraries: LibraryResponseDto[]; let assetRepository: IAssetRepository; - let smartInfoRepository: ISmartInfoRepository; + let smartInfoRepository: ISearchRepository; let asset1: AssetResponseDto; beforeAll(async () => { app = await testApp.create(); server = app.getHttpServer(); assetRepository = app.get(IAssetRepository); - smartInfoRepository = app.get(ISmartInfoRepository); + smartInfoRepository = app.get(ISearchRepository); }); afterAll(async () => { diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index e73858c31..d46678b8b 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -31,8 +31,6 @@ import { AssetBulkUpdateDto, AssetJobName, AssetJobsDto, - AssetOrder, - AssetSearchDto, AssetStatsDto, MapMarkerDto, MemoryLaneDto, @@ -92,34 +90,6 @@ export class AssetService { this.configCore = SystemConfigCore.create(configRepository); } - search(auth: AuthDto, dto: AssetSearchDto) { - let checksum: Buffer | undefined; - - if (dto.checksum) { - const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; - checksum = Buffer.from(dto.checksum, encoding); - } - - const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; - const order = dto.order ? enumToOrder[dto.order] : undefined; - - return this.assetRepository - .search({ - ...dto, - order, - checksum, - ownerId: auth.user.id, - }) - .then((assets) => - assets.map((asset) => - mapAsset(asset, { - stripMetadata: false, - withStack: true, - }), - ), - ); - } - canUploadFile({ auth, fieldName, file }: UploadRequest): true { this.access.requireUploadAccess(auth); diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index bd4cf93ba..0244ecd90 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -1,20 +1,16 @@ -import { AssetType } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsBoolean, IsDateString, - IsEnum, IsInt, IsLatitude, IsLongitude, IsNotEmpty, IsPositive, IsString, - Min, ValidateIf, } from 'class-validator'; -import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util'; +import { Optional, ValidateUUID } from '../../domain.util'; import { BulkIdsDto } from '../response-dto'; export class DeviceIdDto { @@ -32,152 +28,6 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) => o.latitude !== undefined || o.longitude !== undefined; const ValidateGPS = () => ValidateIf(hasGPS); -export class AssetSearchDto { - @ValidateUUID({ optional: true }) - id?: string; - - @ValidateUUID({ optional: true }) - libraryId?: string; - - @IsString() - @Optional() - deviceAssetId?: string; - - @IsString() - @Optional() - deviceId?: string; - - @IsEnum(AssetType) - @Optional() - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) - type?: AssetType; - - @IsString() - @Optional() - checksum?: string; - - @QueryBoolean({ optional: true }) - isArchived?: boolean; - - @QueryBoolean({ optional: true }) - isEncoded?: boolean; - - @QueryBoolean({ optional: true }) - isExternal?: boolean; - - @QueryBoolean({ optional: true }) - isFavorite?: boolean; - - @QueryBoolean({ optional: true }) - isMotion?: boolean; - - @QueryBoolean({ optional: true }) - isOffline?: boolean; - - @QueryBoolean({ optional: true }) - isReadOnly?: boolean; - - @QueryBoolean({ optional: true }) - isVisible?: boolean; - - @QueryBoolean({ optional: true }) - withDeleted?: boolean; - - @QueryBoolean({ optional: true }) - withStacked?: boolean; - - @QueryBoolean({ optional: true }) - withExif?: boolean; - - @QueryBoolean({ optional: true }) - withPeople?: boolean; - - @QueryDate({ optional: true }) - createdBefore?: Date; - - @QueryDate({ optional: true }) - createdAfter?: Date; - - @QueryDate({ optional: true }) - updatedBefore?: Date; - - @QueryDate({ optional: true }) - updatedAfter?: Date; - - @QueryDate({ optional: true }) - trashedBefore?: Date; - - @QueryDate({ optional: true }) - trashedAfter?: Date; - - @QueryDate({ optional: true }) - takenBefore?: Date; - - @QueryDate({ optional: true }) - takenAfter?: Date; - - @IsString() - @Optional() - originalFileName?: string; - - @IsString() - @Optional() - originalPath?: string; - - @IsString() - @Optional() - resizePath?: string; - - @IsString() - @Optional() - webpPath?: string; - - @IsString() - @Optional() - encodedVideoPath?: string; - - @IsString() - @Optional() - city?: string; - - @IsString() - @Optional() - state?: string; - - @IsString() - @Optional() - country?: string; - - @IsString() - @Optional() - make?: string; - - @IsString() - @Optional() - model?: string; - - @IsString() - @Optional() - lensModel?: string; - - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) - order?: AssetOrder; - - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - size?: number; -} - export class AssetBulkUpdateDto extends BulkIdsDto { @Optional() @IsBoolean() diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index 43efbce25..5fdfd7ec5 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -137,6 +137,17 @@ export interface PaginationOptions { skip?: number; } +export enum PaginationMode { + LIMIT_OFFSET = 'limit-offset', + SKIP_TAKE = 'skip-take', +} + +export interface PaginatedBuilderOptions { + take: number; + skip?: number; + mode?: PaginationMode; +} + export interface PaginationResult { items: T[]; hasNextPage: boolean; diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 9d55abc8e..5da866601 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -13,7 +13,7 @@ import { newMediaRepositoryMock, newMoveRepositoryMock, newPersonRepositoryMock, - newSmartInfoRepositoryMock, + newSearchRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, personStub, @@ -31,7 +31,7 @@ import { IMediaRepository, IMoveRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, IStorageRepository, ISystemConfigRepository, WithoutProperty, @@ -76,7 +76,7 @@ describe(PersonService.name, () => { let moveMock: jest.Mocked; let personMock: jest.Mocked; let storageMock: jest.Mocked; - let smartInfoMock: jest.Mocked; + let searchMock: jest.Mocked; let cryptoMock: jest.Mocked; let sut: PersonService; @@ -90,7 +90,7 @@ describe(PersonService.name, () => { mediaMock = newMediaRepositoryMock(); personMock = newPersonRepositoryMock(); storageMock = newStorageRepositoryMock(); - smartInfoMock = newSmartInfoRepositoryMock(); + searchMock = newSearchRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); sut = new PersonService( accessMock, @@ -102,7 +102,7 @@ describe(PersonService.name, () => { configMock, storageMock, jobMock, - smartInfoMock, + searchMock, cryptoMock, ); @@ -752,7 +752,7 @@ describe(PersonService.name, () => { it('should create a face with no person and queue recognition job', async () => { personMock.createFaces.mockResolvedValue([faceStub.face1.id]); machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]); - smartInfoMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]); + searchMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]); assetMock.getByIds.mockResolvedValue([assetStub.image]); const face = { assetId: 'asset-id', @@ -823,7 +823,7 @@ describe(PersonService.name, () => { configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 }, ]); - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(faceStub.primaryFace1.person); @@ -850,7 +850,7 @@ describe(PersonService.name, () => { configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 }, ]); - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); @@ -869,14 +869,14 @@ describe(PersonService.name, () => { it('should not queue face with no matches', async () => { const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1); + expect(searchMock.searchFaces).toHaveBeenCalledTimes(1); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); }); @@ -890,7 +890,7 @@ describe(PersonService.name, () => { configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 }, ]); - smartInfoMock.searchFaces.mockResolvedValue(faces); + searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); @@ -900,7 +900,7 @@ describe(PersonService.name, () => { name: JobName.FACIAL_RECOGNITION, data: { id: faceStub.noPerson1.id, deferred: true }, }); - expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1); + expect(searchMock.searchFaces).toHaveBeenCalledTimes(1); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); }); @@ -914,14 +914,14 @@ describe(PersonService.name, () => { configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 }, ]); - smartInfoMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); + searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.create.mockResolvedValue(personStub.withName); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(2); + expect(searchMock.searchFaces).toHaveBeenCalledTimes(2); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 63fc35000..359084bf2 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -20,7 +20,7 @@ import { IMediaRepository, IMoveRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, IStorageRepository, ISystemConfigRepository, JobItem, @@ -61,7 +61,7 @@ export class PersonService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, + @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, ) { this.access = AccessCore.create(accessRepository); @@ -285,15 +285,7 @@ export class PersonService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { - order: 'DESC', - withFaces: true, - withPeople: false, - withSmartInfo: false, - withSmartSearch: false, - withExif: false, - withStacked: false, - }) + ? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true }) : this.assetRepository.getWithout(pagination, WithoutProperty.FACES); }); diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 00a74f829..4ff58cdb1 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -1,4 +1,4 @@ -import { SearchExploreItem } from '@app/domain'; +import { AssetSearchOptions, SearchExploreItem } from '@app/domain'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { Paginated, PaginationOptions } from '../domain.util'; @@ -11,64 +11,6 @@ export interface AssetStatsOptions { isTrashed?: boolean; } -export interface AssetSearchOptions { - id?: string; - libraryId?: string; - deviceAssetId?: string; - deviceId?: string; - ownerId?: string; - type?: AssetType; - checksum?: Buffer; - - isArchived?: boolean; - isEncoded?: boolean; - isExternal?: boolean; - isFavorite?: boolean; - isMotion?: boolean; - isOffline?: boolean; - isReadOnly?: boolean; - isVisible?: boolean; - - withDeleted?: boolean; - withStacked?: boolean; - withExif?: boolean; - withPeople?: boolean; - withSmartInfo?: boolean; - withSmartSearch?: boolean; - withFaces?: boolean; - - createdBefore?: Date; - createdAfter?: Date; - updatedBefore?: Date; - updatedAfter?: Date; - trashedBefore?: Date; - trashedAfter?: Date; - takenBefore?: Date; - takenAfter?: Date; - - originalFileName?: string; - originalPath?: string; - resizePath?: string; - webpPath?: string; - encodedVideoPath?: string; - - city?: string; - state?: string; - country?: string; - make?: string; - model?: string; - lensModel?: string; - - /** defaults to 'DESC' */ - order?: 'ASC' | 'DESC'; - - /** defaults to 1 */ - page?: number; - - /** defaults to 250 */ - size?: number; -} - export interface LivePhotoSearchOptions { ownerId: string; livePhotoCID: string; @@ -204,7 +146,6 @@ export interface IAssetRepository { getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise; upsertExif(exif: Partial): Promise; upsertJobStatus(jobStatus: Partial): Promise; - search(options: AssetSearchOptions): Promise; getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise>; getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise>; searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise; diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts index 48b1d7e8e..636abd2be 100644 --- a/server/src/domain/repositories/index.ts +++ b/server/src/domain/repositories/index.ts @@ -19,7 +19,6 @@ export * from './person.repository'; export * from './search.repository'; export * from './server-info.repository'; export * from './shared-link.repository'; -export * from './smart-info.repository'; export * from './storage.repository'; export * from './system-config.repository'; export * from './system-metadata.repository'; diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts index 5c3497c8e..4d720f98a 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -1,4 +1,7 @@ -import { AssetType } from '@app/infra/entities'; +import { AssetEntity, AssetFaceEntity, AssetType, SmartInfoEntity } from '@app/infra/entities'; +import { Paginated } from '../domain.util'; + +export const ISearchRepository = 'ISearchRepository'; export enum SearchStrategy { SMART = 'SMART', @@ -54,3 +57,122 @@ export interface SearchExploreItem { fieldName: string; items: SearchExploreItemSet; } + +export type Embedding = number[]; + +export interface SearchAssetIDOptions { + checksum?: Buffer; + deviceAssetId?: string; + id?: string; +} + +export interface SearchUserIDOptions { + deviceId?: string; + libraryId?: string; + ownerId?: string; +} + +export type SearchIDOptions = SearchAssetIDOptions & SearchUserIDOptions; + +export interface SearchStatusOptions { + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + type?: AssetType; + withArchived?: boolean; + withDeleted?: boolean; +} + +export interface SearchOneToOneRelationOptions { + withExif?: boolean; + withSmartInfo?: boolean; +} + +export interface SearchRelationOptions extends SearchOneToOneRelationOptions { + withFaces?: boolean; + withPeople?: boolean; + withStacked?: boolean; +} + +export interface SearchDateOptions { + createdBefore?: Date; + createdAfter?: Date; + takenBefore?: Date; + takenAfter?: Date; + trashedBefore?: Date; + trashedAfter?: Date; + updatedBefore?: Date; + updatedAfter?: Date; +} + +export interface SearchPathOptions { + encodedVideoPath?: string; + originalFileName?: string; + originalPath?: string; + resizePath?: string; + webpPath?: string; +} + +export interface SearchExifOptions { + city?: string; + country?: string; + lensModel?: string; + make?: string; + model?: string; + state?: string; +} + +export interface SearchEmbeddingOptions { + embedding: Embedding; + userIds: string[]; +} + +export interface SearchOrderOptions { + orderDirection?: 'ASC' | 'DESC'; +} + +export interface SearchPaginationOptions { + page: number; + size: number; +} + +export type AssetSearchOptions = SearchDateOptions & + SearchIDOptions & + SearchExifOptions & + SearchOrderOptions & + SearchPathOptions & + SearchRelationOptions & + SearchStatusOptions; + +export type AssetSearchBuilderOptions = Omit; + +export type SmartSearchOptions = SearchDateOptions & + SearchEmbeddingOptions & + SearchExifOptions & + SearchOneToOneRelationOptions & + SearchStatusOptions & + SearchUserIDOptions; + +export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { + hasPerson?: boolean; + numResults: number; + maxDistance?: number; +} + +export interface FaceSearchResult { + distance: number; + face: AssetFaceEntity; +} + +export interface ISearchRepository { + init(modelName: string): Promise; + searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated; + searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; + searchFaces(search: FaceEmbeddingSearch): Promise; + upsert(smartInfo: Partial, embedding?: Embedding): Promise; +} diff --git a/server/src/domain/repositories/smart-info.repository.ts b/server/src/domain/repositories/smart-info.repository.ts deleted file mode 100644 index acb907bc8..000000000 --- a/server/src/domain/repositories/smart-info.repository.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AssetEntity, AssetFaceEntity, SmartInfoEntity } from '@app/infra/entities'; - -export const ISmartInfoRepository = 'ISmartInfoRepository'; - -export type Embedding = number[]; - -export interface EmbeddingSearch { - userIds: string[]; - embedding: Embedding; - numResults: number; - withArchived?: boolean; -} - -export interface FaceEmbeddingSearch extends EmbeddingSearch { - maxDistance?: number; - hasPerson?: boolean; -} - -export interface FaceSearchResult { - face: AssetFaceEntity; - distance: number; -} - -export interface ISmartInfoRepository { - init(modelName: string): Promise; - searchCLIP(search: EmbeddingSearch): Promise; - searchFaces(search: FaceEmbeddingSearch): Promise; - upsert(smartInfo: Partial, embedding?: Embedding): Promise; -} diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 3ddcb3a32..a4e039668 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -1,8 +1,184 @@ +import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; import { AssetType } from '@app/infra/entities'; -import { Transform } from 'class-transformer'; -import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional, toBoolean } from '../../domain.util'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { Optional, QueryBoolean, QueryDate, ValidateUUID, toBoolean } from '../../domain.util'; +class BaseSearchDto { + @ValidateUUID({ optional: true }) + libraryId?: string; + + @IsString() + @IsNotEmpty() + @Optional() + deviceId?: string; + + @IsEnum(AssetType) + @Optional() + @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + type?: AssetType; + + @QueryBoolean({ optional: true }) + isArchived?: boolean; + + @QueryBoolean({ optional: true }) + withArchived?: boolean; + + @QueryBoolean({ optional: true }) + isEncoded?: boolean; + + @QueryBoolean({ optional: true }) + isExternal?: boolean; + + @QueryBoolean({ optional: true }) + isFavorite?: boolean; + + @QueryBoolean({ optional: true }) + isMotion?: boolean; + + @QueryBoolean({ optional: true }) + isOffline?: boolean; + + @QueryBoolean({ optional: true }) + isReadOnly?: boolean; + + @QueryBoolean({ optional: true }) + isVisible?: boolean; + + @QueryBoolean({ optional: true }) + withDeleted?: boolean; + + @QueryBoolean({ optional: true }) + withExif?: boolean; + + @QueryDate({ optional: true }) + createdBefore?: Date; + + @QueryDate({ optional: true }) + createdAfter?: Date; + + @QueryDate({ optional: true }) + updatedBefore?: Date; + + @QueryDate({ optional: true }) + updatedAfter?: Date; + + @QueryDate({ optional: true }) + trashedBefore?: Date; + + @QueryDate({ optional: true }) + trashedAfter?: Date; + + @QueryDate({ optional: true }) + takenBefore?: Date; + + @QueryDate({ optional: true }) + takenAfter?: Date; + + @IsString() + @IsNotEmpty() + @Optional() + city?: string; + + @IsString() + @IsNotEmpty() + @Optional() + state?: string; + + @IsString() + @IsNotEmpty() + @Optional() + country?: string; + + @IsString() + @IsNotEmpty() + @Optional() + make?: string; + + @IsString() + @IsNotEmpty() + @Optional() + model?: string; + + @IsString() + @IsNotEmpty() + @Optional() + lensModel?: string; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; + + @IsInt() + @Min(1) + @Max(1000) + @Type(() => Number) + @Optional() + size?: number; +} + +export class MetadataSearchDto extends BaseSearchDto { + @ValidateUUID({ optional: true }) + id?: string; + + @IsString() + @IsNotEmpty() + @Optional() + deviceAssetId?: string; + + @IsString() + @IsNotEmpty() + @Optional() + checksum?: string; + + @QueryBoolean({ optional: true }) + withStacked?: boolean; + + @QueryBoolean({ optional: true }) + withPeople?: boolean; + + @IsString() + @IsNotEmpty() + @Optional() + originalFileName?: string; + + @IsString() + @IsNotEmpty() + @Optional() + originalPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + resizePath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + webpPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + encodedVideoPath?: string; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + order?: AssetOrder; +} + +export class SmartSearchDto extends BaseSearchDto { + @IsString() + @IsNotEmpty() + query!: string; +} + +// TODO: remove after implementing new search filters +/** @deprecated */ export class SearchDto { @IsString() @IsNotEmpty() @@ -43,6 +219,19 @@ export class SearchDto { @Optional() @Transform(toBoolean) withArchived?: boolean; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; + + @IsInt() + @Min(1) + @Max(1000) + @Type(() => Number) + @Optional() + size?: number; } export class SearchPeopleDto { diff --git a/server/src/domain/search/response-dto/search-response.dto.ts b/server/src/domain/search/response-dto/search-response.dto.ts index 724cd5854..9dd65e7cc 100644 --- a/server/src/domain/search/response-dto/search-response.dto.ts +++ b/server/src/domain/search/response-dto/search-response.dto.ts @@ -29,6 +29,7 @@ class SearchAssetResponseDto { count!: number; items!: AssetResponseDto[]; facets!: SearchFacetResponseDto[]; + nextPage!: string | null; } export class SearchResponseDto { diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/domain/search/search.service.spec.ts index 86373ce2d..4f8d8c8fe 100644 --- a/server/src/domain/search/search.service.spec.ts +++ b/server/src/domain/search/search.service.spec.ts @@ -6,7 +6,7 @@ import { newMachineLearningRepositoryMock, newPartnerRepositoryMock, newPersonRepositoryMock, - newSmartInfoRepositoryMock, + newSearchRepositoryMock, newSystemConfigRepositoryMock, personStub, } from '@test'; @@ -16,7 +16,7 @@ import { IMachineLearningRepository, IPartnerRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, } from '../repositories'; import { SearchDto } from './dto'; @@ -30,7 +30,7 @@ describe(SearchService.name, () => { let configMock: jest.Mocked; let machineMock: jest.Mocked; let personMock: jest.Mocked; - let smartInfoMock: jest.Mocked; + let searchMock: jest.Mocked; let partnerMock: jest.Mocked; beforeEach(() => { @@ -38,9 +38,9 @@ describe(SearchService.name, () => { configMock = newSystemConfigRepositoryMock(); machineMock = newMachineLearningRepositoryMock(); personMock = newPersonRepositoryMock(); - smartInfoMock = newSmartInfoRepositoryMock(); + searchMock = newSearchRepositoryMock(); partnerMock = newPartnerRepositoryMock(); - sut = new SearchService(configMock, machineMock, personMock, smartInfoMock, assetMock, partnerMock); + sut = new SearchService(configMock, machineMock, personMock, searchMock, assetMock, partnerMock); }); it('should work', () => { @@ -104,6 +104,7 @@ describe(SearchService.name, () => { count: 1, items: [mapAsset(assetStub.image)], facets: [], + nextPage: null, }, }; @@ -111,13 +112,13 @@ describe(SearchService.name, () => { expect(result).toEqual(expectedResponse); expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 }); - expect(smartInfoMock.searchCLIP).not.toHaveBeenCalled(); + expect(searchMock.searchSmart).not.toHaveBeenCalled(); }); it('should search archived photos if `withArchived` option is true', async () => { const dto: SearchDto = { q: 'test query', clip: true, withArchived: true }; const embedding = [1, 2, 3]; - smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]); + searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false }); machineMock.encodeText.mockResolvedValueOnce(embedding); partnerMock.getAll.mockResolvedValueOnce([]); const expectedResponse = { @@ -132,25 +133,28 @@ describe(SearchService.name, () => { count: 1, items: [mapAsset(assetStub.image)], facets: [], + nextPage: null, }, }; const result = await sut.search(authStub.user1, dto); expect(result).toEqual(expectedResponse); - expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ - userIds: [authStub.user1.user.id], - embedding, - numResults: 100, - withArchived: true, - }); + expect(searchMock.searchSmart).toHaveBeenCalledWith( + { page: 1, size: 100 }, + { + userIds: [authStub.user1.user.id], + embedding, + withArchived: true, + }, + ); expect(assetMock.searchMetadata).not.toHaveBeenCalled(); }); it('should search by CLIP if `clip` option is true', async () => { const dto: SearchDto = { q: 'test query', clip: true }; const embedding = [1, 2, 3]; - smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]); + searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false }); machineMock.encodeText.mockResolvedValueOnce(embedding); partnerMock.getAll.mockResolvedValueOnce([]); const expectedResponse = { @@ -165,18 +169,21 @@ describe(SearchService.name, () => { count: 1, items: [mapAsset(assetStub.image)], facets: [], + nextPage: null, }, }; const result = await sut.search(authStub.user1, dto); expect(result).toEqual(expectedResponse); - expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ - userIds: [authStub.user1.user.id], - embedding, - numResults: 100, - withArchived: false, - }); + expect(searchMock.searchSmart).toHaveBeenCalledWith( + { page: 1, size: 100 }, + { + userIds: [authStub.user1.user.id], + embedding, + withArchived: false, + }, + ); expect(assetMock.searchMetadata).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 932a865d0..1438dc3be 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,7 +1,7 @@ import { AssetEntity } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { AssetResponseDto, mapAsset } from '../asset'; +import { AssetOrder, AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; import { PersonResponseDto } from '../person'; import { @@ -9,13 +9,13 @@ import { IMachineLearningRepository, IPartnerRepository, IPersonRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, SearchExploreItem, SearchStrategy, } from '../repositories'; import { FeatureFlag, SystemConfigCore } from '../system-config'; -import { SearchDto, SearchPeopleDto } from './dto'; +import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto'; import { SearchResponseDto } from './response-dto'; @Injectable() @@ -27,7 +27,7 @@ export class SearchService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, + @Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, ) { @@ -55,6 +55,53 @@ export class SearchService { })); } + async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { + let checksum: Buffer | undefined; + + if (dto.checksum) { + const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; + checksum = Buffer.from(dto.checksum, encoding); + } + + const page = dto.page ?? 1; + const size = dto.size || 250; + const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; + const { hasNextPage, items } = await this.searchRepository.searchMetadata( + { page, size }, + { + ...dto, + checksum, + ownerId: auth.user.id, + orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC', + }, + ); + + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); + } + + async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { + await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH); + const { machineLearning } = await this.configCore.getConfig(); + const userIds = await this.getUserIdsToSearch(auth); + + const embedding = await this.machineLearning.encodeText( + machineLearning.url, + { text: dto.query }, + machineLearning.clip, + ); + + const page = dto.page ?? 1; + const size = dto.size || 100; + const { hasNextPage, items } = await this.searchRepository.searchSmart( + { page, size }, + { ...dto, userIds, embedding }, + ); + + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); + } + + // TODO: remove after implementing new search filters + /** @deprecated */ async search(auth: AuthDto, dto: SearchDto): Promise { await this.configCore.requireFeature(FeatureFlag.SEARCH); const { machineLearning } = await this.configCore.getConfig(); @@ -70,10 +117,10 @@ export class SearchService { } const userIds = await this.getUserIdsToSearch(auth); - const withArchived = dto.withArchived || false; + const page = dto.page ?? 1; + let nextPage: string | null = null; let assets: AssetEntity[] = []; - switch (strategy) { case SearchStrategy.SMART: { const embedding = await this.machineLearning.encodeText( @@ -81,36 +128,30 @@ export class SearchService { { text: query }, machineLearning.clip, ); - assets = await this.smartInfoRepository.searchCLIP({ - userIds: userIds, - embedding, - numResults: 100, - withArchived, - }); + + const { hasNextPage, items } = await this.searchRepository.searchSmart( + { page, size: dto.size || 100 }, + { + userIds, + embedding, + withArchived: !!dto.withArchived, + }, + ); + if (hasNextPage) { + nextPage = (page + 1).toString(); + } + assets = items; break; } case SearchStrategy.TEXT: { - assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 }); + assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 }); } default: { break; } } - return { - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: assets.length, - count: assets.length, - items: assets.map((asset) => mapAsset(asset)), - facets: [], - }, - }; + return this.mapResponse(assets, nextPage); } private async getUserIdsToSearch(auth: AuthDto): Promise { @@ -122,4 +163,17 @@ export class SearchService { userIds.push(...partnersIds); return userIds; } + + private async mapResponse(assets: AssetEntity[], nextPage: string | null): Promise { + return { + albums: { total: 0, count: 0, items: [], facets: [] }, + assets: { + total: assets.length, + count: assets.length, + items: assets.map((asset) => mapAsset(asset)), + facets: [], + nextPage, + }, + }; + } } diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/domain/smart-info/smart-info.service.spec.ts index 373f8da91..5da7b7824 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/domain/smart-info/smart-info.service.spec.ts @@ -5,7 +5,7 @@ import { newDatabaseRepositoryMock, newJobRepositoryMock, newMachineLearningRepositoryMock, - newSmartInfoRepositoryMock, + newSearchRepositoryMock, newSystemConfigRepositoryMock, } from '@test'; import { JobName } from '../job'; @@ -14,7 +14,7 @@ import { IDatabaseRepository, IJobRepository, IMachineLearningRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, WithoutProperty, } from '../repositories'; @@ -31,18 +31,18 @@ describe(SmartInfoService.name, () => { let assetMock: jest.Mocked; let configMock: jest.Mocked; let jobMock: jest.Mocked; - let smartMock: jest.Mocked; + let searchMock: jest.Mocked; let machineMock: jest.Mocked; let databaseMock: jest.Mocked; beforeEach(async () => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); - smartMock = newSmartInfoRepositoryMock(); + searchMock = newSearchRepositoryMock(); jobMock = newJobRepositoryMock(); machineMock = newMachineLearningRepositoryMock(); databaseMock = newDatabaseRepositoryMock(); - sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, smartMock, configMock); + sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock); assetMock.getByIds.mockResolvedValue([asset]); }); @@ -102,12 +102,12 @@ describe(SmartInfoService.name, () => { await sut.handleEncodeClip({ id: asset.id }); - expect(smartMock.upsert).not.toHaveBeenCalled(); + expect(searchMock.upsert).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { - smartMock.upsert.mockResolvedValue(); + searchMock.upsert.mockResolvedValue(); machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); await sut.handleEncodeClip({ id: asset.id }); @@ -117,7 +117,7 @@ describe(SmartInfoService.name, () => { { imagePath: 'path/to/resize.ext' }, { enabled: true, modelName: 'ViT-B-32__openai' }, ); - expect(smartMock.upsert).toHaveBeenCalledWith( + expect(searchMock.upsert).toHaveBeenCalledWith( { assetId: 'asset-1', }, diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts index 55e4b7080..d193b29b5 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/domain/smart-info/smart-info.service.ts @@ -8,7 +8,7 @@ import { IDatabaseRepository, IJobRepository, IMachineLearningRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, WithoutProperty, } from '../repositories'; @@ -24,7 +24,7 @@ export class SmartInfoService { @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, - @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, + @Inject(ISearchRepository) private repository: ISearchRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, ) { this.configCore = SystemConfigCore.create(configRepository); diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index e8fa3f62e..8addc63a0 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException } from '@nestjs/common'; import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test'; import { QueueName } from '../job'; -import { ICommunicationRepository, ISmartInfoRepository, ISystemConfigRepository, ServerEvent } from '../repositories'; +import { ICommunicationRepository, ISearchRepository, ISystemConfigRepository, ServerEvent } from '../repositories'; import { defaults, SystemConfigValidator } from './system-config.core'; import { SystemConfigService } from './system-config.service'; @@ -146,7 +146,7 @@ describe(SystemConfigService.name, () => { let sut: SystemConfigService; let configMock: jest.Mocked; let communicationMock: jest.Mocked; - let smartInfoMock: jest.Mocked; + let smartInfoMock: jest.Mocked; beforeEach(async () => { delete process.env.IMMICH_CONFIG_FILE; diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 5bf597e35..39a3ea1df 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { ClientEvent, ICommunicationRepository, - ISmartInfoRepository, + ISearchRepository, ISystemConfigRepository, ServerEvent, } from '../repositories'; @@ -32,7 +32,7 @@ export class SystemConfigService { constructor( @Inject(ISystemConfigRepository) private repository: ISystemConfigRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, - @Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, + @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, ) { this.core = SystemConfigCore.create(repository); this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate()); diff --git a/server/src/domain/trash/trash.service.ts b/server/src/domain/trash/trash.service.ts index b1a38f72c..30fd6843e 100644 --- a/server/src/domain/trash/trash.service.ts +++ b/server/src/domain/trash/trash.service.ts @@ -33,7 +33,9 @@ export class TrashService { async restore(auth: AuthDto): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), + this.assetRepository.getByUserId(pagination, auth.user.id, { + trashedBefore: DateTime.now().toJSDate(), + }), ); for await (const assets of assetPagination) { @@ -44,7 +46,9 @@ export class TrashService { async empty(auth: AuthDto): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), + this.assetRepository.getByUserId(pagination, auth.user.id, { + trashedBefore: DateTime.now().toJSDate(), + }), ); for await (const assets of assetPagination) { diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index ad29ff080..57a67d33f 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -3,7 +3,6 @@ import { AssetBulkUpdateDto, AssetJobsDto, AssetResponseDto, - AssetSearchDto, AssetService, AssetStatsDto, AssetStatsResponseDto, @@ -14,7 +13,9 @@ import { MapMarkerResponseDto, MemoryLaneDto, MemoryLaneResponseDto, + MetadataSearchDto, RandomAssetsDto, + SearchService, TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto, @@ -23,7 +24,7 @@ import { UpdateStackParentDto, } from '@app/domain'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; import { Route } from '../interceptors'; @@ -34,11 +35,15 @@ import { UUIDParamDto } from './dto/uuid-param.dto'; @Authenticated() @UseValidation() export class AssetsController { - constructor(private service: AssetService) {} + constructor(private searchService: SearchService) {} @Get() - searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { - return this.service.search(auth, dto); + @ApiOperation({ deprecated: true }) + async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise { + const { + assets: { items }, + } = await this.searchService.searchMetadata(auth, dto); + return items; } } diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index 51ee900ee..1cc204e84 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -1,14 +1,16 @@ import { AuthDto, + MetadataSearchDto, PersonResponseDto, SearchDto, SearchExploreResponseDto, SearchPeopleDto, SearchResponseDto, SearchService, + SmartSearchDto, } from '@app/domain'; import { Controller, Get, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -19,7 +21,18 @@ import { UseValidation } from '../app.utils'; export class SearchController { constructor(private service: SearchService) {} + @Get('metadata') + searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise { + return this.service.searchMetadata(auth, dto); + } + + @Get('smart') + searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise { + return this.service.searchSmart(auth, dto); + } + @Get() + @ApiOperation({ deprecated: true }) search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise { return this.service.search(auth, dto); } diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index 93cb8fb68..b36fdf6f0 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -17,9 +17,9 @@ import { IMoveRepository, IPartnerRepository, IPersonRepository, + ISearchRepository, IServerInfoRepository, ISharedLinkRepository, - ISmartInfoRepository, IStorageRepository, ISystemConfigRepository, ISystemMetadataRepository, @@ -56,9 +56,9 @@ import { MoveRepository, PartnerRepository, PersonRepository, + SearchRepository, ServerInfoRepository, SharedLinkRepository, - SmartInfoRepository, SystemConfigRepository, SystemMetadataRepository, TagRepository, @@ -86,7 +86,7 @@ const providers: Provider[] = [ { provide: IPersonRepository, useClass: PersonRepository }, { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, - { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, + { provide: ISearchRepository, useClass: SearchRepository }, { provide: IStorageRepository, useClass: FilesystemProvider }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index 1036df2af..89bd31966 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -1,7 +1,19 @@ -import { Paginated, PaginationOptions } from '@app/domain'; +import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/domain'; import _ from 'lodash'; -import { Between, FindManyOptions, LessThanOrEqual, MoreThanOrEqual, ObjectLiteral, Repository } from 'typeorm'; -import { chunks, setUnion } from '../domain/domain.util'; +import { + Between, + Brackets, + FindManyOptions, + IsNull, + LessThanOrEqual, + MoreThanOrEqual, + Not, + ObjectLiteral, + Repository, + SelectQueryBuilder, +} from 'typeorm'; +import { PaginatedBuilderOptions, PaginationMode, PaginationResult, chunks, setUnion } from '../domain/domain.util'; +import { AssetEntity } from './entities'; import { DATABASE_PARAMETER_CHUNK_SIZE } from './infra.util'; /** @@ -18,9 +30,21 @@ export function OptionalBetween(from?: T, to?: T) { } } +export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { + const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; + return Number.isInteger(value) && value >= min && value <= max; +}; + +function paginationHelper(items: Entity[], take: number): PaginationResult { + const hasNextPage = items.length > take; + items.splice(take); + + return { items, hasNextPage }; +} + export async function paginate( repository: Repository, - paginationOptions: PaginationOptions, + { take, skip }: PaginationOptions, searchOptions?: FindManyOptions, ): Paginated { const items = await repository.find( @@ -28,27 +52,33 @@ export async function paginate( { ...searchOptions, // Take one more item to check if there's a next page - take: paginationOptions.take + 1, - skip: paginationOptions.skip, + take: take + 1, + skip, }, _.isUndefined, ), ); - const hasNextPage = items.length > paginationOptions.take; - items.splice(paginationOptions.take); + return paginationHelper(items, take); +} - return { items, hasNextPage }; +export async function paginatedBuilder( + qb: SelectQueryBuilder, + { take, skip, mode }: PaginatedBuilderOptions, +): Paginated { + if (mode === PaginationMode.LIMIT_OFFSET) { + qb.limit(take + 1).offset(skip); + } else { + qb.take(take + 1).skip(skip); + } + + const items = await qb.getMany(); + return paginationHelper(items, take); } export const asVector = (embedding: number[], quote = false) => quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`; -export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { - const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; - return Number.isInteger(value) && value >= min && value <= max; -}; - /** * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection, * to overcome the maximum number of parameters allowed by the database driver. @@ -91,3 +121,79 @@ export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator { return Chunked({ ...options, mergeFn: setUnion }); } + +export function searchAssetBuilder( + builder: SelectQueryBuilder, + options: AssetSearchBuilderOptions, +): SelectQueryBuilder { + builder.andWhere( + _.omitBy( + { + createdAt: OptionalBetween(options.createdAfter, options.createdBefore), + updatedAt: OptionalBetween(options.updatedAfter, options.updatedBefore), + deletedAt: OptionalBetween(options.trashedAfter, options.trashedBefore), + fileCreatedAt: OptionalBetween(options.takenAfter, options.takenBefore), + }, + _.isUndefined, + ), + ); + + const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined); + if (Object.keys(exifInfo).length > 0) { + builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo'); + builder.andWhere({ exifInfo }); + } + + const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId', 'ownerId']); + builder.andWhere(_.omitBy(id, _.isUndefined)); + + const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']); + builder.andWhere(_.omitBy(path, _.isUndefined)); + + const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']); + const { isArchived, isEncoded, isMotion, withArchived } = options; + builder.andWhere( + _.omitBy( + { + ...status, + isArchived: isArchived ?? withArchived, + encodedVideoPath: isEncoded ? Not(IsNull()) : undefined, + livePhotoVideoId: isMotion ? Not(IsNull()) : undefined, + }, + _.isUndefined, + ), + ); + + if (options.withExif) { + builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo'); + } + + if (options.withFaces || options.withPeople) { + builder.leftJoinAndSelect(`${builder.alias}.faces`, 'faces'); + } + + if (options.withPeople) { + builder.leftJoinAndSelect(`${builder.alias}.person`, 'person'); + } + + if (options.withSmartInfo) { + builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo'); + } + + if (options.withStacked) { + builder + .leftJoinAndSelect(`${builder.alias}.stack`, 'stack') + .leftJoinAndSelect('stack.assets', 'stackedAssets') + .andWhere( + new Brackets((qb) => qb.where(`stack.primaryAssetId = ${builder.alias}.id`).orWhere('asset.stackId IS NULL')), + ); + } + + const withDeleted = + options.withDeleted ?? (options.trashedAfter !== undefined || options.trashedBefore !== undefined); + if (withDeleted) { + builder.withDeleted(); + } + + return builder; +} diff --git a/server/src/infra/repositories/api-key.repository.ts b/server/src/infra/repositories/api-key.repository.ts index 71226a537..b7ebc303d 100644 --- a/server/src/infra/repositories/api-key.repository.ts +++ b/server/src/infra/repositories/api-key.repository.ts @@ -42,7 +42,7 @@ export class ApiKeyRepository implements IKeyRepository { return this.repository.findOne({ where: { userId, id } }); } - @GenerateSql({ params: [DummyValue.STRING] }) + @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string): Promise { return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } }); } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 95a227b69..215d280f4 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -12,6 +12,7 @@ import { MetadataSearchOptions, MonthDay, Paginated, + PaginationMode, PaginationOptions, SearchExploreItem, TimeBucketItem, @@ -22,26 +23,21 @@ import { } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import _ from 'lodash'; import { DateTime } from 'luxon'; import path from 'node:path'; import { - And, Brackets, FindOptionsRelations, FindOptionsSelect, FindOptionsWhere, In, IsNull, - LessThan, Not, Repository, } from 'typeorm'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; import { DummyValue, GenerateSql } from '../infra.util'; -import { Chunked, ChunkedArray, OptionalBetween, paginate } from '../infra.utils'; - -const DEFAULT_SEARCH_SIZE = 250; +import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; const truncateMap: Record = { [TimeBucketSize.DAY]: 'day', @@ -70,142 +66,6 @@ export class AssetRepository implements IAssetRepository { await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] }); } - search(options: AssetSearchOptions): Promise { - const { - id, - libraryId, - deviceAssetId, - type, - checksum, - ownerId, - - isVisible, - isFavorite, - isExternal, - isReadOnly, - isOffline, - isArchived, - isMotion, - isEncoded, - - createdBefore, - createdAfter, - updatedBefore, - updatedAfter, - trashedBefore, - trashedAfter, - takenBefore, - takenAfter, - - originalFileName, - originalPath, - resizePath, - webpPath, - encodedVideoPath, - - city, - state, - country, - make, - model, - lensModel, - - withDeleted: _withDeleted, - withExif: _withExif, - withStacked, - withPeople, - withSmartInfo, - - order, - } = options; - - const withDeleted = _withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); - - const page = Math.max(options.page || 1, 1); - const size = Math.min(options.size || DEFAULT_SEARCH_SIZE, DEFAULT_SEARCH_SIZE); - - const exifWhere = _.omitBy( - { - city, - state, - country, - make, - model, - lensModel, - }, - _.isUndefined, - ); - - const withExif = Object.keys(exifWhere).length > 0 || _withExif; - - const where: FindOptionsWhere = _.omitBy( - { - ownerId, - id, - libraryId, - deviceAssetId, - type, - checksum, - isVisible, - isFavorite, - isExternal, - isReadOnly, - isOffline, - isArchived, - livePhotoVideoId: isMotion && Not(IsNull()), - originalFileName, - originalPath, - resizePath, - webpPath, - encodedVideoPath: encodedVideoPath ?? (isEncoded && Not(IsNull())), - createdAt: OptionalBetween(createdAfter, createdBefore), - updatedAt: OptionalBetween(updatedAfter, updatedBefore), - deletedAt: OptionalBetween(trashedAfter, trashedBefore), - fileCreatedAt: OptionalBetween(takenAfter, takenBefore), - exifInfo: Object.keys(exifWhere).length > 0 ? exifWhere : undefined, - }, - _.isUndefined, - ); - - const builder = this.repository.createQueryBuilder('asset'); - - if (withExif) { - if (_withExif) { - builder.leftJoinAndSelect('asset.exifInfo', 'exifInfo'); - } else { - builder.leftJoin('asset.exifInfo', 'exifInfo'); - } - } - - if (withPeople) { - builder.leftJoinAndSelect('asset.faces', 'faces'); - builder.leftJoinAndSelect('faces.person', 'person'); - } - - if (withSmartInfo) { - builder.leftJoinAndSelect('asset.smartInfo', 'smartInfo'); - } - - if (withDeleted) { - builder.withDeleted(); - } - - builder.where(where); - - if (withStacked) { - builder - .leftJoinAndSelect('asset.stack', 'stack') - .leftJoinAndSelect('stack.assets', 'stackedAssets') - .andWhere(new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL'))); - } - - return builder - .skip(size * (page - 1)) - .take(size) - .orderBy('asset.fileCreatedAt', order ?? 'DESC') - .getMany(); - } - create(asset: AssetCreate): Promise { return this.repository.save(asset); } @@ -316,17 +176,7 @@ export class AssetRepository implements IAssetRepository { } getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated { - return paginate(this.repository, pagination, { - where: { - ownerId: userId, - isVisible: options.isVisible, - deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined, - }, - relations: { - exifInfo: true, - }, - withDeleted: !!options.trashedBefore, - }); + return this.getAll(pagination, { ...options, id: userId }); } @GenerateSql({ params: [[DummyValue.UUID]] }) @@ -345,24 +195,13 @@ export class AssetRepository implements IAssetRepository { } getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { - return paginate(this.repository, pagination, { - where: { - isVisible: options.isVisible, - type: options.type, - deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined, - }, - relations: { - exifInfo: options.withExif !== false, - smartInfo: options.withSmartInfo !== false, - tags: options.withSmartInfo !== false, - faces: options.withFaces !== false, - smartSearch: options.withSmartInfo === true, - }, - withDeleted: options.withDeleted ?? !!options.trashedBefore, - order: { - // Ensures correct order when paginating - createdAt: options.order ?? 'ASC', - }, + let builder = this.repository.createQueryBuilder('asset'); + builder = searchAssetBuilder(builder, options); + builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); + return paginatedBuilder(builder, { + mode: PaginationMode.SKIP_TAKE, + skip: pagination.skip, + take: pagination.take, }); } @@ -435,7 +274,7 @@ export class AssetRepository implements IAssetRepository { await this.repository.remove(asset); } - @GenerateSql({ params: [[DummyValue.UUID], DummyValue.BUFFER] }) + @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) getByChecksum(userId: string, checksum: Buffer): Promise { return this.repository.findOne({ where: { ownerId: userId, checksum } }); } diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 21703ec8c..d684f6b00 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -17,9 +17,9 @@ export * from './metadata.repository'; export * from './move.repository'; export * from './partner.repository'; export * from './person.repository'; +export * from './search.repository'; export * from './server-info.repository'; export * from './shared-link.repository'; -export * from './smart-info.repository'; export * from './system-config.repository'; export * from './system-metadata.repository'; export * from './tag.repository'; diff --git a/server/src/infra/repositories/smart-info.repository.ts b/server/src/infra/repositories/search.repository.ts similarity index 68% rename from server/src/infra/repositories/smart-info.repository.ts rename to server/src/infra/repositories/search.repository.ts index f74fd4232..7d0421b05 100644 --- a/server/src/infra/repositories/smart-info.repository.ts +++ b/server/src/infra/repositories/search.repository.ts @@ -1,10 +1,15 @@ import { + AssetSearchOptions, DatabaseExtension, Embedding, - EmbeddingSearch, FaceEmbeddingSearch, FaceSearchResult, - ISmartInfoRepository, + ISearchRepository, + Paginated, + PaginationMode, + PaginationResult, + SearchPaginationOptions, + SmartSearchOptions, } from '@app/domain'; import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities'; @@ -14,11 +19,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { vectorExt } from '../database.config'; import { DummyValue, GenerateSql } from '../infra.util'; -import { asVector, isValidInteger } from '../infra.utils'; +import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; @Injectable() -export class SmartInfoRepository implements ISmartInfoRepository { - private logger = new ImmichLogger(SmartInfoRepository.name); +export class SearchRepository implements ISearchRepository { + private logger = new ImmichLogger(SearchRepository.name); private faceColumns: string[]; constructor( @@ -35,48 +40,74 @@ export class SmartInfoRepository implements ISmartInfoRepository { async init(modelName: string): Promise { const { dimSize } = getCLIPModelInfo(modelName); - if (dimSize == null) { - throw new Error(`Invalid CLIP model name: ${modelName}`); - } + const curDimSize = await this.getDimSize(); + this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`); - const currentDimSize = await this.getDimSize(); - this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`); - - if (dimSize != currentDimSize) { - this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`); + if (dimSize != curDimSize) { + this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`); await this.updateDimSize(dimSize); } } @GenerateSql({ - params: [{ userIds: [DummyValue.UUID], embedding: Array.from({ length: 512 }, Math.random), numResults: 100 }], + params: [ + { page: 1, size: 100 }, + { + takenAfter: DummyValue.DATE, + lensModel: DummyValue.STRING, + ownerId: DummyValue.UUID, + withStacked: true, + isFavorite: true, + }, + ], }) - async searchCLIP({ userIds, embedding, numResults, withArchived }: EmbeddingSearch): Promise { - if (!isValidInteger(numResults, { min: 1 })) { - throw new Error(`Invalid value for 'numResults': ${numResults}`); - } + async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated { + let builder = this.assetRepository.createQueryBuilder('asset'); + builder = searchAssetBuilder(builder, options); - // setting this too low messes with prefilter recall - numResults = Math.max(numResults, 64); + builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC'); + + return paginatedBuilder(builder, { + mode: PaginationMode.SKIP_TAKE, + skip: (pagination.page - 1) * pagination.size, + take: pagination.size, + }); + } + + @GenerateSql({ + params: [ + { page: 1, size: 100 }, + { + takenAfter: DummyValue.DATE, + embedding: Array.from({ length: 512 }, Math.random), + lensModel: DummyValue.STRING, + withStacked: true, + isFavorite: true, + userIds: [DummyValue.UUID], + }, + ], + }) + async searchSmart( + pagination: SearchPaginationOptions, + { embedding, userIds, ...options }: SmartSearchOptions, + ): Paginated { + let results: PaginationResult = { items: [], hasNextPage: false }; - let results: AssetEntity[] = []; await this.assetRepository.manager.transaction(async (manager) => { - const query = manager - .createQueryBuilder(AssetEntity, 'a') - .innerJoin('a.smartSearch', 's') - .leftJoinAndSelect('a.exifInfo', 'e') - .where('a.ownerId IN (:...userIds )') - .orderBy('s.embedding <=> :embedding') + let builder = manager.createQueryBuilder(AssetEntity, 'asset'); + builder = searchAssetBuilder(builder, options); + builder + .innerJoin('asset.smartSearch', 'search') + .andWhere('asset.ownerId IN (:...userIds )') + .orderBy('search.embedding <=> :embedding') .setParameters({ userIds, embedding: asVector(embedding) }); - if (!withArchived) { - query.andWhere('a.isArchived = false'); - } - query.andWhere('a.isVisible = true').andWhere('a.fileCreatedAt < NOW()'); - query.limit(numResults); - - await manager.query(this.getRuntimeConfig(numResults)); - results = await query.getMany(); + await manager.query(this.getRuntimeConfig(pagination.size)); + results = await paginatedBuilder(builder, { + mode: PaginationMode.LIMIT_OFFSET, + skip: (pagination.page - 1) * pagination.size, + take: pagination.size, + }); }); return results; @@ -135,7 +166,6 @@ export class SmartInfoRepository implements ISmartInfoRepository { .where('res.distance <= :maxDistance', { maxDistance }) .getRawMany(); }); - return results.map((row) => ({ face: this.assetFaceRepository.create(row), distance: row.distance, @@ -163,17 +193,14 @@ export class SmartInfoRepository implements ISmartInfoRepository { throw new Error(`Invalid CLIP dimension size: ${dimSize}`); } - const currentDimSize = await this.getDimSize(); - if (currentDimSize === dimSize) { + const curDimSize = await this.getDimSize(); + if (curDimSize === dimSize) { return; } this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); await this.smartSearchRepository.manager.transaction(async (manager) => { - if (vectorExt === DatabaseExtension.VECTORS) { - await manager.query(`SET vectors.pgvector_compatibility=on`); - } await manager.query(`DROP TABLE smart_search`); await manager.query(` @@ -182,12 +209,15 @@ export class SmartInfoRepository implements ISmartInfoRepository { embedding vector(${dimSize}) NOT NULL )`); await manager.query(` - CREATE INDEX IF NOT EXISTS clip_index ON smart_search - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); + CREATE INDEX clip_index ON smart_search + USING vectors (embedding vector_cos_ops) WITH (options = $$ + [indexing.hnsw] + m = 16 + ef_construction = 300 + $$)`); }); - this.logger.log(`Successfully updated database CLIP dimension size from ${currentDimSize} to ${dimSize}.`); + this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); } private async getDimSize(): Promise { diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts index 348762d95..0b10c018c 100644 --- a/server/src/infra/sql-generator/index.ts +++ b/server/src/infra/sql-generator/index.ts @@ -19,8 +19,8 @@ import { MoveRepository, PartnerRepository, PersonRepository, + SearchRepository, SharedLinkRepository, - SmartInfoRepository, SystemConfigRepository, SystemMetadataRepository, TagRepository, @@ -41,7 +41,7 @@ const repositories = [ PartnerRepository, PersonRepository, SharedLinkRepository, - SmartInfoRepository, + SearchRepository, SystemConfigRepository, SystemMetadataRepository, TagRepository, @@ -142,7 +142,7 @@ class SqlGenerator { this.sqlLogger.clear(); // errors still generate sql, which is all we care about - await target.apply(instance, params).catch(() => null); + await target.apply(instance, params).catch((error: Error) => console.error(`${queryLabel} error: ${error}`)); if (this.sqlLogger.queries.length === 0) { console.warn(`No queries recorded for ${queryLabel}`); diff --git a/server/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql new file mode 100644 index 000000000..538a85409 --- /dev/null +++ b/server/src/infra/sql/search.repository.sql @@ -0,0 +1,234 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- SearchRepository.searchMetadata +SELECT DISTINCT + "distinctAlias"."asset_id" AS "ids_asset_id", + "distinctAlias"."asset_fileCreatedAt" +FROM + ( + SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."resizePath" AS "asset_resizePath", + "asset"."webpPath" AS "asset_webpPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isReadOnly" AS "asset_isReadOnly", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "stack"."id" AS "stack_id", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."resizePath" AS "stackedAssets_resizePath", + "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId" + FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) + WHERE + ( + "asset"."fileCreatedAt" >= $1 + AND "exifInfo"."lensModel" = $2 + AND "asset"."ownerId" = $3 + AND 1 = 1 + AND "asset"."isFavorite" = $4 + AND ( + "stack"."primaryAssetId" = "asset"."id" + OR "asset"."stackId" IS NULL + ) + ) + AND ("asset"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "distinctAlias"."asset_fileCreatedAt" DESC, + "asset_id" ASC +LIMIT + 101 + +-- SearchRepository.searchSmart +START TRANSACTION +SET + LOCAL vectors.enable_prefilter = on; + +SET + LOCAL vectors.search_mode = vbase; + +SET + LOCAL vectors.hnsw_ef_search = 100; +SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."resizePath" AS "asset_resizePath", + "asset"."webpPath" AS "asset_webpPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isReadOnly" AS "asset_isReadOnly", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "stack"."id" AS "stack_id", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."resizePath" AS "stackedAssets_resizePath", + "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId" +FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) + INNER JOIN "smart_search" "search" ON "search"."assetId" = "asset"."id" +WHERE + ( + "asset"."fileCreatedAt" >= $1 + AND "exifInfo"."lensModel" = $2 + AND 1 = 1 + AND 1 = 1 + AND "asset"."isFavorite" = $3 + AND ( + "stack"."primaryAssetId" = "asset"."id" + OR "asset"."stackId" IS NULL + ) + AND "asset"."ownerId" IN ($4) + ) + AND ("asset"."deletedAt" IS NULL) +ORDER BY + "search"."embedding" <= > $5 ASC +LIMIT + 101 +COMMIT + +-- SearchRepository.searchFaces +START TRANSACTION +SET + LOCAL vectors.enable_prefilter = on; + +SET + LOCAL vectors.search_mode = vbase; + +SET + LOCAL vectors.hnsw_ef_search = 100; +WITH + "cte" AS ( + SELECT + "faces"."id" AS "id", + "faces"."assetId" AS "assetId", + "faces"."personId" AS "personId", + "faces"."imageWidth" AS "imageWidth", + "faces"."imageHeight" AS "imageHeight", + "faces"."boundingBoxX1" AS "boundingBoxX1", + "faces"."boundingBoxY1" AS "boundingBoxY1", + "faces"."boundingBoxX2" AS "boundingBoxX2", + "faces"."boundingBoxY2" AS "boundingBoxY2", + "faces"."embedding" <= > $1 AS "distance" + FROM + "asset_faces" "faces" + INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId" + AND ("asset"."deletedAt" IS NULL) + WHERE + "asset"."ownerId" IN ($2) + ORDER BY + "faces"."embedding" <= > $1 ASC + LIMIT + 100 + ) +SELECT + res.* +FROM + "cte" "res" +WHERE + res.distance <= $3 +COMMIT diff --git a/server/src/infra/sql/smart.info.repository.sql b/server/src/infra/sql/smart.info.repository.sql deleted file mode 100644 index 3151aede7..000000000 --- a/server/src/infra/sql/smart.info.repository.sql +++ /dev/null @@ -1,129 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator - --- SmartInfoRepository.searchCLIP -START TRANSACTION -SET - LOCAL vectors.enable_prefilter = on; - -SET - LOCAL vectors.search_mode = vbase; - -SET - LOCAL vectors.hnsw_ef_search = 100; -SELECT - "a"."id" AS "a_id", - "a"."deviceAssetId" AS "a_deviceAssetId", - "a"."ownerId" AS "a_ownerId", - "a"."libraryId" AS "a_libraryId", - "a"."deviceId" AS "a_deviceId", - "a"."type" AS "a_type", - "a"."originalPath" AS "a_originalPath", - "a"."resizePath" AS "a_resizePath", - "a"."webpPath" AS "a_webpPath", - "a"."thumbhash" AS "a_thumbhash", - "a"."encodedVideoPath" AS "a_encodedVideoPath", - "a"."createdAt" AS "a_createdAt", - "a"."updatedAt" AS "a_updatedAt", - "a"."deletedAt" AS "a_deletedAt", - "a"."fileCreatedAt" AS "a_fileCreatedAt", - "a"."localDateTime" AS "a_localDateTime", - "a"."fileModifiedAt" AS "a_fileModifiedAt", - "a"."isFavorite" AS "a_isFavorite", - "a"."isArchived" AS "a_isArchived", - "a"."isExternal" AS "a_isExternal", - "a"."isReadOnly" AS "a_isReadOnly", - "a"."isOffline" AS "a_isOffline", - "a"."checksum" AS "a_checksum", - "a"."duration" AS "a_duration", - "a"."isVisible" AS "a_isVisible", - "a"."livePhotoVideoId" AS "a_livePhotoVideoId", - "a"."originalFileName" AS "a_originalFileName", - "a"."sidecarPath" AS "a_sidecarPath", - "a"."stackId" AS "a_stackId", - "e"."assetId" AS "e_assetId", - "e"."description" AS "e_description", - "e"."exifImageWidth" AS "e_exifImageWidth", - "e"."exifImageHeight" AS "e_exifImageHeight", - "e"."fileSizeInByte" AS "e_fileSizeInByte", - "e"."orientation" AS "e_orientation", - "e"."dateTimeOriginal" AS "e_dateTimeOriginal", - "e"."modifyDate" AS "e_modifyDate", - "e"."timeZone" AS "e_timeZone", - "e"."latitude" AS "e_latitude", - "e"."longitude" AS "e_longitude", - "e"."projectionType" AS "e_projectionType", - "e"."city" AS "e_city", - "e"."livePhotoCID" AS "e_livePhotoCID", - "e"."autoStackId" AS "e_autoStackId", - "e"."state" AS "e_state", - "e"."country" AS "e_country", - "e"."make" AS "e_make", - "e"."model" AS "e_model", - "e"."lensModel" AS "e_lensModel", - "e"."fNumber" AS "e_fNumber", - "e"."focalLength" AS "e_focalLength", - "e"."iso" AS "e_iso", - "e"."exposureTime" AS "e_exposureTime", - "e"."profileDescription" AS "e_profileDescription", - "e"."colorspace" AS "e_colorspace", - "e"."bitsPerSample" AS "e_bitsPerSample", - "e"."fps" AS "e_fps" -FROM - "assets" "a" - INNER JOIN "smart_search" "s" ON "s"."assetId" = "a"."id" - LEFT JOIN "exif" "e" ON "e"."assetId" = "a"."id" -WHERE - ( - "a"."ownerId" IN ($1) - AND "a"."isArchived" = false - AND "a"."isVisible" = true - AND "a"."fileCreatedAt" < NOW() - ) - AND ("a"."deletedAt" IS NULL) -ORDER BY - "s"."embedding" <= > $2 ASC -LIMIT - 100 -COMMIT - --- SmartInfoRepository.searchFaces -START TRANSACTION -SET - LOCAL vectors.enable_prefilter = on; - -SET - LOCAL vectors.search_mode = vbase; - -SET - LOCAL vectors.hnsw_ef_search = 100; -WITH - "cte" AS ( - SELECT - "faces"."id" AS "id", - "faces"."assetId" AS "assetId", - "faces"."personId" AS "personId", - "faces"."imageWidth" AS "imageWidth", - "faces"."imageHeight" AS "imageHeight", - "faces"."boundingBoxX1" AS "boundingBoxX1", - "faces"."boundingBoxY1" AS "boundingBoxY1", - "faces"."boundingBoxX2" AS "boundingBoxX2", - "faces"."boundingBoxY2" AS "boundingBoxY2", - "faces"."embedding" <= > $1 AS "distance" - FROM - "asset_faces" "faces" - INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId" - AND ("asset"."deletedAt" IS NULL) - WHERE - "asset"."ownerId" IN ($2) - ORDER BY - "faces"."embedding" <= > $1 ASC - LIMIT - 100 - ) -SELECT - res.* -FROM - "cte" "res" -WHERE - res.distance <= $3 -COMMIT diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 9d778883d..1c98b78c9 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -32,7 +32,6 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getTimeBuckets: jest.fn(), restoreAll: jest.fn(), softDeleteAll: jest.fn(), - search: jest.fn(), getAssetIdByCity: jest.fn(), getAssetIdByTag: jest.fn(), searchMetadata: jest.fn(), diff --git a/server/test/repositories/index.ts b/server/test/repositories/index.ts index e31a3a1c4..90fd1326b 100644 --- a/server/test/repositories/index.ts +++ b/server/test/repositories/index.ts @@ -15,8 +15,8 @@ export * from './metadata.repository.mock'; export * from './move.repository.mock'; export * from './partner.repository.mock'; export * from './person.repository.mock'; +export * from './search.repository.mock'; export * from './shared-link.repository.mock'; -export * from './smart-info.repository.mock'; export * from './storage.repository.mock'; export * from './system-config.repository.mock'; export * from './system-info.repository.mock'; diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts new file mode 100644 index 000000000..e0bdab269 --- /dev/null +++ b/server/test/repositories/search.repository.mock.ts @@ -0,0 +1,11 @@ +import { ISearchRepository } from '@app/domain'; + +export const newSearchRepositoryMock = (): jest.Mocked => { + return { + init: jest.fn(), + searchMetadata: jest.fn(), + searchSmart: jest.fn(), + searchFaces: jest.fn(), + upsert: jest.fn(), + }; +}; diff --git a/server/test/repositories/smart-info.repository.mock.ts b/server/test/repositories/smart-info.repository.mock.ts deleted file mode 100644 index c7bc4f5c5..000000000 --- a/server/test/repositories/smart-info.repository.mock.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ISmartInfoRepository } from '@app/domain'; - -export const newSmartInfoRepositoryMock = (): jest.Mocked => { - return { - init: jest.fn(), - searchCLIP: jest.fn(), - searchFaces: jest.fn(), - upsert: jest.fn(), - }; -}; diff --git a/web/src/lib/components/asset-viewer/intersection-observer.svelte b/web/src/lib/components/asset-viewer/intersection-observer.svelte index f1fbc4aa2..df89a2ed7 100644 --- a/web/src/lib/components/asset-viewer/intersection-observer.svelte +++ b/web/src/lib/components/asset-viewer/intersection-observer.svelte @@ -9,7 +9,7 @@ export let right = 0; export let root: HTMLElement | null = null; - let intersecting = false; + export let intersecting = false; let container: HTMLDivElement; const dispatch = createEventDispatcher<{ hidden: HTMLDivElement; diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index de540b320..1df6a0d9d 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -37,6 +37,7 @@ export let readonly = false; export let showArchiveIcon = false; export let showStackedIcon = true; + export let intersecting = false; let className = ''; export { className as class }; @@ -85,7 +86,7 @@ }; - +
onMouseEnter()} - on:mouseleave={() => onMouseLeave()} + on:mouseenter={onMouseEnter} + on:mouseleave={onMouseLeave} on:click={thumbnailClickedHandler} on:keydown={thumbnailKeyDownHandler} > diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 669d3ec81..a3bc54c45 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -8,6 +8,10 @@ import { getThumbnailSize } from '$lib/utils/thumbnail-util'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { onDestroy } from 'svelte'; + import { createEventDispatcher } from 'svelte'; + import type { BucketPosition } from '$lib/stores/assets.store'; + + const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>(); export let assets: AssetResponseDto[]; export let selectedAssets: Set = new Set(); @@ -18,7 +22,6 @@ let selectedAsset: AssetResponseDto; let currentViewAssetIndex = 0; - let viewWidth: number; $: thumbnailSize = getThumbnailSize(assets.length, viewWidth); @@ -88,7 +91,7 @@ {#if assets.length > 0}
- {#each assets as asset (asset.id)} + {#each assets as asset, i (asset.id)}
(isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} on:select={selectAssetHandler} + on:intersected={(event) => + i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined} selected={selectedAssets.has(asset)} {showArchiveIcon} /> diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 8ed9d55fa..8dfba184a 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -32,6 +32,7 @@ const parameters = new URLSearchParams({ q: searchValue, smart: smartSearch, + take: '100', }); showHistory = false; diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index 42f9af0f0..ea068a713 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -14,7 +14,6 @@ import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; - import type { AssetResponseDto } from '@api'; import type { PageData } from './$types'; import Icon from '$lib/components/elements/icon.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; @@ -27,15 +26,20 @@ import { preventRaceConditionSearchBar } from '$lib/stores/search.store'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; + import type { AssetResponseDto, SearchResponseDto } from '@immich/sdk'; + import { authenticate } from '$lib/utils/auth'; + import { api } from '@api'; export let data: PageData; + const MAX_ASSET_COUNT = 5000; let { isViewing: showAssetViewer } = assetViewingStore; // The GalleryViewer pushes it's own history state, which causes weird // behavior for history.back(). To prevent that we store the previous page // manually and navigate back to that. let previousRoute = AppRoute.EXPLORE as string; + $: curPage = data.results?.assets.nextPage; $: albums = data.results?.albums.items; const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); @@ -107,6 +111,33 @@ const handleSelectAll = () => { selectedAssets = new Set(searchResultAssets); }; + + export const loadNextPage = async () => { + if (curPage == null || !term || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) { + return; + } + + await authenticate(); + let results: SearchResponseDto | null = null; + $page.url.searchParams.set('page', curPage.toString()); + const res = await api.searchApi.search({}, { params: $page.url.searchParams }); + if (searchResultAssets) { + searchResultAssets.push(...res.data.assets.items); + } else { + searchResultAssets = res.data.assets.items; + } + + const assets = { + ...res.data.assets, + items: searchResultAssets, + }; + results = { + assets, + albums: res.data.albums, + }; + + data.results = results; + };
@@ -164,7 +195,12 @@
{#if searchResultAssets && searchResultAssets.length > 0}
- +
{:else}
diff --git a/web/src/routes/(user)/search/+page.ts b/web/src/routes/(user)/search/+page.ts index b6cbac101..75b9df487 100644 --- a/web/src/routes/(user)/search/+page.ts +++ b/web/src/routes/(user)/search/+page.ts @@ -1,5 +1,5 @@ import { authenticate } from '$lib/utils/auth'; -import { type SearchResponseDto, api } from '@api'; +import { type AssetResponseDto, type SearchResponseDto, api } from '@api'; import type { PageLoad } from './$types'; import { QueryParameter } from '$lib/constants'; @@ -10,8 +10,18 @@ export const load = (async (data) => { url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined; let results: SearchResponseDto | null = null; if (term) { - const { data } = await api.searchApi.search({}, { params: url.searchParams }); - results = data; + const res = await api.searchApi.search({}, { params: data.url.searchParams }); + let items: AssetResponseDto[] = (data as unknown as { results: SearchResponseDto }).results?.assets.items; + if (items) { + items.push(...res.data.assets.items); + } else { + items = res.data.assets.items; + } + const assets = { ...res.data.assets, items }; + results = { + assets, + albums: res.data.albums, + }; } return {