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