mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 13:46:47 -04:00
feat(server, web): smart search filtering and pagination (#6525)
* initial pagination impl * use limit + offset instead of take + skip * wip web pagination * working infinite scroll * update api * formatting * fix rebase * search refactor * re-add runtime config for vector search * fix rebase * fixes * useless omitBy * unnecessary handling * add sql decorator for `searchAssets` * fixed search builder * fixed sql * remove mock method * linting * fixed pagination * fixed unit tests * formatting * fix e2e tests * re-flatten search builder * refactor endpoints * clean up dto * refinements * don't break everything just yet * update openapi spec & sql * update api * linting * update sql * fixes * optimize web code * fix typing * add page limit * make limit based on asset count * increase limit * simpler import
This commit is contained in:
parent
f1e4fdf175
commit
e334443919
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -162,7 +162,9 @@ Class | Method | HTTP request | Description
|
|||||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||||
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
|
*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* | [**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* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
|
||||||
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
|
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
|
||||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
||||||
|
6
mobile/openapi/doc/AssetApi.md
generated
6
mobile/openapi/doc/AssetApi.md
generated
@ -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)
|
[[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**
|
# **searchAssets**
|
||||||
> List<AssetResponseDto> 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<AssetResponseDto> 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 updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||||
final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
|
final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
|
||||||
final webpPath = webpPath_example; // String |
|
final webpPath = webpPath_example; // String |
|
||||||
|
final withArchived = true; // bool |
|
||||||
final withDeleted = true; // bool |
|
final withDeleted = true; // bool |
|
||||||
final withExif = true; // bool |
|
final withExif = true; // bool |
|
||||||
final withPeople = true; // bool |
|
final withPeople = true; // bool |
|
||||||
final withStacked = true; // bool |
|
final withStacked = true; // bool |
|
||||||
|
|
||||||
try {
|
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);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AssetApi->searchAssets: $e\n');
|
print('Exception when calling AssetApi->searchAssets: $e\n');
|
||||||
@ -1146,6 +1147,7 @@ Name | Type | Description | Notes
|
|||||||
**updatedAfter** | **DateTime**| | [optional]
|
**updatedAfter** | **DateTime**| | [optional]
|
||||||
**updatedBefore** | **DateTime**| | [optional]
|
**updatedBefore** | **DateTime**| | [optional]
|
||||||
**webpPath** | **String**| | [optional]
|
**webpPath** | **String**| | [optional]
|
||||||
|
**withArchived** | **bool**| | [optional]
|
||||||
**withDeleted** | **bool**| | [optional]
|
**withDeleted** | **bool**| | [optional]
|
||||||
**withExif** | **bool**| | [optional]
|
**withExif** | **bool**| | [optional]
|
||||||
**withPeople** | **bool**| | [optional]
|
**withPeople** | **bool**| | [optional]
|
||||||
|
260
mobile/openapi/doc/SearchApi.md
generated
260
mobile/openapi/doc/SearchApi.md
generated
@ -11,7 +11,9 @@ Method | HTTP request | Description
|
|||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
|
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||||
[**search**](SearchApi.md#search) | **GET** /search |
|
[**search**](SearchApi.md#search) | **GET** /search |
|
||||||
|
[**searchMetadata**](SearchApi.md#searchmetadata) | **GET** /search/metadata |
|
||||||
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
|
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
|
||||||
|
[**searchSmart**](SearchApi.md#searchsmart) | **GET** /search/smart |
|
||||||
|
|
||||||
|
|
||||||
# **getExploreData**
|
# **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)
|
[[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**
|
# **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 api_instance = SearchApi();
|
||||||
final clip = true; // bool | @deprecated
|
final clip = true; // bool | @deprecated
|
||||||
final motion = true; // bool |
|
final motion = true; // bool |
|
||||||
|
final page = 8.14; // num |
|
||||||
final q = q_example; // String |
|
final q = q_example; // String |
|
||||||
final query = query_example; // String |
|
final query = query_example; // String |
|
||||||
final recent = true; // bool |
|
final recent = true; // bool |
|
||||||
|
final size = 8.14; // num |
|
||||||
final smart = true; // bool |
|
final smart = true; // bool |
|
||||||
final type = type_example; // String |
|
final type = type_example; // String |
|
||||||
final withArchived = true; // bool |
|
final withArchived = true; // bool |
|
||||||
|
|
||||||
try {
|
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);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling SearchApi->search: $e\n');
|
print('Exception when calling SearchApi->search: $e\n');
|
||||||
@ -112,9 +116,11 @@ Name | Type | Description | Notes
|
|||||||
------------- | ------------- | ------------- | -------------
|
------------- | ------------- | ------------- | -------------
|
||||||
**clip** | **bool**| @deprecated | [optional]
|
**clip** | **bool**| @deprecated | [optional]
|
||||||
**motion** | **bool**| | [optional]
|
**motion** | **bool**| | [optional]
|
||||||
|
**page** | **num**| | [optional]
|
||||||
**q** | **String**| | [optional]
|
**q** | **String**| | [optional]
|
||||||
**query** | **String**| | [optional]
|
**query** | **String**| | [optional]
|
||||||
**recent** | **bool**| | [optional]
|
**recent** | **bool**| | [optional]
|
||||||
|
**size** | **num**| | [optional]
|
||||||
**smart** | **bool**| | [optional]
|
**smart** | **bool**| | [optional]
|
||||||
**type** | **String**| | [optional]
|
**type** | **String**| | [optional]
|
||||||
**withArchived** | **bool**| | [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)
|
[[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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = 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**
|
# **searchPerson**
|
||||||
> List<PersonResponseDto> searchPerson(name, withHidden)
|
> List<PersonResponseDto> 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)
|
[[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<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = 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)
|
||||||
|
|
||||||
|
1
mobile/openapi/doc/SearchAssetResponseDto.md
generated
1
mobile/openapi/doc/SearchAssetResponseDto.md
generated
@ -11,6 +11,7 @@ Name | Type | Description | Notes
|
|||||||
**count** | **int** | |
|
**count** | **int** | |
|
||||||
**facets** | [**List<SearchFacetResponseDto>**](SearchFacetResponseDto.md) | | [default to const []]
|
**facets** | [**List<SearchFacetResponseDto>**](SearchFacetResponseDto.md) | | [default to const []]
|
||||||
**items** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
|
**items** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
|
||||||
|
**nextPage** | **String** | |
|
||||||
**total** | **int** | |
|
**total** | **int** | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
13
mobile/openapi/lib/api/asset_api.dart
generated
13
mobile/openapi/lib/api/asset_api.dart
generated
@ -1177,6 +1177,8 @@ class AssetApi {
|
|||||||
///
|
///
|
||||||
/// * [String] webpPath:
|
/// * [String] webpPath:
|
||||||
///
|
///
|
||||||
|
/// * [bool] withArchived:
|
||||||
|
///
|
||||||
/// * [bool] withDeleted:
|
/// * [bool] withDeleted:
|
||||||
///
|
///
|
||||||
/// * [bool] withExif:
|
/// * [bool] withExif:
|
||||||
@ -1184,7 +1186,7 @@ class AssetApi {
|
|||||||
/// * [bool] withPeople:
|
/// * [bool] withPeople:
|
||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
Future<Response> 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<Response> 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
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/assets';
|
final path = r'/assets';
|
||||||
|
|
||||||
@ -1303,6 +1305,9 @@ class AssetApi {
|
|||||||
if (webpPath != null) {
|
if (webpPath != null) {
|
||||||
queryParams.addAll(_queryParams('', 'webpPath', webpPath));
|
queryParams.addAll(_queryParams('', 'webpPath', webpPath));
|
||||||
}
|
}
|
||||||
|
if (withArchived != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withArchived', withArchived));
|
||||||
|
}
|
||||||
if (withDeleted != null) {
|
if (withDeleted != null) {
|
||||||
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
|
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
|
||||||
}
|
}
|
||||||
@ -1404,6 +1409,8 @@ class AssetApi {
|
|||||||
///
|
///
|
||||||
/// * [String] webpPath:
|
/// * [String] webpPath:
|
||||||
///
|
///
|
||||||
|
/// * [bool] withArchived:
|
||||||
|
///
|
||||||
/// * [bool] withDeleted:
|
/// * [bool] withDeleted:
|
||||||
///
|
///
|
||||||
/// * [bool] withExif:
|
/// * [bool] withExif:
|
||||||
@ -1411,8 +1418,8 @@ class AssetApi {
|
|||||||
/// * [bool] withPeople:
|
/// * [bool] withPeople:
|
||||||
///
|
///
|
||||||
/// * [bool] withStacked:
|
/// * [bool] withStacked:
|
||||||
Future<List<AssetResponseDto>?> 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<List<AssetResponseDto>?> 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, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, );
|
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) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
610
mobile/openapi/lib/api/search_api.dart
generated
610
mobile/openapi/lib/api/search_api.dart
generated
@ -68,18 +68,22 @@ class SearchApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] motion:
|
/// * [bool] motion:
|
||||||
///
|
///
|
||||||
|
/// * [num] page:
|
||||||
|
///
|
||||||
/// * [String] q:
|
/// * [String] q:
|
||||||
///
|
///
|
||||||
/// * [String] query:
|
/// * [String] query:
|
||||||
///
|
///
|
||||||
/// * [bool] recent:
|
/// * [bool] recent:
|
||||||
///
|
///
|
||||||
|
/// * [num] size:
|
||||||
|
///
|
||||||
/// * [bool] smart:
|
/// * [bool] smart:
|
||||||
///
|
///
|
||||||
/// * [String] type:
|
/// * [String] type:
|
||||||
///
|
///
|
||||||
/// * [bool] withArchived:
|
/// * [bool] withArchived:
|
||||||
Future<Response> searchWithHttpInfo({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async {
|
Future<Response> 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
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/search';
|
final path = r'/search';
|
||||||
|
|
||||||
@ -96,6 +100,9 @@ class SearchApi {
|
|||||||
if (motion != null) {
|
if (motion != null) {
|
||||||
queryParams.addAll(_queryParams('', 'motion', motion));
|
queryParams.addAll(_queryParams('', 'motion', motion));
|
||||||
}
|
}
|
||||||
|
if (page != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'page', page));
|
||||||
|
}
|
||||||
if (q != null) {
|
if (q != null) {
|
||||||
queryParams.addAll(_queryParams('', 'q', q));
|
queryParams.addAll(_queryParams('', 'q', q));
|
||||||
}
|
}
|
||||||
@ -105,6 +112,9 @@ class SearchApi {
|
|||||||
if (recent != null) {
|
if (recent != null) {
|
||||||
queryParams.addAll(_queryParams('', 'recent', recent));
|
queryParams.addAll(_queryParams('', 'recent', recent));
|
||||||
}
|
}
|
||||||
|
if (size != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'size', size));
|
||||||
|
}
|
||||||
if (smart != null) {
|
if (smart != null) {
|
||||||
queryParams.addAll(_queryParams('', 'smart', smart));
|
queryParams.addAll(_queryParams('', 'smart', smart));
|
||||||
}
|
}
|
||||||
@ -136,19 +146,354 @@ class SearchApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] motion:
|
/// * [bool] motion:
|
||||||
///
|
///
|
||||||
|
/// * [num] page:
|
||||||
|
///
|
||||||
/// * [String] q:
|
/// * [String] q:
|
||||||
///
|
///
|
||||||
/// * [String] query:
|
/// * [String] query:
|
||||||
///
|
///
|
||||||
/// * [bool] recent:
|
/// * [bool] recent:
|
||||||
///
|
///
|
||||||
|
/// * [num] size:
|
||||||
|
///
|
||||||
/// * [bool] smart:
|
/// * [bool] smart:
|
||||||
///
|
///
|
||||||
/// * [String] type:
|
/// * [String] type:
|
||||||
///
|
///
|
||||||
/// * [bool] withArchived:
|
/// * [bool] withArchived:
|
||||||
Future<SearchResponseDto?> search({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async {
|
Future<SearchResponseDto?> 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, q: q, query: query, recent: recent, smart: smart, type: type, withArchived: withArchived, );
|
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<Response> 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 = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
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 = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
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<SearchResponseDto?> 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) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -220,4 +565,263 @@ class SearchApi {
|
|||||||
}
|
}
|
||||||
return null;
|
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<Response> 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 = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
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 = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
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<SearchResponseDto?> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ class SearchAssetResponseDto {
|
|||||||
required this.count,
|
required this.count,
|
||||||
this.facets = const [],
|
this.facets = const [],
|
||||||
this.items = const [],
|
this.items = const [],
|
||||||
|
required this.nextPage,
|
||||||
required this.total,
|
required this.total,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,6 +26,8 @@ class SearchAssetResponseDto {
|
|||||||
|
|
||||||
List<AssetResponseDto> items;
|
List<AssetResponseDto> items;
|
||||||
|
|
||||||
|
String? nextPage;
|
||||||
|
|
||||||
int total;
|
int total;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -32,6 +35,7 @@ class SearchAssetResponseDto {
|
|||||||
other.count == count &&
|
other.count == count &&
|
||||||
_deepEquality.equals(other.facets, facets) &&
|
_deepEquality.equals(other.facets, facets) &&
|
||||||
_deepEquality.equals(other.items, items) &&
|
_deepEquality.equals(other.items, items) &&
|
||||||
|
other.nextPage == nextPage &&
|
||||||
other.total == total;
|
other.total == total;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,16 +44,22 @@ class SearchAssetResponseDto {
|
|||||||
(count.hashCode) +
|
(count.hashCode) +
|
||||||
(facets.hashCode) +
|
(facets.hashCode) +
|
||||||
(items.hashCode) +
|
(items.hashCode) +
|
||||||
|
(nextPage == null ? 0 : nextPage!.hashCode) +
|
||||||
(total.hashCode);
|
(total.hashCode);
|
||||||
|
|
||||||
@override
|
@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<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'count'] = this.count;
|
json[r'count'] = this.count;
|
||||||
json[r'facets'] = this.facets;
|
json[r'facets'] = this.facets;
|
||||||
json[r'items'] = this.items;
|
json[r'items'] = this.items;
|
||||||
|
if (this.nextPage != null) {
|
||||||
|
json[r'nextPage'] = this.nextPage;
|
||||||
|
} else {
|
||||||
|
// json[r'nextPage'] = null;
|
||||||
|
}
|
||||||
json[r'total'] = this.total;
|
json[r'total'] = this.total;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -65,6 +75,7 @@ class SearchAssetResponseDto {
|
|||||||
count: mapValueOfType<int>(json, r'count')!,
|
count: mapValueOfType<int>(json, r'count')!,
|
||||||
facets: SearchFacetResponseDto.listFromJson(json[r'facets']),
|
facets: SearchFacetResponseDto.listFromJson(json[r'facets']),
|
||||||
items: AssetResponseDto.listFromJson(json[r'items']),
|
items: AssetResponseDto.listFromJson(json[r'items']),
|
||||||
|
nextPage: mapValueOfType<String>(json, r'nextPage'),
|
||||||
total: mapValueOfType<int>(json, r'total')!,
|
total: mapValueOfType<int>(json, r'total')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -116,6 +127,7 @@ class SearchAssetResponseDto {
|
|||||||
'count',
|
'count',
|
||||||
'facets',
|
'facets',
|
||||||
'items',
|
'items',
|
||||||
|
'nextPage',
|
||||||
'total',
|
'total',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
2
mobile/openapi/test/asset_api_test.dart
generated
2
mobile/openapi/test/asset_api_test.dart
generated
@ -110,7 +110,7 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<List<AssetResponseDto>> 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<List<AssetResponseDto>> 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 {
|
test('test searchAssets', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
12
mobile/openapi/test/search_api_test.dart
generated
12
mobile/openapi/test/search_api_test.dart
generated
@ -22,15 +22,25 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<SearchResponseDto> search({ bool clip, bool motion, String q, String query, bool recent, bool smart, String type, bool withArchived }) async
|
//Future<SearchResponseDto> 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 {
|
test('test search', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future<SearchResponseDto> 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<List<PersonResponseDto>> searchPerson(String name, { bool withHidden }) async
|
//Future<List<PersonResponseDto>> searchPerson(String name, { bool withHidden }) async
|
||||||
test('test searchPerson', () async {
|
test('test searchPerson', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future<SearchResponseDto> 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
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String nextPage
|
||||||
|
test('to test the property `nextPage`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// int total
|
// int total
|
||||||
test('to test the property `total`', () async {
|
test('to test the property `total`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -2130,6 +2130,7 @@
|
|||||||
},
|
},
|
||||||
"/assets": {
|
"/assets": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"deprecated": true,
|
||||||
"operationId": "searchAssets",
|
"operationId": "searchAssets",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -2430,6 +2431,14 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "withArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "withDeleted",
|
"name": "withDeleted",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -4354,6 +4363,7 @@
|
|||||||
},
|
},
|
||||||
"/search": {
|
"/search": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"deprecated": true,
|
||||||
"operationId": "search",
|
"operationId": "search",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -4374,6 +4384,14 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "page",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -4398,6 +4416,14 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "smart",
|
"name": "smart",
|
||||||
"required": false,
|
"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": {
|
"/search/person": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "searchPerson",
|
"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": {
|
"/server-info": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getServerInfo",
|
"operationId": "getServerInfo",
|
||||||
@ -8458,6 +9145,10 @@
|
|||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"nextPage": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"total": {
|
"total": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
@ -8466,6 +9157,7 @@
|
|||||||
"count",
|
"count",
|
||||||
"facets",
|
"facets",
|
||||||
"items",
|
"items",
|
||||||
|
"nextPage",
|
||||||
"total"
|
"total"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -588,6 +588,7 @@ export type SearchAssetResponseDto = {
|
|||||||
count: number;
|
count: number;
|
||||||
facets: SearchFacetResponseDto[];
|
facets: SearchFacetResponseDto[];
|
||||||
items: AssetResponseDto[];
|
items: AssetResponseDto[];
|
||||||
|
nextPage: string | null;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
export type SearchResponseDto = {
|
export type SearchResponseDto = {
|
||||||
@ -1461,7 +1462,7 @@ export function updateAsset({ id, updateAssetDto }: {
|
|||||||
body: 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;
|
checksum?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
country?: string;
|
country?: string;
|
||||||
@ -1498,6 +1499,7 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef
|
|||||||
updatedAfter?: string;
|
updatedAfter?: string;
|
||||||
updatedBefore?: string;
|
updatedBefore?: string;
|
||||||
webpPath?: string;
|
webpPath?: string;
|
||||||
|
withArchived?: boolean;
|
||||||
withDeleted?: boolean;
|
withDeleted?: boolean;
|
||||||
withExif?: boolean;
|
withExif?: boolean;
|
||||||
withPeople?: boolean;
|
withPeople?: boolean;
|
||||||
@ -1543,6 +1545,7 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef
|
|||||||
updatedAfter,
|
updatedAfter,
|
||||||
updatedBefore,
|
updatedBefore,
|
||||||
webpPath,
|
webpPath,
|
||||||
|
withArchived,
|
||||||
withDeleted,
|
withDeleted,
|
||||||
withExif,
|
withExif,
|
||||||
withPeople,
|
withPeople,
|
||||||
@ -2047,12 +2050,14 @@ export function getPersonThumbnail({ id }: {
|
|||||||
...opts
|
...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;
|
clip?: boolean;
|
||||||
motion?: boolean;
|
motion?: boolean;
|
||||||
|
page?: number;
|
||||||
q?: string;
|
q?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
recent?: boolean;
|
recent?: boolean;
|
||||||
|
size?: number;
|
||||||
smart?: boolean;
|
smart?: boolean;
|
||||||
$type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER";
|
$type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER";
|
||||||
withArchived?: boolean;
|
withArchived?: boolean;
|
||||||
@ -2063,9 +2068,11 @@ export function search({ clip, motion, q, query, recent, smart, $type, withArchi
|
|||||||
}>(`/search${QS.query(QS.explode({
|
}>(`/search${QS.query(QS.explode({
|
||||||
clip,
|
clip,
|
||||||
motion,
|
motion,
|
||||||
|
page,
|
||||||
q,
|
q,
|
||||||
query,
|
query,
|
||||||
recent,
|
recent,
|
||||||
|
size,
|
||||||
smart,
|
smart,
|
||||||
"type": $type,
|
"type": $type,
|
||||||
withArchived
|
withArchived
|
||||||
@ -2081,6 +2088,98 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...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 }: {
|
export function searchPerson({ name, withHidden }: {
|
||||||
name: string;
|
name: string;
|
||||||
withHidden?: boolean;
|
withHidden?: boolean;
|
||||||
@ -2095,6 +2194,78 @@ export function searchPerson({ name, withHidden }: {
|
|||||||
...opts
|
...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) {
|
export function getServerInfo(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -169,7 +169,11 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
{
|
{
|
||||||
should: 'should reject size as a string',
|
should: 'should reject size as a string',
|
||||||
query: { size: 'abc' },
|
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',
|
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: () => ({
|
deferred: () => ({
|
||||||
query: { make: 'Cannon' },
|
query: { make: 'Cannon' },
|
||||||
assets: [asset3],
|
assets: [asset3],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
mapAsset,
|
mapAsset,
|
||||||
@ -20,14 +20,14 @@ describe(`${SearchController.name}`, () => {
|
|||||||
let accessToken: string;
|
let accessToken: string;
|
||||||
let libraries: LibraryResponseDto[];
|
let libraries: LibraryResponseDto[];
|
||||||
let assetRepository: IAssetRepository;
|
let assetRepository: IAssetRepository;
|
||||||
let smartInfoRepository: ISmartInfoRepository;
|
let smartInfoRepository: ISearchRepository;
|
||||||
let asset1: AssetResponseDto;
|
let asset1: AssetResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await testApp.create();
|
app = await testApp.create();
|
||||||
server = app.getHttpServer();
|
server = app.getHttpServer();
|
||||||
assetRepository = app.get<IAssetRepository>(IAssetRepository);
|
assetRepository = app.get<IAssetRepository>(IAssetRepository);
|
||||||
smartInfoRepository = app.get<ISmartInfoRepository>(ISmartInfoRepository);
|
smartInfoRepository = app.get<ISearchRepository>(ISearchRepository);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -31,8 +31,6 @@ import {
|
|||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetJobsDto,
|
AssetJobsDto,
|
||||||
AssetOrder,
|
|
||||||
AssetSearchDto,
|
|
||||||
AssetStatsDto,
|
AssetStatsDto,
|
||||||
MapMarkerDto,
|
MapMarkerDto,
|
||||||
MemoryLaneDto,
|
MemoryLaneDto,
|
||||||
@ -92,34 +90,6 @@ export class AssetService {
|
|||||||
this.configCore = SystemConfigCore.create(configRepository);
|
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 {
|
canUploadFile({ auth, fieldName, file }: UploadRequest): true {
|
||||||
this.access.requireUploadAccess(auth);
|
this.access.requireUploadAccess(auth);
|
||||||
|
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
import { AssetType } from '@app/infra/entities';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
IsEnum,
|
|
||||||
IsInt,
|
IsInt,
|
||||||
IsLatitude,
|
IsLatitude,
|
||||||
IsLongitude,
|
IsLongitude,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
Min,
|
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util';
|
import { Optional, ValidateUUID } from '../../domain.util';
|
||||||
import { BulkIdsDto } from '../response-dto';
|
import { BulkIdsDto } from '../response-dto';
|
||||||
|
|
||||||
export class DeviceIdDto {
|
export class DeviceIdDto {
|
||||||
@ -32,152 +28,6 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
|
|||||||
o.latitude !== undefined || o.longitude !== undefined;
|
o.latitude !== undefined || o.longitude !== undefined;
|
||||||
const ValidateGPS = () => ValidateIf(hasGPS);
|
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 {
|
export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||||
@Optional()
|
@Optional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@ -137,6 +137,17 @@ export interface PaginationOptions {
|
|||||||
skip?: number;
|
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<T> {
|
export interface PaginationResult<T> {
|
||||||
items: T[];
|
items: T[];
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
newMediaRepositoryMock,
|
newMediaRepositoryMock,
|
||||||
newMoveRepositoryMock,
|
newMoveRepositoryMock,
|
||||||
newPersonRepositoryMock,
|
newPersonRepositoryMock,
|
||||||
newSmartInfoRepositoryMock,
|
newSearchRepositoryMock,
|
||||||
newStorageRepositoryMock,
|
newStorageRepositoryMock,
|
||||||
newSystemConfigRepositoryMock,
|
newSystemConfigRepositoryMock,
|
||||||
personStub,
|
personStub,
|
||||||
@ -31,7 +31,7 @@ import {
|
|||||||
IMediaRepository,
|
IMediaRepository,
|
||||||
IMoveRepository,
|
IMoveRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
@ -76,7 +76,7 @@ describe(PersonService.name, () => {
|
|||||||
let moveMock: jest.Mocked<IMoveRepository>;
|
let moveMock: jest.Mocked<IMoveRepository>;
|
||||||
let personMock: jest.Mocked<IPersonRepository>;
|
let personMock: jest.Mocked<IPersonRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
let smartInfoMock: jest.Mocked<ISmartInfoRepository>;
|
let searchMock: jest.Mocked<ISearchRepository>;
|
||||||
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
let sut: PersonService;
|
let sut: PersonService;
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ describe(PersonService.name, () => {
|
|||||||
mediaMock = newMediaRepositoryMock();
|
mediaMock = newMediaRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
smartInfoMock = newSmartInfoRepositoryMock();
|
searchMock = newSearchRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
sut = new PersonService(
|
sut = new PersonService(
|
||||||
accessMock,
|
accessMock,
|
||||||
@ -102,7 +102,7 @@ describe(PersonService.name, () => {
|
|||||||
configMock,
|
configMock,
|
||||||
storageMock,
|
storageMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
smartInfoMock,
|
searchMock,
|
||||||
cryptoMock,
|
cryptoMock,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -752,7 +752,7 @@ describe(PersonService.name, () => {
|
|||||||
it('should create a face with no person and queue recognition job', async () => {
|
it('should create a face with no person and queue recognition job', async () => {
|
||||||
personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
|
personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
|
||||||
machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]);
|
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]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
const face = {
|
const face = {
|
||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
@ -823,7 +823,7 @@ describe(PersonService.name, () => {
|
|||||||
configMock.load.mockResolvedValue([
|
configMock.load.mockResolvedValue([
|
||||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
|
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
|
||||||
]);
|
]);
|
||||||
smartInfoMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
|
personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
|
||||||
|
|
||||||
@ -850,7 +850,7 @@ describe(PersonService.name, () => {
|
|||||||
configMock.load.mockResolvedValue([
|
configMock.load.mockResolvedValue([
|
||||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
|
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
|
||||||
]);
|
]);
|
||||||
smartInfoMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
@ -869,14 +869,14 @@ describe(PersonService.name, () => {
|
|||||||
it('should not queue face with no matches', async () => {
|
it('should not queue face with no matches', async () => {
|
||||||
const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
|
const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
|
||||||
|
|
||||||
smartInfoMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1);
|
expect(searchMock.searchFaces).toHaveBeenCalledTimes(1);
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -890,7 +890,7 @@ describe(PersonService.name, () => {
|
|||||||
configMock.load.mockResolvedValue([
|
configMock.load.mockResolvedValue([
|
||||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
|
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
|
||||||
]);
|
]);
|
||||||
smartInfoMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
@ -900,7 +900,7 @@ describe(PersonService.name, () => {
|
|||||||
name: JobName.FACIAL_RECOGNITION,
|
name: JobName.FACIAL_RECOGNITION,
|
||||||
data: { id: faceStub.noPerson1.id, deferred: true },
|
data: { id: faceStub.noPerson1.id, deferred: true },
|
||||||
});
|
});
|
||||||
expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1);
|
expect(searchMock.searchFaces).toHaveBeenCalledTimes(1);
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -914,14 +914,14 @@ describe(PersonService.name, () => {
|
|||||||
configMock.load.mockResolvedValue([
|
configMock.load.mockResolvedValue([
|
||||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
|
{ 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.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue(personStub.withName);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
||||||
|
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(2);
|
expect(searchMock.searchFaces).toHaveBeenCalledTimes(2);
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
IMediaRepository,
|
IMediaRepository,
|
||||||
IMoveRepository,
|
IMoveRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
JobItem,
|
JobItem,
|
||||||
@ -61,7 +61,7 @@ export class PersonService {
|
|||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository,
|
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
) {
|
) {
|
||||||
this.access = AccessCore.create(accessRepository);
|
this.access = AccessCore.create(accessRepository);
|
||||||
@ -285,15 +285,7 @@ export class PersonService {
|
|||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination, {
|
? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true })
|
||||||
order: 'DESC',
|
|
||||||
withFaces: true,
|
|
||||||
withPeople: false,
|
|
||||||
withSmartInfo: false,
|
|
||||||
withSmartSearch: false,
|
|
||||||
withExif: false,
|
|
||||||
withStacked: false,
|
|
||||||
})
|
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
||||||
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||||
import { Paginated, PaginationOptions } from '../domain.util';
|
import { Paginated, PaginationOptions } from '../domain.util';
|
||||||
@ -11,64 +11,6 @@ export interface AssetStatsOptions {
|
|||||||
isTrashed?: boolean;
|
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 {
|
export interface LivePhotoSearchOptions {
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
livePhotoCID: string;
|
livePhotoCID: string;
|
||||||
@ -204,7 +146,6 @@ export interface IAssetRepository {
|
|||||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||||
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
|
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
|
||||||
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
|
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
|
||||||
search(options: AssetSearchOptions): Promise<AssetEntity[]>;
|
|
||||||
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||||
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||||
searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>;
|
searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>;
|
||||||
|
@ -19,7 +19,6 @@ export * from './person.repository';
|
|||||||
export * from './search.repository';
|
export * from './search.repository';
|
||||||
export * from './server-info.repository';
|
export * from './server-info.repository';
|
||||||
export * from './shared-link.repository';
|
export * from './shared-link.repository';
|
||||||
export * from './smart-info.repository';
|
|
||||||
export * from './storage.repository';
|
export * from './storage.repository';
|
||||||
export * from './system-config.repository';
|
export * from './system-config.repository';
|
||||||
export * from './system-metadata.repository';
|
export * from './system-metadata.repository';
|
||||||
|
@ -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 {
|
export enum SearchStrategy {
|
||||||
SMART = 'SMART',
|
SMART = 'SMART',
|
||||||
@ -54,3 +57,122 @@ export interface SearchExploreItem<T> {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
items: SearchExploreItemSet<T>;
|
items: SearchExploreItemSet<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<AssetSearchOptions, 'orderDirection'>;
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
|
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
|
||||||
|
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||||
|
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
||||||
|
}
|
||||||
|
@ -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<void>;
|
|
||||||
searchCLIP(search: EmbeddingSearch): Promise<AssetEntity[]>;
|
|
||||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
|
||||||
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
|
||||||
}
|
|
@ -1,8 +1,184 @@
|
|||||||
|
import { AssetOrder } from '@app/domain/asset/dto/asset.dto';
|
||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { Transform } from 'class-transformer';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import { Optional, toBoolean } from '../../domain.util';
|
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 {
|
export class SearchDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ -43,6 +219,19 @@ export class SearchDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@Transform(toBoolean)
|
@Transform(toBoolean)
|
||||||
withArchived?: boolean;
|
withArchived?: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@Optional()
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(1000)
|
||||||
|
@Type(() => Number)
|
||||||
|
@Optional()
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SearchPeopleDto {
|
export class SearchPeopleDto {
|
||||||
|
@ -29,6 +29,7 @@ class SearchAssetResponseDto {
|
|||||||
count!: number;
|
count!: number;
|
||||||
items!: AssetResponseDto[];
|
items!: AssetResponseDto[];
|
||||||
facets!: SearchFacetResponseDto[];
|
facets!: SearchFacetResponseDto[];
|
||||||
|
nextPage!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SearchResponseDto {
|
export class SearchResponseDto {
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
newMachineLearningRepositoryMock,
|
newMachineLearningRepositoryMock,
|
||||||
newPartnerRepositoryMock,
|
newPartnerRepositoryMock,
|
||||||
newPersonRepositoryMock,
|
newPersonRepositoryMock,
|
||||||
newSmartInfoRepositoryMock,
|
newSearchRepositoryMock,
|
||||||
newSystemConfigRepositoryMock,
|
newSystemConfigRepositoryMock,
|
||||||
personStub,
|
personStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
IMachineLearningRepository,
|
IMachineLearningRepository,
|
||||||
IPartnerRepository,
|
IPartnerRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { SearchDto } from './dto';
|
import { SearchDto } from './dto';
|
||||||
@ -30,7 +30,7 @@ describe(SearchService.name, () => {
|
|||||||
let configMock: jest.Mocked<ISystemConfigRepository>;
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||||
let machineMock: jest.Mocked<IMachineLearningRepository>;
|
let machineMock: jest.Mocked<IMachineLearningRepository>;
|
||||||
let personMock: jest.Mocked<IPersonRepository>;
|
let personMock: jest.Mocked<IPersonRepository>;
|
||||||
let smartInfoMock: jest.Mocked<ISmartInfoRepository>;
|
let searchMock: jest.Mocked<ISearchRepository>;
|
||||||
let partnerMock: jest.Mocked<IPartnerRepository>;
|
let partnerMock: jest.Mocked<IPartnerRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -38,9 +38,9 @@ describe(SearchService.name, () => {
|
|||||||
configMock = newSystemConfigRepositoryMock();
|
configMock = newSystemConfigRepositoryMock();
|
||||||
machineMock = newMachineLearningRepositoryMock();
|
machineMock = newMachineLearningRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
smartInfoMock = newSmartInfoRepositoryMock();
|
searchMock = newSearchRepositoryMock();
|
||||||
partnerMock = newPartnerRepositoryMock();
|
partnerMock = newPartnerRepositoryMock();
|
||||||
sut = new SearchService(configMock, machineMock, personMock, smartInfoMock, assetMock, partnerMock);
|
sut = new SearchService(configMock, machineMock, personMock, searchMock, assetMock, partnerMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -104,6 +104,7 @@ describe(SearchService.name, () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
items: [mapAsset(assetStub.image)],
|
items: [mapAsset(assetStub.image)],
|
||||||
facets: [],
|
facets: [],
|
||||||
|
nextPage: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,13 +112,13 @@ describe(SearchService.name, () => {
|
|||||||
|
|
||||||
expect(result).toEqual(expectedResponse);
|
expect(result).toEqual(expectedResponse);
|
||||||
expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 });
|
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 () => {
|
it('should search archived photos if `withArchived` option is true', async () => {
|
||||||
const dto: SearchDto = { q: 'test query', clip: true, withArchived: true };
|
const dto: SearchDto = { q: 'test query', clip: true, withArchived: true };
|
||||||
const embedding = [1, 2, 3];
|
const embedding = [1, 2, 3];
|
||||||
smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]);
|
searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
|
||||||
machineMock.encodeText.mockResolvedValueOnce(embedding);
|
machineMock.encodeText.mockResolvedValueOnce(embedding);
|
||||||
partnerMock.getAll.mockResolvedValueOnce([]);
|
partnerMock.getAll.mockResolvedValueOnce([]);
|
||||||
const expectedResponse = {
|
const expectedResponse = {
|
||||||
@ -132,25 +133,28 @@ describe(SearchService.name, () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
items: [mapAsset(assetStub.image)],
|
items: [mapAsset(assetStub.image)],
|
||||||
facets: [],
|
facets: [],
|
||||||
|
nextPage: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await sut.search(authStub.user1, dto);
|
const result = await sut.search(authStub.user1, dto);
|
||||||
|
|
||||||
expect(result).toEqual(expectedResponse);
|
expect(result).toEqual(expectedResponse);
|
||||||
expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({
|
expect(searchMock.searchSmart).toHaveBeenCalledWith(
|
||||||
userIds: [authStub.user1.user.id],
|
{ page: 1, size: 100 },
|
||||||
embedding,
|
{
|
||||||
numResults: 100,
|
userIds: [authStub.user1.user.id],
|
||||||
withArchived: true,
|
embedding,
|
||||||
});
|
withArchived: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
|
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should search by CLIP if `clip` option is true', async () => {
|
it('should search by CLIP if `clip` option is true', async () => {
|
||||||
const dto: SearchDto = { q: 'test query', clip: true };
|
const dto: SearchDto = { q: 'test query', clip: true };
|
||||||
const embedding = [1, 2, 3];
|
const embedding = [1, 2, 3];
|
||||||
smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]);
|
searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
|
||||||
machineMock.encodeText.mockResolvedValueOnce(embedding);
|
machineMock.encodeText.mockResolvedValueOnce(embedding);
|
||||||
partnerMock.getAll.mockResolvedValueOnce([]);
|
partnerMock.getAll.mockResolvedValueOnce([]);
|
||||||
const expectedResponse = {
|
const expectedResponse = {
|
||||||
@ -165,18 +169,21 @@ describe(SearchService.name, () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
items: [mapAsset(assetStub.image)],
|
items: [mapAsset(assetStub.image)],
|
||||||
facets: [],
|
facets: [],
|
||||||
|
nextPage: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await sut.search(authStub.user1, dto);
|
const result = await sut.search(authStub.user1, dto);
|
||||||
|
|
||||||
expect(result).toEqual(expectedResponse);
|
expect(result).toEqual(expectedResponse);
|
||||||
expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({
|
expect(searchMock.searchSmart).toHaveBeenCalledWith(
|
||||||
userIds: [authStub.user1.user.id],
|
{ page: 1, size: 100 },
|
||||||
embedding,
|
{
|
||||||
numResults: 100,
|
userIds: [authStub.user1.user.id],
|
||||||
withArchived: false,
|
embedding,
|
||||||
});
|
withArchived: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
|
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AssetEntity } from '@app/infra/entities';
|
import { AssetEntity } from '@app/infra/entities';
|
||||||
import { ImmichLogger } from '@app/infra/logger';
|
import { ImmichLogger } from '@app/infra/logger';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AssetResponseDto, mapAsset } from '../asset';
|
import { AssetOrder, AssetResponseDto, mapAsset } from '../asset';
|
||||||
import { AuthDto } from '../auth';
|
import { AuthDto } from '../auth';
|
||||||
import { PersonResponseDto } from '../person';
|
import { PersonResponseDto } from '../person';
|
||||||
import {
|
import {
|
||||||
@ -9,13 +9,13 @@ import {
|
|||||||
IMachineLearningRepository,
|
IMachineLearningRepository,
|
||||||
IPartnerRepository,
|
IPartnerRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
SearchExploreItem,
|
SearchExploreItem,
|
||||||
SearchStrategy,
|
SearchStrategy,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { FeatureFlag, SystemConfigCore } from '../system-config';
|
import { FeatureFlag, SystemConfigCore } from '../system-config';
|
||||||
import { SearchDto, SearchPeopleDto } from './dto';
|
import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto';
|
||||||
import { SearchResponseDto } from './response-dto';
|
import { SearchResponseDto } from './response-dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -27,7 +27,7 @@ export class SearchService {
|
|||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||||
@Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository,
|
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
) {
|
) {
|
||||||
@ -55,6 +55,53 @@ export class SearchService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||||
|
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<SearchResponseDto> {
|
||||||
|
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<SearchResponseDto> {
|
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
|
||||||
await this.configCore.requireFeature(FeatureFlag.SEARCH);
|
await this.configCore.requireFeature(FeatureFlag.SEARCH);
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
@ -70,10 +117,10 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userIds = await this.getUserIdsToSearch(auth);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const withArchived = dto.withArchived || false;
|
const page = dto.page ?? 1;
|
||||||
|
|
||||||
|
let nextPage: string | null = null;
|
||||||
let assets: AssetEntity[] = [];
|
let assets: AssetEntity[] = [];
|
||||||
|
|
||||||
switch (strategy) {
|
switch (strategy) {
|
||||||
case SearchStrategy.SMART: {
|
case SearchStrategy.SMART: {
|
||||||
const embedding = await this.machineLearning.encodeText(
|
const embedding = await this.machineLearning.encodeText(
|
||||||
@ -81,36 +128,30 @@ export class SearchService {
|
|||||||
{ text: query },
|
{ text: query },
|
||||||
machineLearning.clip,
|
machineLearning.clip,
|
||||||
);
|
);
|
||||||
assets = await this.smartInfoRepository.searchCLIP({
|
|
||||||
userIds: userIds,
|
const { hasNextPage, items } = await this.searchRepository.searchSmart(
|
||||||
embedding,
|
{ page, size: dto.size || 100 },
|
||||||
numResults: 100,
|
{
|
||||||
withArchived,
|
userIds,
|
||||||
});
|
embedding,
|
||||||
|
withArchived: !!dto.withArchived,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (hasNextPage) {
|
||||||
|
nextPage = (page + 1).toString();
|
||||||
|
}
|
||||||
|
assets = items;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SearchStrategy.TEXT: {
|
case SearchStrategy.TEXT: {
|
||||||
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 });
|
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 });
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return this.mapResponse(assets, nextPage);
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: assets.length,
|
|
||||||
count: assets.length,
|
|
||||||
items: assets.map((asset) => mapAsset(asset)),
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
|
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
|
||||||
@ -122,4 +163,17 @@ export class SearchService {
|
|||||||
userIds.push(...partnersIds);
|
userIds.push(...partnersIds);
|
||||||
return userIds;
|
return userIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async mapResponse(assets: AssetEntity[], nextPage: string | null): Promise<SearchResponseDto> {
|
||||||
|
return {
|
||||||
|
albums: { total: 0, count: 0, items: [], facets: [] },
|
||||||
|
assets: {
|
||||||
|
total: assets.length,
|
||||||
|
count: assets.length,
|
||||||
|
items: assets.map((asset) => mapAsset(asset)),
|
||||||
|
facets: [],
|
||||||
|
nextPage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
newDatabaseRepositoryMock,
|
newDatabaseRepositoryMock,
|
||||||
newJobRepositoryMock,
|
newJobRepositoryMock,
|
||||||
newMachineLearningRepositoryMock,
|
newMachineLearningRepositoryMock,
|
||||||
newSmartInfoRepositoryMock,
|
newSearchRepositoryMock,
|
||||||
newSystemConfigRepositoryMock,
|
newSystemConfigRepositoryMock,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { JobName } from '../job';
|
import { JobName } from '../job';
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
IDatabaseRepository,
|
IDatabaseRepository,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
IMachineLearningRepository,
|
IMachineLearningRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
@ -31,18 +31,18 @@ describe(SmartInfoService.name, () => {
|
|||||||
let assetMock: jest.Mocked<IAssetRepository>;
|
let assetMock: jest.Mocked<IAssetRepository>;
|
||||||
let configMock: jest.Mocked<ISystemConfigRepository>;
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||||
let jobMock: jest.Mocked<IJobRepository>;
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
let smartMock: jest.Mocked<ISmartInfoRepository>;
|
let searchMock: jest.Mocked<ISearchRepository>;
|
||||||
let machineMock: jest.Mocked<IMachineLearningRepository>;
|
let machineMock: jest.Mocked<IMachineLearningRepository>;
|
||||||
let databaseMock: jest.Mocked<IDatabaseRepository>;
|
let databaseMock: jest.Mocked<IDatabaseRepository>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
configMock = newSystemConfigRepositoryMock();
|
configMock = newSystemConfigRepositoryMock();
|
||||||
smartMock = newSmartInfoRepositoryMock();
|
searchMock = newSearchRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
machineMock = newMachineLearningRepositoryMock();
|
machineMock = newMachineLearningRepositoryMock();
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, smartMock, configMock);
|
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock);
|
||||||
|
|
||||||
assetMock.getByIds.mockResolvedValue([asset]);
|
assetMock.getByIds.mockResolvedValue([asset]);
|
||||||
});
|
});
|
||||||
@ -102,12 +102,12 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
await sut.handleEncodeClip({ id: asset.id });
|
await sut.handleEncodeClip({ id: asset.id });
|
||||||
|
|
||||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
expect(searchMock.upsert).not.toHaveBeenCalled();
|
||||||
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save the returned objects', async () => {
|
it('should save the returned objects', async () => {
|
||||||
smartMock.upsert.mockResolvedValue();
|
searchMock.upsert.mockResolvedValue();
|
||||||
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
|
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
|
||||||
|
|
||||||
await sut.handleEncodeClip({ id: asset.id });
|
await sut.handleEncodeClip({ id: asset.id });
|
||||||
@ -117,7 +117,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
{ imagePath: 'path/to/resize.ext' },
|
{ imagePath: 'path/to/resize.ext' },
|
||||||
{ enabled: true, modelName: 'ViT-B-32__openai' },
|
{ enabled: true, modelName: 'ViT-B-32__openai' },
|
||||||
);
|
);
|
||||||
expect(smartMock.upsert).toHaveBeenCalledWith(
|
expect(searchMock.upsert).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
assetId: 'asset-1',
|
assetId: 'asset-1',
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
IDatabaseRepository,
|
IDatabaseRepository,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
IMachineLearningRepository,
|
IMachineLearningRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
@ -24,7 +24,7 @@ export class SmartInfoService {
|
|||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||||
@Inject(ISmartInfoRepository) private repository: ISmartInfoRepository,
|
@Inject(ISearchRepository) private repository: ISearchRepository,
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
) {
|
) {
|
||||||
this.configCore = SystemConfigCore.create(configRepository);
|
this.configCore = SystemConfigCore.create(configRepository);
|
||||||
|
@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger';
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test';
|
import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test';
|
||||||
import { QueueName } from '../job';
|
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 { defaults, SystemConfigValidator } from './system-config.core';
|
||||||
import { SystemConfigService } from './system-config.service';
|
import { SystemConfigService } from './system-config.service';
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
let sut: SystemConfigService;
|
let sut: SystemConfigService;
|
||||||
let configMock: jest.Mocked<ISystemConfigRepository>;
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||||
let communicationMock: jest.Mocked<ICommunicationRepository>;
|
let communicationMock: jest.Mocked<ICommunicationRepository>;
|
||||||
let smartInfoMock: jest.Mocked<ISmartInfoRepository>;
|
let smartInfoMock: jest.Mocked<ISearchRepository>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
delete process.env.IMMICH_CONFIG_FILE;
|
delete process.env.IMMICH_CONFIG_FILE;
|
||||||
|
@ -6,7 +6,7 @@ import _ from 'lodash';
|
|||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
ICommunicationRepository,
|
ICommunicationRepository,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
ServerEvent,
|
ServerEvent,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
@ -32,7 +32,7 @@ export class SystemConfigService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemConfigRepository) private repository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) private repository: ISystemConfigRepository,
|
||||||
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
|
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
|
||||||
@Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository,
|
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||||
) {
|
) {
|
||||||
this.core = SystemConfigCore.create(repository);
|
this.core = SystemConfigCore.create(repository);
|
||||||
this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate());
|
this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate());
|
||||||
|
@ -33,7 +33,9 @@ export class TrashService {
|
|||||||
|
|
||||||
async restore(auth: AuthDto): Promise<void> {
|
async restore(auth: AuthDto): Promise<void> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
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) {
|
for await (const assets of assetPagination) {
|
||||||
@ -44,7 +46,9 @@ export class TrashService {
|
|||||||
|
|
||||||
async empty(auth: AuthDto): Promise<void> {
|
async empty(auth: AuthDto): Promise<void> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
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) {
|
for await (const assets of assetPagination) {
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
AssetJobsDto,
|
AssetJobsDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetSearchDto,
|
|
||||||
AssetService,
|
AssetService,
|
||||||
AssetStatsDto,
|
AssetStatsDto,
|
||||||
AssetStatsResponseDto,
|
AssetStatsResponseDto,
|
||||||
@ -14,7 +13,9 @@ import {
|
|||||||
MapMarkerResponseDto,
|
MapMarkerResponseDto,
|
||||||
MemoryLaneDto,
|
MemoryLaneDto,
|
||||||
MemoryLaneResponseDto,
|
MemoryLaneResponseDto,
|
||||||
|
MetadataSearchDto,
|
||||||
RandomAssetsDto,
|
RandomAssetsDto,
|
||||||
|
SearchService,
|
||||||
TimeBucketAssetDto,
|
TimeBucketAssetDto,
|
||||||
TimeBucketDto,
|
TimeBucketDto,
|
||||||
TimeBucketResponseDto,
|
TimeBucketResponseDto,
|
||||||
@ -23,7 +24,7 @@ import {
|
|||||||
UpdateStackParentDto,
|
UpdateStackParentDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
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 { Auth, Authenticated, SharedLinkRoute } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
import { Route } from '../interceptors';
|
import { Route } from '../interceptors';
|
||||||
@ -34,11 +35,15 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
|
|||||||
@Authenticated()
|
@Authenticated()
|
||||||
@UseValidation()
|
@UseValidation()
|
||||||
export class AssetsController {
|
export class AssetsController {
|
||||||
constructor(private service: AssetService) {}
|
constructor(private searchService: SearchService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
@ApiOperation({ deprecated: true })
|
||||||
return this.service.search(auth, dto);
|
async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> {
|
||||||
|
const {
|
||||||
|
assets: { items },
|
||||||
|
} = await this.searchService.searchMetadata(auth, dto);
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
|
MetadataSearchDto,
|
||||||
PersonResponseDto,
|
PersonResponseDto,
|
||||||
SearchDto,
|
SearchDto,
|
||||||
SearchExploreResponseDto,
|
SearchExploreResponseDto,
|
||||||
SearchPeopleDto,
|
SearchPeopleDto,
|
||||||
SearchResponseDto,
|
SearchResponseDto,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
SmartSearchDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Controller, Get, Query } from '@nestjs/common';
|
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 { Auth, Authenticated } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
|
|
||||||
@ -19,7 +21,18 @@ import { UseValidation } from '../app.utils';
|
|||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private service: SearchService) {}
|
constructor(private service: SearchService) {}
|
||||||
|
|
||||||
|
@Get('metadata')
|
||||||
|
searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||||
|
return this.service.searchMetadata(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('smart')
|
||||||
|
searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
|
return this.service.searchSmart(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@ApiOperation({ deprecated: true })
|
||||||
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
||||||
return this.service.search(auth, dto);
|
return this.service.search(auth, dto);
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@ import {
|
|||||||
IMoveRepository,
|
IMoveRepository,
|
||||||
IPartnerRepository,
|
IPartnerRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
|
ISearchRepository,
|
||||||
IServerInfoRepository,
|
IServerInfoRepository,
|
||||||
ISharedLinkRepository,
|
ISharedLinkRepository,
|
||||||
ISmartInfoRepository,
|
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
ISystemMetadataRepository,
|
ISystemMetadataRepository,
|
||||||
@ -56,9 +56,9 @@ import {
|
|||||||
MoveRepository,
|
MoveRepository,
|
||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
|
SearchRepository,
|
||||||
ServerInfoRepository,
|
ServerInfoRepository,
|
||||||
SharedLinkRepository,
|
SharedLinkRepository,
|
||||||
SmartInfoRepository,
|
|
||||||
SystemConfigRepository,
|
SystemConfigRepository,
|
||||||
SystemMetadataRepository,
|
SystemMetadataRepository,
|
||||||
TagRepository,
|
TagRepository,
|
||||||
@ -86,7 +86,7 @@ const providers: Provider[] = [
|
|||||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository },
|
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||||
{ provide: IStorageRepository, useClass: FilesystemProvider },
|
{ provide: IStorageRepository, useClass: FilesystemProvider },
|
||||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||||
|
@ -1,7 +1,19 @@
|
|||||||
import { Paginated, PaginationOptions } from '@app/domain';
|
import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/domain';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Between, FindManyOptions, LessThanOrEqual, MoreThanOrEqual, ObjectLiteral, Repository } from 'typeorm';
|
import {
|
||||||
import { chunks, setUnion } from '../domain/domain.util';
|
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';
|
import { DATABASE_PARAMETER_CHUNK_SIZE } from './infra.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,9 +30,21 @@ export function OptionalBetween<T>(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<Entity extends ObjectLiteral>(items: Entity[], take: number): PaginationResult<Entity> {
|
||||||
|
const hasNextPage = items.length > take;
|
||||||
|
items.splice(take);
|
||||||
|
|
||||||
|
return { items, hasNextPage };
|
||||||
|
}
|
||||||
|
|
||||||
export async function paginate<Entity extends ObjectLiteral>(
|
export async function paginate<Entity extends ObjectLiteral>(
|
||||||
repository: Repository<Entity>,
|
repository: Repository<Entity>,
|
||||||
paginationOptions: PaginationOptions,
|
{ take, skip }: PaginationOptions,
|
||||||
searchOptions?: FindManyOptions<Entity>,
|
searchOptions?: FindManyOptions<Entity>,
|
||||||
): Paginated<Entity> {
|
): Paginated<Entity> {
|
||||||
const items = await repository.find(
|
const items = await repository.find(
|
||||||
@ -28,27 +52,33 @@ export async function paginate<Entity extends ObjectLiteral>(
|
|||||||
{
|
{
|
||||||
...searchOptions,
|
...searchOptions,
|
||||||
// Take one more item to check if there's a next page
|
// Take one more item to check if there's a next page
|
||||||
take: paginationOptions.take + 1,
|
take: take + 1,
|
||||||
skip: paginationOptions.skip,
|
skip,
|
||||||
},
|
},
|
||||||
_.isUndefined,
|
_.isUndefined,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasNextPage = items.length > paginationOptions.take;
|
return paginationHelper(items, take);
|
||||||
items.splice(paginationOptions.take);
|
}
|
||||||
|
|
||||||
return { items, hasNextPage };
|
export async function paginatedBuilder<Entity extends ObjectLiteral>(
|
||||||
|
qb: SelectQueryBuilder<Entity>,
|
||||||
|
{ take, skip, mode }: PaginatedBuilderOptions,
|
||||||
|
): Paginated<Entity> {
|
||||||
|
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) =>
|
export const asVector = (embedding: number[], quote = false) =>
|
||||||
quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`;
|
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,
|
* 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.
|
* 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 {
|
export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator {
|
||||||
return Chunked({ ...options, mergeFn: setUnion });
|
return Chunked({ ...options, mergeFn: setUnion });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchAssetBuilder(
|
||||||
|
builder: SelectQueryBuilder<AssetEntity>,
|
||||||
|
options: AssetSearchBuilderOptions,
|
||||||
|
): SelectQueryBuilder<AssetEntity> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@ -42,7 +42,7 @@ export class ApiKeyRepository implements IKeyRepository {
|
|||||||
return this.repository.findOne({ where: { userId, id } });
|
return this.repository.findOne({ where: { userId, id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getByUserId(userId: string): Promise<APIKeyEntity[]> {
|
getByUserId(userId: string): Promise<APIKeyEntity[]> {
|
||||||
return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } });
|
return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } });
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
MetadataSearchOptions,
|
MetadataSearchOptions,
|
||||||
MonthDay,
|
MonthDay,
|
||||||
Paginated,
|
Paginated,
|
||||||
|
PaginationMode,
|
||||||
PaginationOptions,
|
PaginationOptions,
|
||||||
SearchExploreItem,
|
SearchExploreItem,
|
||||||
TimeBucketItem,
|
TimeBucketItem,
|
||||||
@ -22,26 +23,21 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import _ from 'lodash';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import {
|
import {
|
||||||
And,
|
|
||||||
Brackets,
|
Brackets,
|
||||||
FindOptionsRelations,
|
FindOptionsRelations,
|
||||||
FindOptionsSelect,
|
FindOptionsSelect,
|
||||||
FindOptionsWhere,
|
FindOptionsWhere,
|
||||||
In,
|
In,
|
||||||
IsNull,
|
IsNull,
|
||||||
LessThan,
|
|
||||||
Not,
|
Not,
|
||||||
Repository,
|
Repository,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities';
|
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities';
|
||||||
import { DummyValue, GenerateSql } from '../infra.util';
|
import { DummyValue, GenerateSql } from '../infra.util';
|
||||||
import { Chunked, ChunkedArray, OptionalBetween, paginate } from '../infra.utils';
|
import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
|
||||||
|
|
||||||
const DEFAULT_SEARCH_SIZE = 250;
|
|
||||||
|
|
||||||
const truncateMap: Record<TimeBucketSize, string> = {
|
const truncateMap: Record<TimeBucketSize, string> = {
|
||||||
[TimeBucketSize.DAY]: 'day',
|
[TimeBucketSize.DAY]: 'day',
|
||||||
@ -70,142 +66,6 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] });
|
await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
search(options: AssetSearchOptions): Promise<AssetEntity[]> {
|
|
||||||
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<AssetEntity> = _.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<AssetEntity> {
|
create(asset: AssetCreate): Promise<AssetEntity> {
|
||||||
return this.repository.save(asset);
|
return this.repository.save(asset);
|
||||||
}
|
}
|
||||||
@ -316,17 +176,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
|
getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
|
||||||
return paginate(this.repository, pagination, {
|
return this.getAll(pagination, { ...options, id: userId });
|
||||||
where: {
|
|
||||||
ownerId: userId,
|
|
||||||
isVisible: options.isVisible,
|
|
||||||
deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined,
|
|
||||||
},
|
|
||||||
relations: {
|
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
withDeleted: !!options.trashedBefore,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
@ -345,24 +195,13 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
|
getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
|
||||||
return paginate(this.repository, pagination, {
|
let builder = this.repository.createQueryBuilder('asset');
|
||||||
where: {
|
builder = searchAssetBuilder(builder, options);
|
||||||
isVisible: options.isVisible,
|
builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC');
|
||||||
type: options.type,
|
return paginatedBuilder<AssetEntity>(builder, {
|
||||||
deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined,
|
mode: PaginationMode.SKIP_TAKE,
|
||||||
},
|
skip: pagination.skip,
|
||||||
relations: {
|
take: pagination.take,
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +274,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
await this.repository.remove(asset);
|
await this.repository.remove(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.BUFFER] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||||
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null> {
|
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null> {
|
||||||
return this.repository.findOne({ where: { ownerId: userId, checksum } });
|
return this.repository.findOne({ where: { ownerId: userId, checksum } });
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@ export * from './metadata.repository';
|
|||||||
export * from './move.repository';
|
export * from './move.repository';
|
||||||
export * from './partner.repository';
|
export * from './partner.repository';
|
||||||
export * from './person.repository';
|
export * from './person.repository';
|
||||||
|
export * from './search.repository';
|
||||||
export * from './server-info.repository';
|
export * from './server-info.repository';
|
||||||
export * from './shared-link.repository';
|
export * from './shared-link.repository';
|
||||||
export * from './smart-info.repository';
|
|
||||||
export * from './system-config.repository';
|
export * from './system-config.repository';
|
||||||
export * from './system-metadata.repository';
|
export * from './system-metadata.repository';
|
||||||
export * from './tag.repository';
|
export * from './tag.repository';
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
|
AssetSearchOptions,
|
||||||
DatabaseExtension,
|
DatabaseExtension,
|
||||||
Embedding,
|
Embedding,
|
||||||
EmbeddingSearch,
|
|
||||||
FaceEmbeddingSearch,
|
FaceEmbeddingSearch,
|
||||||
FaceSearchResult,
|
FaceSearchResult,
|
||||||
ISmartInfoRepository,
|
ISearchRepository,
|
||||||
|
Paginated,
|
||||||
|
PaginationMode,
|
||||||
|
PaginationResult,
|
||||||
|
SearchPaginationOptions,
|
||||||
|
SmartSearchOptions,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
|
import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
|
||||||
import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities';
|
import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities';
|
||||||
@ -14,11 +19,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { vectorExt } from '../database.config';
|
import { vectorExt } from '../database.config';
|
||||||
import { DummyValue, GenerateSql } from '../infra.util';
|
import { DummyValue, GenerateSql } from '../infra.util';
|
||||||
import { asVector, isValidInteger } from '../infra.utils';
|
import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SmartInfoRepository implements ISmartInfoRepository {
|
export class SearchRepository implements ISearchRepository {
|
||||||
private logger = new ImmichLogger(SmartInfoRepository.name);
|
private logger = new ImmichLogger(SearchRepository.name);
|
||||||
private faceColumns: string[];
|
private faceColumns: string[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -35,48 +40,74 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||||||
|
|
||||||
async init(modelName: string): Promise<void> {
|
async init(modelName: string): Promise<void> {
|
||||||
const { dimSize } = getCLIPModelInfo(modelName);
|
const { dimSize } = getCLIPModelInfo(modelName);
|
||||||
if (dimSize == null) {
|
const curDimSize = await this.getDimSize();
|
||||||
throw new Error(`Invalid CLIP model name: ${modelName}`);
|
this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`);
|
||||||
}
|
|
||||||
|
|
||||||
const currentDimSize = await this.getDimSize();
|
if (dimSize != curDimSize) {
|
||||||
this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`);
|
this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`);
|
||||||
|
|
||||||
if (dimSize != currentDimSize) {
|
|
||||||
this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`);
|
|
||||||
await this.updateDimSize(dimSize);
|
await this.updateDimSize(dimSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@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<AssetEntity[]> {
|
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
||||||
if (!isValidInteger(numResults, { min: 1 })) {
|
let builder = this.assetRepository.createQueryBuilder('asset');
|
||||||
throw new Error(`Invalid value for 'numResults': ${numResults}`);
|
builder = searchAssetBuilder(builder, options);
|
||||||
}
|
|
||||||
|
|
||||||
// setting this too low messes with prefilter recall
|
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||||
numResults = Math.max(numResults, 64);
|
|
||||||
|
return paginatedBuilder<AssetEntity>(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<AssetEntity> {
|
||||||
|
let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
|
||||||
|
|
||||||
let results: AssetEntity[] = [];
|
|
||||||
await this.assetRepository.manager.transaction(async (manager) => {
|
await this.assetRepository.manager.transaction(async (manager) => {
|
||||||
const query = manager
|
let builder = manager.createQueryBuilder(AssetEntity, 'asset');
|
||||||
.createQueryBuilder(AssetEntity, 'a')
|
builder = searchAssetBuilder(builder, options);
|
||||||
.innerJoin('a.smartSearch', 's')
|
builder
|
||||||
.leftJoinAndSelect('a.exifInfo', 'e')
|
.innerJoin('asset.smartSearch', 'search')
|
||||||
.where('a.ownerId IN (:...userIds )')
|
.andWhere('asset.ownerId IN (:...userIds )')
|
||||||
.orderBy('s.embedding <=> :embedding')
|
.orderBy('search.embedding <=> :embedding')
|
||||||
.setParameters({ userIds, embedding: asVector(embedding) });
|
.setParameters({ userIds, embedding: asVector(embedding) });
|
||||||
|
|
||||||
if (!withArchived) {
|
await manager.query(this.getRuntimeConfig(pagination.size));
|
||||||
query.andWhere('a.isArchived = false');
|
results = await paginatedBuilder<AssetEntity>(builder, {
|
||||||
}
|
mode: PaginationMode.LIMIT_OFFSET,
|
||||||
query.andWhere('a.isVisible = true').andWhere('a.fileCreatedAt < NOW()');
|
skip: (pagination.page - 1) * pagination.size,
|
||||||
query.limit(numResults);
|
take: pagination.size,
|
||||||
|
});
|
||||||
await manager.query(this.getRuntimeConfig(numResults));
|
|
||||||
results = await query.getMany();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@ -135,7 +166,6 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||||||
.where('res.distance <= :maxDistance', { maxDistance })
|
.where('res.distance <= :maxDistance', { maxDistance })
|
||||||
.getRawMany();
|
.getRawMany();
|
||||||
});
|
});
|
||||||
|
|
||||||
return results.map((row) => ({
|
return results.map((row) => ({
|
||||||
face: this.assetFaceRepository.create(row),
|
face: this.assetFaceRepository.create(row),
|
||||||
distance: row.distance,
|
distance: row.distance,
|
||||||
@ -163,17 +193,14 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||||||
throw new Error(`Invalid CLIP dimension size: ${dimSize}`);
|
throw new Error(`Invalid CLIP dimension size: ${dimSize}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDimSize = await this.getDimSize();
|
const curDimSize = await this.getDimSize();
|
||||||
if (currentDimSize === dimSize) {
|
if (curDimSize === dimSize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
|
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
|
||||||
|
|
||||||
await this.smartSearchRepository.manager.transaction(async (manager) => {
|
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(`DROP TABLE smart_search`);
|
||||||
|
|
||||||
await manager.query(`
|
await manager.query(`
|
||||||
@ -182,12 +209,15 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||||||
embedding vector(${dimSize}) NOT NULL )`);
|
embedding vector(${dimSize}) NOT NULL )`);
|
||||||
|
|
||||||
await manager.query(`
|
await manager.query(`
|
||||||
CREATE INDEX IF NOT EXISTS clip_index ON smart_search
|
CREATE INDEX clip_index ON smart_search
|
||||||
USING hnsw (embedding vector_cosine_ops)
|
USING vectors (embedding vector_cos_ops) WITH (options = $$
|
||||||
WITH (ef_construction = 300, m = 16)`);
|
[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<number> {
|
private async getDimSize(): Promise<number> {
|
@ -19,8 +19,8 @@ import {
|
|||||||
MoveRepository,
|
MoveRepository,
|
||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
|
SearchRepository,
|
||||||
SharedLinkRepository,
|
SharedLinkRepository,
|
||||||
SmartInfoRepository,
|
|
||||||
SystemConfigRepository,
|
SystemConfigRepository,
|
||||||
SystemMetadataRepository,
|
SystemMetadataRepository,
|
||||||
TagRepository,
|
TagRepository,
|
||||||
@ -41,7 +41,7 @@ const repositories = [
|
|||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
SharedLinkRepository,
|
SharedLinkRepository,
|
||||||
SmartInfoRepository,
|
SearchRepository,
|
||||||
SystemConfigRepository,
|
SystemConfigRepository,
|
||||||
SystemMetadataRepository,
|
SystemMetadataRepository,
|
||||||
TagRepository,
|
TagRepository,
|
||||||
@ -142,7 +142,7 @@ class SqlGenerator {
|
|||||||
this.sqlLogger.clear();
|
this.sqlLogger.clear();
|
||||||
|
|
||||||
// errors still generate sql, which is all we care about
|
// 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) {
|
if (this.sqlLogger.queries.length === 0) {
|
||||||
console.warn(`No queries recorded for ${queryLabel}`);
|
console.warn(`No queries recorded for ${queryLabel}`);
|
||||||
|
234
server/src/infra/sql/search.repository.sql
Normal file
234
server/src/infra/sql/search.repository.sql
Normal file
@ -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
|
@ -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
|
|
@ -32,7 +32,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
|||||||
getTimeBuckets: jest.fn(),
|
getTimeBuckets: jest.fn(),
|
||||||
restoreAll: jest.fn(),
|
restoreAll: jest.fn(),
|
||||||
softDeleteAll: jest.fn(),
|
softDeleteAll: jest.fn(),
|
||||||
search: jest.fn(),
|
|
||||||
getAssetIdByCity: jest.fn(),
|
getAssetIdByCity: jest.fn(),
|
||||||
getAssetIdByTag: jest.fn(),
|
getAssetIdByTag: jest.fn(),
|
||||||
searchMetadata: jest.fn(),
|
searchMetadata: jest.fn(),
|
||||||
|
@ -15,8 +15,8 @@ export * from './metadata.repository.mock';
|
|||||||
export * from './move.repository.mock';
|
export * from './move.repository.mock';
|
||||||
export * from './partner.repository.mock';
|
export * from './partner.repository.mock';
|
||||||
export * from './person.repository.mock';
|
export * from './person.repository.mock';
|
||||||
|
export * from './search.repository.mock';
|
||||||
export * from './shared-link.repository.mock';
|
export * from './shared-link.repository.mock';
|
||||||
export * from './smart-info.repository.mock';
|
|
||||||
export * from './storage.repository.mock';
|
export * from './storage.repository.mock';
|
||||||
export * from './system-config.repository.mock';
|
export * from './system-config.repository.mock';
|
||||||
export * from './system-info.repository.mock';
|
export * from './system-info.repository.mock';
|
||||||
|
11
server/test/repositories/search.repository.mock.ts
Normal file
11
server/test/repositories/search.repository.mock.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ISearchRepository } from '@app/domain';
|
||||||
|
|
||||||
|
export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
|
||||||
|
return {
|
||||||
|
init: jest.fn(),
|
||||||
|
searchMetadata: jest.fn(),
|
||||||
|
searchSmart: jest.fn(),
|
||||||
|
searchFaces: jest.fn(),
|
||||||
|
upsert: jest.fn(),
|
||||||
|
};
|
||||||
|
};
|
@ -1,10 +0,0 @@
|
|||||||
import { ISmartInfoRepository } from '@app/domain';
|
|
||||||
|
|
||||||
export const newSmartInfoRepositoryMock = (): jest.Mocked<ISmartInfoRepository> => {
|
|
||||||
return {
|
|
||||||
init: jest.fn(),
|
|
||||||
searchCLIP: jest.fn(),
|
|
||||||
searchFaces: jest.fn(),
|
|
||||||
upsert: jest.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
@ -9,7 +9,7 @@
|
|||||||
export let right = 0;
|
export let right = 0;
|
||||||
export let root: HTMLElement | null = null;
|
export let root: HTMLElement | null = null;
|
||||||
|
|
||||||
let intersecting = false;
|
export let intersecting = false;
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
hidden: HTMLDivElement;
|
hidden: HTMLDivElement;
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
export let showStackedIcon = true;
|
export let showStackedIcon = true;
|
||||||
|
export let intersecting = false;
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
@ -85,7 +86,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IntersectionObserver once={false} let:intersecting>
|
<IntersectionObserver once={false} on:intersected bind:intersecting>
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
style:width="{width}px"
|
style:width="{width}px"
|
||||||
@ -95,8 +96,8 @@
|
|||||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||||
class:cursor-not-allowed={disabled}
|
class:cursor-not-allowed={disabled}
|
||||||
class:hover:cursor-pointer={!disabled}
|
class:hover:cursor-pointer={!disabled}
|
||||||
on:mouseenter={() => onMouseEnter()}
|
on:mouseenter={onMouseEnter}
|
||||||
on:mouseleave={() => onMouseLeave()}
|
on:mouseleave={onMouseLeave}
|
||||||
on:click={thumbnailClickedHandler}
|
on:click={thumbnailClickedHandler}
|
||||||
on:keydown={thumbnailKeyDownHandler}
|
on:keydown={thumbnailKeyDownHandler}
|
||||||
>
|
>
|
||||||
|
@ -8,6 +8,10 @@
|
|||||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { onDestroy } from 'svelte';
|
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 assets: AssetResponseDto[];
|
||||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||||
@ -18,7 +22,6 @@
|
|||||||
|
|
||||||
let selectedAsset: AssetResponseDto;
|
let selectedAsset: AssetResponseDto;
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
|
|
||||||
let viewWidth: number;
|
let viewWidth: number;
|
||||||
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
|
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
|
||||||
|
|
||||||
@ -88,7 +91,7 @@
|
|||||||
|
|
||||||
{#if assets.length > 0}
|
{#if assets.length > 0}
|
||||||
<div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}>
|
<div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}>
|
||||||
{#each assets as asset (asset.id)}
|
{#each assets as asset, i (asset.id)}
|
||||||
<div animate:flip={{ duration: 500 }}>
|
<div animate:flip={{ duration: 500 }}>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
{asset}
|
{asset}
|
||||||
@ -97,6 +100,8 @@
|
|||||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||||
on:select={selectAssetHandler}
|
on:select={selectAssetHandler}
|
||||||
|
on:intersected={(event) =>
|
||||||
|
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||||
selected={selectedAssets.has(asset)}
|
selected={selectedAssets.has(asset)}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
/>
|
/>
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
const parameters = new URLSearchParams({
|
const parameters = new URLSearchParams({
|
||||||
q: searchValue,
|
q: searchValue,
|
||||||
smart: smartSearch,
|
smart: smartSearch,
|
||||||
|
take: '100',
|
||||||
});
|
});
|
||||||
|
|
||||||
showHistory = false;
|
showHistory = false;
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.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 SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import type { AssetResponseDto } from '@api';
|
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
@ -27,15 +26,20 @@
|
|||||||
import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
|
import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
|
||||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
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;
|
export let data: PageData;
|
||||||
|
|
||||||
|
const MAX_ASSET_COUNT = 5000;
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
||||||
// The GalleryViewer pushes it's own history state, which causes weird
|
// The GalleryViewer pushes it's own history state, which causes weird
|
||||||
// behavior for history.back(). To prevent that we store the previous page
|
// behavior for history.back(). To prevent that we store the previous page
|
||||||
// manually and navigate back to that.
|
// manually and navigate back to that.
|
||||||
let previousRoute = AppRoute.EXPLORE as string;
|
let previousRoute = AppRoute.EXPLORE as string;
|
||||||
|
$: curPage = data.results?.assets.nextPage;
|
||||||
$: albums = data.results?.albums.items;
|
$: albums = data.results?.albums.items;
|
||||||
|
|
||||||
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
||||||
@ -107,6 +111,33 @@
|
|||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
selectedAssets = new Set(searchResultAssets);
|
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;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@ -164,7 +195,12 @@
|
|||||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||||
{#if searchResultAssets && searchResultAssets.length > 0}
|
{#if searchResultAssets && searchResultAssets.length > 0}
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
<GalleryViewer assets={searchResultAssets} bind:selectedAssets showArchiveIcon={true} />
|
<GalleryViewer
|
||||||
|
assets={searchResultAssets}
|
||||||
|
bind:selectedAssets
|
||||||
|
on:intersected={loadNextPage}
|
||||||
|
showArchiveIcon={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
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 type { PageLoad } from './$types';
|
||||||
import { QueryParameter } from '$lib/constants';
|
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;
|
url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined;
|
||||||
let results: SearchResponseDto | null = null;
|
let results: SearchResponseDto | null = null;
|
||||||
if (term) {
|
if (term) {
|
||||||
const { data } = await api.searchApi.search({}, { params: url.searchParams });
|
const res = await api.searchApi.search({}, { params: data.url.searchParams });
|
||||||
results = data;
|
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 {
|
return {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user