mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(web,server)!: configure machine learning via the UI (#3768)
This commit is contained in:
		
							parent
							
								
									2cccef174a
								
							
						
					
					
						commit
						8211afb726
					
				
							
								
								
									
										141
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										141
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'total': number;
 | 
					    'total': number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * @export
 | 
					 | 
				
			||||||
 * @interface SearchConfigResponseDto
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export interface SearchConfigResponseDto {
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {boolean}
 | 
					 | 
				
			||||||
     * @memberof SearchConfigResponseDto
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'enabled': boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto {
 | 
				
			|||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
     * @memberof ServerFeaturesDto
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'machineLearning': boolean;
 | 
					    'clipEncode': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'facialRecognition': boolean;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto {
 | 
				
			|||||||
     * @memberof ServerFeaturesDto
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'search': boolean;
 | 
					    'search': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'sidecar': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'tagImage': boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
@ -2611,6 +2616,12 @@ export interface SystemConfigDto {
 | 
				
			|||||||
     * @memberof SystemConfigDto
 | 
					     * @memberof SystemConfigDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'job': SystemConfigJobDto;
 | 
					    'job': SystemConfigJobDto;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {SystemConfigMachineLearningDto}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'machineLearning': SystemConfigMachineLearningDto;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {SystemConfigOAuthDto}
 | 
					     * @type {SystemConfigOAuthDto}
 | 
				
			||||||
@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'videoConversion': JobSettingsDto;
 | 
					    'videoConversion': JobSettingsDto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SystemConfigMachineLearningDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'clipEncodeEnabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'enabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'facialRecognitionEnabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'tagImageEnabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'url': string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
                url: toPathString(localVarUrlObj),
 | 
					 | 
				
			||||||
                options: localVarRequestOptions,
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					 | 
				
			||||||
            const localVarPath = `/search/config`;
 | 
					 | 
				
			||||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
					 | 
				
			||||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
					 | 
				
			||||||
            let baseOptions;
 | 
					 | 
				
			||||||
            if (configuration) {
 | 
					 | 
				
			||||||
                baseOptions = configuration.baseOptions;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
					 | 
				
			||||||
            const localVarHeaderParameter = {} as any;
 | 
					 | 
				
			||||||
            const localVarQueryParameter = {} as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication cookie required
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication api_key required
 | 
					 | 
				
			||||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication bearer required
 | 
					 | 
				
			||||||
            // http bearer authentication required
 | 
					 | 
				
			||||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> {
 | 
					 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options);
 | 
					 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {string} [q] 
 | 
					         * @param {string} [q] 
 | 
				
			||||||
@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
 | 
				
			|||||||
        getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
 | 
					        getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
 | 
				
			||||||
            return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> {
 | 
					 | 
				
			||||||
            return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath));
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
					         * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
				
			||||||
@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI {
 | 
				
			|||||||
        return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
 | 
					        return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
     * @throws {RequiredError}
 | 
					 | 
				
			||||||
     * @memberof SearchApi
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public getSearchConfig(options?: AxiosRequestConfig) {
 | 
					 | 
				
			||||||
        return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
					     * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,7 @@ This often happens when using a reverse proxy or cloudflare tunnel in front of I
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Why is Immich slow on low-memory systems like the Raspberry Pi?
 | 
					### Why is Immich slow on low-memory systems like the Raspberry Pi?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file.
 | 
					Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_ENABLED=false` in your .env file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### How to disable machine-learning and TypeSense?
 | 
					### How to disable machine-learning and TypeSense?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -47,7 +47,7 @@ Immich uses optional machine-learning features to enhance search results. This f
 | 
				
			|||||||
Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning.
 | 
					Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning.
 | 
				
			||||||
:::
 | 
					:::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_URL=false` & `TYPESENSE_ENABLED=false` in your .env file.
 | 
					These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_ENABLED=false` & `TYPESENSE_ENABLED=false` in your .env file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
 | 
					### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -132,7 +132,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
IMMICH_WEB_URL=http://immich-web:3000
 | 
					IMMICH_WEB_URL=http://immich-web:3000
 | 
				
			||||||
IMMICH_SERVER_URL=http://immich-server:3001
 | 
					IMMICH_SERVER_URL=http://immich-server:3001
 | 
				
			||||||
IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
####################################################################################
 | 
					####################################################################################
 | 
				
			||||||
# Alternative API's External Address - Optional
 | 
					# Alternative API's External Address - Optional
 | 
				
			||||||
 | 
				
			|||||||
@ -51,10 +51,11 @@ These environment variables are used by the `docker-compose.yml` file and do **N
 | 
				
			|||||||
## URLs
 | 
					## URLs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Variable                          | Description                  |                Default                | Services              |
 | 
					| Variable                          | Description                  |                Default                | Services              |
 | 
				
			||||||
| :---------------------------- | :------------------------------------------------------- | :-----------------------------------: | :-------------------- |
 | 
					| :-------------------------------- | :--------------------------- | :-----------------------------------: | :-------------------- |
 | 
				
			||||||
| `IMMICH_WEB_URL`                  | Immich Web URL               |       `http://immich-web:3000`        | proxy                 |
 | 
					| `IMMICH_WEB_URL`                  | Immich Web URL               |       `http://immich-web:3000`        | proxy                 |
 | 
				
			||||||
| `IMMICH_SERVER_URL`               | Immich Server URL            |      `http://immich-server:3001`      | web, proxy            |
 | 
					| `IMMICH_SERVER_URL`               | Immich Server URL            |      `http://immich-server:3001`      | web, proxy            |
 | 
				
			||||||
| `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, set `"false"` to disable ML | `http://immich-machine-learning:3003` | server, microservices |
 | 
					| `IMMICH_MACHINE_LEARNING_ENABLED` | Enabled machine learning     |                `true`                 | server, microservices |
 | 
				
			||||||
 | 
					| `IMMICH_MACHINE_LEARNING_URL`     | Immich Machine Learning URL, | `http://immich-machine-learning:3003` | server, microservices |
 | 
				
			||||||
| `PUBLIC_IMMICH_SERVER_URL`        | Public Immich URL            |      `http://immich-server:3001`      | web                   |
 | 
					| `PUBLIC_IMMICH_SERVER_URL`        | Public Immich URL            |      `http://immich-server:3001`      | web                   |
 | 
				
			||||||
| `IMMICH_API_URL_EXTERNAL`         | Immich API URL External      |                `/api`                 | web                   |
 | 
					| `IMMICH_API_URL_EXTERNAL`         | Immich API URL External      |                `/api`                 | web                   |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							@ -84,7 +84,6 @@ doc/SearchAlbumResponseDto.md
 | 
				
			|||||||
doc/SearchApi.md
 | 
					doc/SearchApi.md
 | 
				
			||||||
doc/SearchAssetDto.md
 | 
					doc/SearchAssetDto.md
 | 
				
			||||||
doc/SearchAssetResponseDto.md
 | 
					doc/SearchAssetResponseDto.md
 | 
				
			||||||
doc/SearchConfigResponseDto.md
 | 
					 | 
				
			||||||
doc/SearchExploreItem.md
 | 
					doc/SearchExploreItem.md
 | 
				
			||||||
doc/SearchExploreResponseDto.md
 | 
					doc/SearchExploreResponseDto.md
 | 
				
			||||||
doc/SearchFacetCountResponseDto.md
 | 
					doc/SearchFacetCountResponseDto.md
 | 
				
			||||||
@ -108,6 +107,7 @@ doc/SystemConfigApi.md
 | 
				
			|||||||
doc/SystemConfigDto.md
 | 
					doc/SystemConfigDto.md
 | 
				
			||||||
doc/SystemConfigFFmpegDto.md
 | 
					doc/SystemConfigFFmpegDto.md
 | 
				
			||||||
doc/SystemConfigJobDto.md
 | 
					doc/SystemConfigJobDto.md
 | 
				
			||||||
 | 
					doc/SystemConfigMachineLearningDto.md
 | 
				
			||||||
doc/SystemConfigOAuthDto.md
 | 
					doc/SystemConfigOAuthDto.md
 | 
				
			||||||
doc/SystemConfigPasswordLoginDto.md
 | 
					doc/SystemConfigPasswordLoginDto.md
 | 
				
			||||||
doc/SystemConfigStorageTemplateDto.md
 | 
					doc/SystemConfigStorageTemplateDto.md
 | 
				
			||||||
@ -228,7 +228,6 @@ lib/model/queue_status_dto.dart
 | 
				
			|||||||
lib/model/search_album_response_dto.dart
 | 
					lib/model/search_album_response_dto.dart
 | 
				
			||||||
lib/model/search_asset_dto.dart
 | 
					lib/model/search_asset_dto.dart
 | 
				
			||||||
lib/model/search_asset_response_dto.dart
 | 
					lib/model/search_asset_response_dto.dart
 | 
				
			||||||
lib/model/search_config_response_dto.dart
 | 
					 | 
				
			||||||
lib/model/search_explore_item.dart
 | 
					lib/model/search_explore_item.dart
 | 
				
			||||||
lib/model/search_explore_response_dto.dart
 | 
					lib/model/search_explore_response_dto.dart
 | 
				
			||||||
lib/model/search_facet_count_response_dto.dart
 | 
					lib/model/search_facet_count_response_dto.dart
 | 
				
			||||||
@ -249,6 +248,7 @@ lib/model/smart_info_response_dto.dart
 | 
				
			|||||||
lib/model/system_config_dto.dart
 | 
					lib/model/system_config_dto.dart
 | 
				
			||||||
lib/model/system_config_f_fmpeg_dto.dart
 | 
					lib/model/system_config_f_fmpeg_dto.dart
 | 
				
			||||||
lib/model/system_config_job_dto.dart
 | 
					lib/model/system_config_job_dto.dart
 | 
				
			||||||
 | 
					lib/model/system_config_machine_learning_dto.dart
 | 
				
			||||||
lib/model/system_config_o_auth_dto.dart
 | 
					lib/model/system_config_o_auth_dto.dart
 | 
				
			||||||
lib/model/system_config_password_login_dto.dart
 | 
					lib/model/system_config_password_login_dto.dart
 | 
				
			||||||
lib/model/system_config_storage_template_dto.dart
 | 
					lib/model/system_config_storage_template_dto.dart
 | 
				
			||||||
@ -353,7 +353,6 @@ test/search_album_response_dto_test.dart
 | 
				
			|||||||
test/search_api_test.dart
 | 
					test/search_api_test.dart
 | 
				
			||||||
test/search_asset_dto_test.dart
 | 
					test/search_asset_dto_test.dart
 | 
				
			||||||
test/search_asset_response_dto_test.dart
 | 
					test/search_asset_response_dto_test.dart
 | 
				
			||||||
test/search_config_response_dto_test.dart
 | 
					 | 
				
			||||||
test/search_explore_item_test.dart
 | 
					test/search_explore_item_test.dart
 | 
				
			||||||
test/search_explore_response_dto_test.dart
 | 
					test/search_explore_response_dto_test.dart
 | 
				
			||||||
test/search_facet_count_response_dto_test.dart
 | 
					test/search_facet_count_response_dto_test.dart
 | 
				
			||||||
@ -377,6 +376,7 @@ test/system_config_api_test.dart
 | 
				
			|||||||
test/system_config_dto_test.dart
 | 
					test/system_config_dto_test.dart
 | 
				
			||||||
test/system_config_f_fmpeg_dto_test.dart
 | 
					test/system_config_f_fmpeg_dto_test.dart
 | 
				
			||||||
test/system_config_job_dto_test.dart
 | 
					test/system_config_job_dto_test.dart
 | 
				
			||||||
 | 
					test/system_config_machine_learning_dto_test.dart
 | 
				
			||||||
test/system_config_o_auth_dto_test.dart
 | 
					test/system_config_o_auth_dto_test.dart
 | 
				
			||||||
test/system_config_password_login_dto_test.dart
 | 
					test/system_config_password_login_dto_test.dart
 | 
				
			||||||
test/system_config_storage_template_dto_test.dart
 | 
					test/system_config_storage_template_dto_test.dart
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							@ -140,7 +140,6 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | 
 | 
					*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | 
 | 
				
			||||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | 
 | 
					*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | 
 | 
				
			||||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | 
 | 
					*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | 
 | 
				
			||||||
*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
					 | 
				
			||||||
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
 | 
					*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
 | 
				
			||||||
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | 
 | 
					*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | 
 | 
				
			||||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
					*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
				
			||||||
@ -253,7 +252,6 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
 | 
					 - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
 | 
				
			||||||
 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
					 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
				
			||||||
 - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
 | 
					 - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
 | 
				
			||||||
 - [SearchConfigResponseDto](doc//SearchConfigResponseDto.md)
 | 
					 | 
				
			||||||
 - [SearchExploreItem](doc//SearchExploreItem.md)
 | 
					 - [SearchExploreItem](doc//SearchExploreItem.md)
 | 
				
			||||||
 - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md)
 | 
					 - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md)
 | 
				
			||||||
 - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
 | 
					 - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
 | 
				
			||||||
@ -274,6 +272,7 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [SystemConfigDto](doc//SystemConfigDto.md)
 | 
					 - [SystemConfigDto](doc//SystemConfigDto.md)
 | 
				
			||||||
 - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
 | 
					 - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
 | 
				
			||||||
 - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
 | 
					 - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
 | 
				
			||||||
 | 
					 - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
 | 
				
			||||||
 - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
 | 
					 - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
 | 
				
			||||||
 - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
 | 
					 - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
 | 
				
			||||||
 - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
 | 
					 - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										52
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
								
							@ -10,7 +10,6 @@ All URIs are relative to */api*
 | 
				
			|||||||
Method | HTTP request | Description
 | 
					Method | HTTP request | Description
 | 
				
			||||||
------------- | ------------- | -------------
 | 
					------------- | ------------- | -------------
 | 
				
			||||||
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | 
 | 
					[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | 
 | 
				
			||||||
[**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
					 | 
				
			||||||
[**search**](SearchApi.md#search) | **GET** /search | 
 | 
					[**search**](SearchApi.md#search) | **GET** /search | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,57 +64,6 @@ This endpoint does not need any parameter.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **getSearchConfig**
 | 
					 | 
				
			||||||
> SearchConfigResponseDto getSearchConfig()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 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();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
try {
 | 
					 | 
				
			||||||
    final result = api_instance.getSearchConfig();
 | 
					 | 
				
			||||||
    print(result);
 | 
					 | 
				
			||||||
} catch (e) {
 | 
					 | 
				
			||||||
    print('Exception when calling SearchApi->getSearchConfig: $e\n');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Parameters
 | 
					 | 
				
			||||||
This endpoint does not need any parameter.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Return type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[**SearchConfigResponseDto**](SearchConfigResponseDto.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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# **search**
 | 
					# **search**
 | 
				
			||||||
> SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
 | 
					> SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/doc/ServerFeaturesDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/doc/ServerFeaturesDto.md
									
									
									
										generated
									
									
									
								
							@ -8,11 +8,14 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
**machineLearning** | **bool** |  | 
 | 
					**clipEncode** | **bool** |  | 
 | 
				
			||||||
 | 
					**facialRecognition** | **bool** |  | 
 | 
				
			||||||
**oauth** | **bool** |  | 
 | 
					**oauth** | **bool** |  | 
 | 
				
			||||||
**oauthAutoLaunch** | **bool** |  | 
 | 
					**oauthAutoLaunch** | **bool** |  | 
 | 
				
			||||||
**passwordLogin** | **bool** |  | 
 | 
					**passwordLogin** | **bool** |  | 
 | 
				
			||||||
**search** | **bool** |  | 
 | 
					**search** | **bool** |  | 
 | 
				
			||||||
 | 
					**sidecar** | **bool** |  | 
 | 
				
			||||||
 | 
					**tagImage** | **bool** |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							@ -10,6 +10,7 @@ Name | Type | Description | Notes
 | 
				
			|||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  | 
 | 
					**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  | 
 | 
				
			||||||
**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  | 
 | 
					**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  | 
 | 
				
			||||||
 | 
					**machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) |  | 
 | 
				
			||||||
**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  | 
 | 
					**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  | 
 | 
				
			||||||
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  | 
 | 
					**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  | 
 | 
				
			||||||
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  | 
 | 
					**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  | 
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
# openapi.model.SearchConfigResponseDto
 | 
					# openapi.model.SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Load the model package
 | 
					## Load the model package
 | 
				
			||||||
```dart
 | 
					```dart
 | 
				
			||||||
@ -8,7 +8,11 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**clipEncodeEnabled** | **bool** |  | 
 | 
				
			||||||
**enabled** | **bool** |  | 
 | 
					**enabled** | **bool** |  | 
 | 
				
			||||||
 | 
					**facialRecognitionEnabled** | **bool** |  | 
 | 
				
			||||||
 | 
					**tagImageEnabled** | **bool** |  | 
 | 
				
			||||||
 | 
					**url** | **String** |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							@ -115,7 +115,6 @@ part 'model/queue_status_dto.dart';
 | 
				
			|||||||
part 'model/search_album_response_dto.dart';
 | 
					part 'model/search_album_response_dto.dart';
 | 
				
			||||||
part 'model/search_asset_dto.dart';
 | 
					part 'model/search_asset_dto.dart';
 | 
				
			||||||
part 'model/search_asset_response_dto.dart';
 | 
					part 'model/search_asset_response_dto.dart';
 | 
				
			||||||
part 'model/search_config_response_dto.dart';
 | 
					 | 
				
			||||||
part 'model/search_explore_item.dart';
 | 
					part 'model/search_explore_item.dart';
 | 
				
			||||||
part 'model/search_explore_response_dto.dart';
 | 
					part 'model/search_explore_response_dto.dart';
 | 
				
			||||||
part 'model/search_facet_count_response_dto.dart';
 | 
					part 'model/search_facet_count_response_dto.dart';
 | 
				
			||||||
@ -136,6 +135,7 @@ part 'model/smart_info_response_dto.dart';
 | 
				
			|||||||
part 'model/system_config_dto.dart';
 | 
					part 'model/system_config_dto.dart';
 | 
				
			||||||
part 'model/system_config_f_fmpeg_dto.dart';
 | 
					part 'model/system_config_f_fmpeg_dto.dart';
 | 
				
			||||||
part 'model/system_config_job_dto.dart';
 | 
					part 'model/system_config_job_dto.dart';
 | 
				
			||||||
 | 
					part 'model/system_config_machine_learning_dto.dart';
 | 
				
			||||||
part 'model/system_config_o_auth_dto.dart';
 | 
					part 'model/system_config_o_auth_dto.dart';
 | 
				
			||||||
part 'model/system_config_password_login_dto.dart';
 | 
					part 'model/system_config_password_login_dto.dart';
 | 
				
			||||||
part 'model/system_config_storage_template_dto.dart';
 | 
					part 'model/system_config_storage_template_dto.dart';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										41
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							@ -60,47 +60,6 @@ class SearchApi {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Performs an HTTP 'GET /search/config' operation and returns the [Response].
 | 
					 | 
				
			||||||
  Future<Response> getSearchConfigWithHttpInfo() async {
 | 
					 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					 | 
				
			||||||
    final path = r'/search/config';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ignore: prefer_final_locals
 | 
					 | 
				
			||||||
    Object? postBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final queryParams = <QueryParam>[];
 | 
					 | 
				
			||||||
    final headerParams = <String, String>{};
 | 
					 | 
				
			||||||
    final formParams = <String, String>{};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const contentTypes = <String>[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return apiClient.invokeAPI(
 | 
					 | 
				
			||||||
      path,
 | 
					 | 
				
			||||||
      'GET',
 | 
					 | 
				
			||||||
      queryParams,
 | 
					 | 
				
			||||||
      postBody,
 | 
					 | 
				
			||||||
      headerParams,
 | 
					 | 
				
			||||||
      formParams,
 | 
					 | 
				
			||||||
      contentTypes.isEmpty ? null : contentTypes.first,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<SearchConfigResponseDto?> getSearchConfig() async {
 | 
					 | 
				
			||||||
    final response = await getSearchConfigWithHttpInfo();
 | 
					 | 
				
			||||||
    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), 'SearchConfigResponseDto',) as SearchConfigResponseDto;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Performs an HTTP 'GET /search' operation and returns the [Response].
 | 
					  /// Performs an HTTP 'GET /search' operation and returns the [Response].
 | 
				
			||||||
  /// Parameters:
 | 
					  /// Parameters:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							@ -323,8 +323,6 @@ class ApiClient {
 | 
				
			|||||||
          return SearchAssetDto.fromJson(value);
 | 
					          return SearchAssetDto.fromJson(value);
 | 
				
			||||||
        case 'SearchAssetResponseDto':
 | 
					        case 'SearchAssetResponseDto':
 | 
				
			||||||
          return SearchAssetResponseDto.fromJson(value);
 | 
					          return SearchAssetResponseDto.fromJson(value);
 | 
				
			||||||
        case 'SearchConfigResponseDto':
 | 
					 | 
				
			||||||
          return SearchConfigResponseDto.fromJson(value);
 | 
					 | 
				
			||||||
        case 'SearchExploreItem':
 | 
					        case 'SearchExploreItem':
 | 
				
			||||||
          return SearchExploreItem.fromJson(value);
 | 
					          return SearchExploreItem.fromJson(value);
 | 
				
			||||||
        case 'SearchExploreResponseDto':
 | 
					        case 'SearchExploreResponseDto':
 | 
				
			||||||
@ -365,6 +363,8 @@ class ApiClient {
 | 
				
			|||||||
          return SystemConfigFFmpegDto.fromJson(value);
 | 
					          return SystemConfigFFmpegDto.fromJson(value);
 | 
				
			||||||
        case 'SystemConfigJobDto':
 | 
					        case 'SystemConfigJobDto':
 | 
				
			||||||
          return SystemConfigJobDto.fromJson(value);
 | 
					          return SystemConfigJobDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SystemConfigMachineLearningDto':
 | 
				
			||||||
 | 
					          return SystemConfigMachineLearningDto.fromJson(value);
 | 
				
			||||||
        case 'SystemConfigOAuthDto':
 | 
					        case 'SystemConfigOAuthDto':
 | 
				
			||||||
          return SystemConfigOAuthDto.fromJson(value);
 | 
					          return SystemConfigOAuthDto.fromJson(value);
 | 
				
			||||||
        case 'SystemConfigPasswordLoginDto':
 | 
					        case 'SystemConfigPasswordLoginDto':
 | 
				
			||||||
 | 
				
			|||||||
@ -1,98 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// @dart=2.12
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ignore_for_file: unused_element, unused_import
 | 
					 | 
				
			||||||
// ignore_for_file: always_put_required_named_parameters_first
 | 
					 | 
				
			||||||
// ignore_for_file: constant_identifier_names
 | 
					 | 
				
			||||||
// ignore_for_file: lines_longer_than_80_chars
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
part of openapi.api;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SearchConfigResponseDto {
 | 
					 | 
				
			||||||
  /// Returns a new [SearchConfigResponseDto] instance.
 | 
					 | 
				
			||||||
  SearchConfigResponseDto({
 | 
					 | 
				
			||||||
    required this.enabled,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bool enabled;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is SearchConfigResponseDto &&
 | 
					 | 
				
			||||||
     other.enabled == enabled;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  int get hashCode =>
 | 
					 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					 | 
				
			||||||
    (enabled.hashCode);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  String toString() => 'SearchConfigResponseDto[enabled=$enabled]';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					 | 
				
			||||||
      json[r'enabled'] = this.enabled;
 | 
					 | 
				
			||||||
    return json;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Returns a new [SearchConfigResponseDto] instance and imports its values from
 | 
					 | 
				
			||||||
  /// [value] if it's a [Map], null otherwise.
 | 
					 | 
				
			||||||
  // ignore: prefer_constructors_over_static_methods
 | 
					 | 
				
			||||||
  static SearchConfigResponseDto? fromJson(dynamic value) {
 | 
					 | 
				
			||||||
    if (value is Map) {
 | 
					 | 
				
			||||||
      final json = value.cast<String, dynamic>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return SearchConfigResponseDto(
 | 
					 | 
				
			||||||
        enabled: mapValueOfType<bool>(json, r'enabled')!,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static List<SearchConfigResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
 | 
					 | 
				
			||||||
    final result = <SearchConfigResponseDto>[];
 | 
					 | 
				
			||||||
    if (json is List && json.isNotEmpty) {
 | 
					 | 
				
			||||||
      for (final row in json) {
 | 
					 | 
				
			||||||
        final value = SearchConfigResponseDto.fromJson(row);
 | 
					 | 
				
			||||||
        if (value != null) {
 | 
					 | 
				
			||||||
          result.add(value);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return result.toList(growable: growable);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static Map<String, SearchConfigResponseDto> mapFromJson(dynamic json) {
 | 
					 | 
				
			||||||
    final map = <String, SearchConfigResponseDto>{};
 | 
					 | 
				
			||||||
    if (json is Map && json.isNotEmpty) {
 | 
					 | 
				
			||||||
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
					 | 
				
			||||||
      for (final entry in json.entries) {
 | 
					 | 
				
			||||||
        final value = SearchConfigResponseDto.fromJson(entry.value);
 | 
					 | 
				
			||||||
        if (value != null) {
 | 
					 | 
				
			||||||
          map[entry.key] = value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return map;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // maps a json object with a list of SearchConfigResponseDto-objects as value to a dart map
 | 
					 | 
				
			||||||
  static Map<String, List<SearchConfigResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
					 | 
				
			||||||
    final map = <String, List<SearchConfigResponseDto>>{};
 | 
					 | 
				
			||||||
    if (json is Map && json.isNotEmpty) {
 | 
					 | 
				
			||||||
      // ignore: parameter_assignments
 | 
					 | 
				
			||||||
      json = json.cast<String, dynamic>();
 | 
					 | 
				
			||||||
      for (final entry in json.entries) {
 | 
					 | 
				
			||||||
        map[entry.key] = SearchConfigResponseDto.listFromJson(entry.value, growable: growable,);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return map;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// The list of required keys that must be present in a JSON.
 | 
					 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					 | 
				
			||||||
    'enabled',
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							
								
								
									
										44
									
								
								mobile/openapi/lib/model/server_features_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								mobile/openapi/lib/model/server_features_dto.dart
									
									
									
										generated
									
									
									
								
							@ -13,14 +13,19 @@ part of openapi.api;
 | 
				
			|||||||
class ServerFeaturesDto {
 | 
					class ServerFeaturesDto {
 | 
				
			||||||
  /// Returns a new [ServerFeaturesDto] instance.
 | 
					  /// Returns a new [ServerFeaturesDto] instance.
 | 
				
			||||||
  ServerFeaturesDto({
 | 
					  ServerFeaturesDto({
 | 
				
			||||||
    required this.machineLearning,
 | 
					    required this.clipEncode,
 | 
				
			||||||
 | 
					    required this.facialRecognition,
 | 
				
			||||||
    required this.oauth,
 | 
					    required this.oauth,
 | 
				
			||||||
    required this.oauthAutoLaunch,
 | 
					    required this.oauthAutoLaunch,
 | 
				
			||||||
    required this.passwordLogin,
 | 
					    required this.passwordLogin,
 | 
				
			||||||
    required this.search,
 | 
					    required this.search,
 | 
				
			||||||
 | 
					    required this.sidecar,
 | 
				
			||||||
 | 
					    required this.tagImage,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool machineLearning;
 | 
					  bool clipEncode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool facialRecognition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool oauth;
 | 
					  bool oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,33 +35,46 @@ class ServerFeaturesDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  bool search;
 | 
					  bool search;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool sidecar;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool tagImage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto &&
 | 
				
			||||||
     other.machineLearning == machineLearning &&
 | 
					     other.clipEncode == clipEncode &&
 | 
				
			||||||
 | 
					     other.facialRecognition == facialRecognition &&
 | 
				
			||||||
     other.oauth == oauth &&
 | 
					     other.oauth == oauth &&
 | 
				
			||||||
     other.oauthAutoLaunch == oauthAutoLaunch &&
 | 
					     other.oauthAutoLaunch == oauthAutoLaunch &&
 | 
				
			||||||
     other.passwordLogin == passwordLogin &&
 | 
					     other.passwordLogin == passwordLogin &&
 | 
				
			||||||
     other.search == search;
 | 
					     other.search == search &&
 | 
				
			||||||
 | 
					     other.sidecar == sidecar &&
 | 
				
			||||||
 | 
					     other.tagImage == tagImage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (machineLearning.hashCode) +
 | 
					    (clipEncode.hashCode) +
 | 
				
			||||||
 | 
					    (facialRecognition.hashCode) +
 | 
				
			||||||
    (oauth.hashCode) +
 | 
					    (oauth.hashCode) +
 | 
				
			||||||
    (oauthAutoLaunch.hashCode) +
 | 
					    (oauthAutoLaunch.hashCode) +
 | 
				
			||||||
    (passwordLogin.hashCode) +
 | 
					    (passwordLogin.hashCode) +
 | 
				
			||||||
    (search.hashCode);
 | 
					    (search.hashCode) +
 | 
				
			||||||
 | 
					    (sidecar.hashCode) +
 | 
				
			||||||
 | 
					    (tagImage.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'ServerFeaturesDto[machineLearning=$machineLearning, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search]';
 | 
					  String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, facialRecognition=$facialRecognition, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
      json[r'machineLearning'] = this.machineLearning;
 | 
					      json[r'clipEncode'] = this.clipEncode;
 | 
				
			||||||
 | 
					      json[r'facialRecognition'] = this.facialRecognition;
 | 
				
			||||||
      json[r'oauth'] = this.oauth;
 | 
					      json[r'oauth'] = this.oauth;
 | 
				
			||||||
      json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
 | 
					      json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
 | 
				
			||||||
      json[r'passwordLogin'] = this.passwordLogin;
 | 
					      json[r'passwordLogin'] = this.passwordLogin;
 | 
				
			||||||
      json[r'search'] = this.search;
 | 
					      json[r'search'] = this.search;
 | 
				
			||||||
 | 
					      json[r'sidecar'] = this.sidecar;
 | 
				
			||||||
 | 
					      json[r'tagImage'] = this.tagImage;
 | 
				
			||||||
    return json;
 | 
					    return json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -68,11 +86,14 @@ class ServerFeaturesDto {
 | 
				
			|||||||
      final json = value.cast<String, dynamic>();
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return ServerFeaturesDto(
 | 
					      return ServerFeaturesDto(
 | 
				
			||||||
        machineLearning: mapValueOfType<bool>(json, r'machineLearning')!,
 | 
					        clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
 | 
				
			||||||
 | 
					        facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
 | 
				
			||||||
        oauth: mapValueOfType<bool>(json, r'oauth')!,
 | 
					        oauth: mapValueOfType<bool>(json, r'oauth')!,
 | 
				
			||||||
        oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
 | 
					        oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
 | 
				
			||||||
        passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
 | 
					        passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
 | 
				
			||||||
        search: mapValueOfType<bool>(json, r'search')!,
 | 
					        search: mapValueOfType<bool>(json, r'search')!,
 | 
				
			||||||
 | 
					        sidecar: mapValueOfType<bool>(json, r'sidecar')!,
 | 
				
			||||||
 | 
					        tagImage: mapValueOfType<bool>(json, r'tagImage')!,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
@ -120,11 +141,14 @@ class ServerFeaturesDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// The list of required keys that must be present in a JSON.
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    'machineLearning',
 | 
					    'clipEncode',
 | 
				
			||||||
 | 
					    'facialRecognition',
 | 
				
			||||||
    'oauth',
 | 
					    'oauth',
 | 
				
			||||||
    'oauthAutoLaunch',
 | 
					    'oauthAutoLaunch',
 | 
				
			||||||
    'passwordLogin',
 | 
					    'passwordLogin',
 | 
				
			||||||
    'search',
 | 
					    'search',
 | 
				
			||||||
 | 
					    'sidecar',
 | 
				
			||||||
 | 
					    'tagImage',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							@ -15,6 +15,7 @@ class SystemConfigDto {
 | 
				
			|||||||
  SystemConfigDto({
 | 
					  SystemConfigDto({
 | 
				
			||||||
    required this.ffmpeg,
 | 
					    required this.ffmpeg,
 | 
				
			||||||
    required this.job,
 | 
					    required this.job,
 | 
				
			||||||
 | 
					    required this.machineLearning,
 | 
				
			||||||
    required this.oauth,
 | 
					    required this.oauth,
 | 
				
			||||||
    required this.passwordLogin,
 | 
					    required this.passwordLogin,
 | 
				
			||||||
    required this.storageTemplate,
 | 
					    required this.storageTemplate,
 | 
				
			||||||
@ -25,6 +26,8 @@ class SystemConfigDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  SystemConfigJobDto job;
 | 
					  SystemConfigJobDto job;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SystemConfigMachineLearningDto machineLearning;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SystemConfigOAuthDto oauth;
 | 
					  SystemConfigOAuthDto oauth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SystemConfigPasswordLoginDto passwordLogin;
 | 
					  SystemConfigPasswordLoginDto passwordLogin;
 | 
				
			||||||
@ -37,6 +40,7 @@ class SystemConfigDto {
 | 
				
			|||||||
  bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
 | 
				
			||||||
     other.ffmpeg == ffmpeg &&
 | 
					     other.ffmpeg == ffmpeg &&
 | 
				
			||||||
     other.job == job &&
 | 
					     other.job == job &&
 | 
				
			||||||
 | 
					     other.machineLearning == machineLearning &&
 | 
				
			||||||
     other.oauth == oauth &&
 | 
					     other.oauth == oauth &&
 | 
				
			||||||
     other.passwordLogin == passwordLogin &&
 | 
					     other.passwordLogin == passwordLogin &&
 | 
				
			||||||
     other.storageTemplate == storageTemplate &&
 | 
					     other.storageTemplate == storageTemplate &&
 | 
				
			||||||
@ -47,18 +51,20 @@ class SystemConfigDto {
 | 
				
			|||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (ffmpeg.hashCode) +
 | 
					    (ffmpeg.hashCode) +
 | 
				
			||||||
    (job.hashCode) +
 | 
					    (job.hashCode) +
 | 
				
			||||||
 | 
					    (machineLearning.hashCode) +
 | 
				
			||||||
    (oauth.hashCode) +
 | 
					    (oauth.hashCode) +
 | 
				
			||||||
    (passwordLogin.hashCode) +
 | 
					    (passwordLogin.hashCode) +
 | 
				
			||||||
    (storageTemplate.hashCode) +
 | 
					    (storageTemplate.hashCode) +
 | 
				
			||||||
    (thumbnail.hashCode);
 | 
					    (thumbnail.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
 | 
					  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
      json[r'ffmpeg'] = this.ffmpeg;
 | 
					      json[r'ffmpeg'] = this.ffmpeg;
 | 
				
			||||||
      json[r'job'] = this.job;
 | 
					      json[r'job'] = this.job;
 | 
				
			||||||
 | 
					      json[r'machineLearning'] = this.machineLearning;
 | 
				
			||||||
      json[r'oauth'] = this.oauth;
 | 
					      json[r'oauth'] = this.oauth;
 | 
				
			||||||
      json[r'passwordLogin'] = this.passwordLogin;
 | 
					      json[r'passwordLogin'] = this.passwordLogin;
 | 
				
			||||||
      json[r'storageTemplate'] = this.storageTemplate;
 | 
					      json[r'storageTemplate'] = this.storageTemplate;
 | 
				
			||||||
@ -76,6 +82,7 @@ class SystemConfigDto {
 | 
				
			|||||||
      return SystemConfigDto(
 | 
					      return SystemConfigDto(
 | 
				
			||||||
        ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
 | 
					        ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
 | 
				
			||||||
        job: SystemConfigJobDto.fromJson(json[r'job'])!,
 | 
					        job: SystemConfigJobDto.fromJson(json[r'job'])!,
 | 
				
			||||||
 | 
					        machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
 | 
				
			||||||
        oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
 | 
					        oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
 | 
				
			||||||
        passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
 | 
					        passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
 | 
				
			||||||
        storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
 | 
					        storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
 | 
				
			||||||
@ -129,6 +136,7 @@ class SystemConfigDto {
 | 
				
			|||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    'ffmpeg',
 | 
					    'ffmpeg',
 | 
				
			||||||
    'job',
 | 
					    'job',
 | 
				
			||||||
 | 
					    'machineLearning',
 | 
				
			||||||
    'oauth',
 | 
					    'oauth',
 | 
				
			||||||
    'passwordLogin',
 | 
					    'passwordLogin',
 | 
				
			||||||
    'storageTemplate',
 | 
					    'storageTemplate',
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										130
									
								
								mobile/openapi/lib/model/system_config_machine_learning_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								mobile/openapi/lib/model/system_config_machine_learning_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, unused_import
 | 
				
			||||||
 | 
					// ignore_for_file: always_put_required_named_parameters_first
 | 
				
			||||||
 | 
					// ignore_for_file: constant_identifier_names
 | 
				
			||||||
 | 
					// ignore_for_file: lines_longer_than_80_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of openapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SystemConfigMachineLearningDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SystemConfigMachineLearningDto] instance.
 | 
				
			||||||
 | 
					  SystemConfigMachineLearningDto({
 | 
				
			||||||
 | 
					    required this.clipEncodeEnabled,
 | 
				
			||||||
 | 
					    required this.enabled,
 | 
				
			||||||
 | 
					    required this.facialRecognitionEnabled,
 | 
				
			||||||
 | 
					    required this.tagImageEnabled,
 | 
				
			||||||
 | 
					    required this.url,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool clipEncodeEnabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool enabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool facialRecognitionEnabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool tagImageEnabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SystemConfigMachineLearningDto &&
 | 
				
			||||||
 | 
					     other.clipEncodeEnabled == clipEncodeEnabled &&
 | 
				
			||||||
 | 
					     other.enabled == enabled &&
 | 
				
			||||||
 | 
					     other.facialRecognitionEnabled == facialRecognitionEnabled &&
 | 
				
			||||||
 | 
					     other.tagImageEnabled == tagImageEnabled &&
 | 
				
			||||||
 | 
					     other.url == url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (clipEncodeEnabled.hashCode) +
 | 
				
			||||||
 | 
					    (enabled.hashCode) +
 | 
				
			||||||
 | 
					    (facialRecognitionEnabled.hashCode) +
 | 
				
			||||||
 | 
					    (tagImageEnabled.hashCode) +
 | 
				
			||||||
 | 
					    (url.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SystemConfigMachineLearningDto[clipEncodeEnabled=$clipEncodeEnabled, enabled=$enabled, facialRecognitionEnabled=$facialRecognitionEnabled, tagImageEnabled=$tagImageEnabled, url=$url]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'clipEncodeEnabled'] = this.clipEncodeEnabled;
 | 
				
			||||||
 | 
					      json[r'enabled'] = this.enabled;
 | 
				
			||||||
 | 
					      json[r'facialRecognitionEnabled'] = this.facialRecognitionEnabled;
 | 
				
			||||||
 | 
					      json[r'tagImageEnabled'] = this.tagImageEnabled;
 | 
				
			||||||
 | 
					      json[r'url'] = this.url;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SystemConfigMachineLearningDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SystemConfigMachineLearningDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SystemConfigMachineLearningDto(
 | 
				
			||||||
 | 
					        clipEncodeEnabled: mapValueOfType<bool>(json, r'clipEncodeEnabled')!,
 | 
				
			||||||
 | 
					        enabled: mapValueOfType<bool>(json, r'enabled')!,
 | 
				
			||||||
 | 
					        facialRecognitionEnabled: mapValueOfType<bool>(json, r'facialRecognitionEnabled')!,
 | 
				
			||||||
 | 
					        tagImageEnabled: mapValueOfType<bool>(json, r'tagImageEnabled')!,
 | 
				
			||||||
 | 
					        url: mapValueOfType<String>(json, r'url')!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SystemConfigMachineLearningDto> listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SystemConfigMachineLearningDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SystemConfigMachineLearningDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SystemConfigMachineLearningDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SystemConfigMachineLearningDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SystemConfigMachineLearningDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SystemConfigMachineLearningDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SystemConfigMachineLearningDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SystemConfigMachineLearningDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>();
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        map[entry.key] = SystemConfigMachineLearningDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'clipEncodeEnabled',
 | 
				
			||||||
 | 
					    'enabled',
 | 
				
			||||||
 | 
					    'facialRecognitionEnabled',
 | 
				
			||||||
 | 
					    'tagImageEnabled',
 | 
				
			||||||
 | 
					    'url',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
								
							@ -22,11 +22,6 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Future<SearchConfigResponseDto> getSearchConfig() async
 | 
					 | 
				
			||||||
    test('test getSearchConfig', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
 | 
					    //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
 | 
				
			||||||
    test('test search', () async {
 | 
					    test('test search', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// @dart=2.12
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ignore_for_file: unused_element, unused_import
 | 
					 | 
				
			||||||
// ignore_for_file: always_put_required_named_parameters_first
 | 
					 | 
				
			||||||
// ignore_for_file: constant_identifier_names
 | 
					 | 
				
			||||||
// ignore_for_file: lines_longer_than_80_chars
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
import 'package:test/test.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// tests for SearchConfigResponseDto
 | 
					 | 
				
			||||||
void main() {
 | 
					 | 
				
			||||||
  // final instance = SearchConfigResponseDto();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  group('test SearchConfigResponseDto', () {
 | 
					 | 
				
			||||||
    // bool enabled
 | 
					 | 
				
			||||||
    test('to test the property `enabled`', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										19
									
								
								mobile/openapi/test/server_features_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/test/server_features_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -16,8 +16,13 @@ void main() {
 | 
				
			|||||||
  // final instance = ServerFeaturesDto();
 | 
					  // final instance = ServerFeaturesDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('test ServerFeaturesDto', () {
 | 
					  group('test ServerFeaturesDto', () {
 | 
				
			||||||
    // bool machineLearning
 | 
					    // bool clipEncode
 | 
				
			||||||
    test('to test the property `machineLearning`', () async {
 | 
					    test('to test the property `clipEncode`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool facialRecognition
 | 
				
			||||||
 | 
					    test('to test the property `facialRecognition`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,6 +46,16 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool sidecar
 | 
				
			||||||
 | 
					    test('to test the property `sidecar`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool tagImage
 | 
				
			||||||
 | 
					    test('to test the property `tagImage`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -26,6 +26,11 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // SystemConfigMachineLearningDto machineLearning
 | 
				
			||||||
 | 
					    test('to test the property `machineLearning`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // SystemConfigOAuthDto oauth
 | 
					    // SystemConfigOAuthDto oauth
 | 
				
			||||||
    test('to test the property `oauth`', () async {
 | 
					    test('to test the property `oauth`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										47
									
								
								mobile/openapi/test/system_config_machine_learning_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								mobile/openapi/test/system_config_machine_learning_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, unused_import
 | 
				
			||||||
 | 
					// ignore_for_file: always_put_required_named_parameters_first
 | 
				
			||||||
 | 
					// ignore_for_file: constant_identifier_names
 | 
				
			||||||
 | 
					// ignore_for_file: lines_longer_than_80_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SystemConfigMachineLearningDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SystemConfigMachineLearningDto', () {
 | 
				
			||||||
 | 
					    // bool clipEncodeEnabled
 | 
				
			||||||
 | 
					    test('to test the property `clipEncodeEnabled`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool enabled
 | 
				
			||||||
 | 
					    test('to test the property `enabled`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool facialRecognitionEnabled
 | 
				
			||||||
 | 
					    test('to test the property `facialRecognitionEnabled`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // bool tagImageEnabled
 | 
				
			||||||
 | 
					    test('to test the property `tagImageEnabled`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String url
 | 
				
			||||||
 | 
					    test('to test the property `url`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3243,38 +3243,6 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "/search/config": {
 | 
					 | 
				
			||||||
      "get": {
 | 
					 | 
				
			||||||
        "operationId": "getSearchConfig",
 | 
					 | 
				
			||||||
        "parameters": [],
 | 
					 | 
				
			||||||
        "responses": {
 | 
					 | 
				
			||||||
          "200": {
 | 
					 | 
				
			||||||
            "content": {
 | 
					 | 
				
			||||||
              "application/json": {
 | 
					 | 
				
			||||||
                "schema": {
 | 
					 | 
				
			||||||
                  "$ref": "#/components/schemas/SearchConfigResponseDto"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            "description": ""
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "security": [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "bearer": []
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "cookie": []
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "api_key": []
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        "tags": [
 | 
					 | 
				
			||||||
          "Search"
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "/search/explore": {
 | 
					    "/search/explore": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "operationId": "getExploreData",
 | 
					        "operationId": "getExploreData",
 | 
				
			||||||
@ -6424,17 +6392,6 @@
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "SearchConfigResponseDto": {
 | 
					 | 
				
			||||||
        "properties": {
 | 
					 | 
				
			||||||
          "enabled": {
 | 
					 | 
				
			||||||
            "type": "boolean"
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "required": [
 | 
					 | 
				
			||||||
          "enabled"
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        "type": "object"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "SearchExploreItem": {
 | 
					      "SearchExploreItem": {
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "data": {
 | 
					          "data": {
 | 
				
			||||||
@ -6518,7 +6475,10 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "ServerFeaturesDto": {
 | 
					      "ServerFeaturesDto": {
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "machineLearning": {
 | 
					          "clipEncode": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "facialRecognition": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "oauth": {
 | 
					          "oauth": {
 | 
				
			||||||
@ -6532,11 +6492,20 @@
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          "search": {
 | 
					          "search": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "sidecar": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "tagImage": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "machineLearning",
 | 
					          "clipEncode",
 | 
				
			||||||
 | 
					          "facialRecognition",
 | 
				
			||||||
 | 
					          "sidecar",
 | 
				
			||||||
          "search",
 | 
					          "search",
 | 
				
			||||||
 | 
					          "tagImage",
 | 
				
			||||||
          "oauth",
 | 
					          "oauth",
 | 
				
			||||||
          "oauthAutoLaunch",
 | 
					          "oauthAutoLaunch",
 | 
				
			||||||
          "passwordLogin"
 | 
					          "passwordLogin"
 | 
				
			||||||
@ -6868,6 +6837,9 @@
 | 
				
			|||||||
          "job": {
 | 
					          "job": {
 | 
				
			||||||
            "$ref": "#/components/schemas/SystemConfigJobDto"
 | 
					            "$ref": "#/components/schemas/SystemConfigJobDto"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "machineLearning": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/SystemConfigMachineLearningDto"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "oauth": {
 | 
					          "oauth": {
 | 
				
			||||||
            "$ref": "#/components/schemas/SystemConfigOAuthDto"
 | 
					            "$ref": "#/components/schemas/SystemConfigOAuthDto"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -6883,6 +6855,7 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "required": [
 | 
					        "required": [
 | 
				
			||||||
          "ffmpeg",
 | 
					          "ffmpeg",
 | 
				
			||||||
 | 
					          "machineLearning",
 | 
				
			||||||
          "oauth",
 | 
					          "oauth",
 | 
				
			||||||
          "passwordLogin",
 | 
					          "passwordLogin",
 | 
				
			||||||
          "storageTemplate",
 | 
					          "storageTemplate",
 | 
				
			||||||
@ -6989,6 +6962,33 @@
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "type": "object"
 | 
					        "type": "object"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "SystemConfigMachineLearningDto": {
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "clipEncodeEnabled": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "enabled": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "facialRecognitionEnabled": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "tagImageEnabled": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "url": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "enabled",
 | 
				
			||||||
 | 
					          "url",
 | 
				
			||||||
 | 
					          "clipEncodeEnabled",
 | 
				
			||||||
 | 
					          "facialRecognitionEnabled",
 | 
				
			||||||
 | 
					          "tagImageEnabled"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "type": "object"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "SystemConfigOAuthDto": {
 | 
					      "SystemConfigOAuthDto": {
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
          "autoLaunch": {
 | 
					          "autoLaunch": {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
import { AssetType } from '@app/infra/entities';
 | 
					import { AssetType } from '@app/infra/entities';
 | 
				
			||||||
import { BadRequestException } from '@nestjs/common';
 | 
					 | 
				
			||||||
import { Duration } from 'luxon';
 | 
					import { Duration } from 'luxon';
 | 
				
			||||||
import { extname } from 'node:path';
 | 
					import { extname } from 'node:path';
 | 
				
			||||||
import pkg from 'src/../../package.json';
 | 
					import pkg from 'src/../../package.json';
 | 
				
			||||||
@ -24,17 +23,6 @@ export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${s
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
 | 
					export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SEARCH_ENABLED = process.env.TYPESENSE_ENABLED !== 'false';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
 | 
					 | 
				
			||||||
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function assertMachineLearningEnabled() {
 | 
					 | 
				
			||||||
  if (!MACHINE_LEARNING_ENABLED) {
 | 
					 | 
				
			||||||
    throw new BadRequestException('Machine learning is not enabled.');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const image: Record<string, string[]> = {
 | 
					const image: Record<string, string[]> = {
 | 
				
			||||||
  '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
 | 
					  '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
 | 
				
			||||||
  '.ari': ['image/ari', 'image/x-arriflex-ari'],
 | 
					  '.ari': ['image/ari', 'image/x-arriflex-ari'],
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ import {
 | 
				
			|||||||
  newPersonRepositoryMock,
 | 
					  newPersonRepositoryMock,
 | 
				
			||||||
  newSearchRepositoryMock,
 | 
					  newSearchRepositoryMock,
 | 
				
			||||||
  newStorageRepositoryMock,
 | 
					  newStorageRepositoryMock,
 | 
				
			||||||
 | 
					  newSystemConfigRepositoryMock,
 | 
				
			||||||
  personStub,
 | 
					  personStub,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
@ -18,6 +19,7 @@ import { IPersonRepository } from '../person';
 | 
				
			|||||||
import { ISearchRepository } from '../search';
 | 
					import { ISearchRepository } from '../search';
 | 
				
			||||||
import { IMachineLearningRepository } from '../smart-info';
 | 
					import { IMachineLearningRepository } from '../smart-info';
 | 
				
			||||||
import { IStorageRepository } from '../storage';
 | 
					import { IStorageRepository } from '../storage';
 | 
				
			||||||
 | 
					import { ISystemConfigRepository } from '../system-config';
 | 
				
			||||||
import { IFaceRepository } from './face.repository';
 | 
					import { IFaceRepository } from './face.repository';
 | 
				
			||||||
import { FacialRecognitionService } from './facial-recognition.services';
 | 
					import { FacialRecognitionService } from './facial-recognition.services';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -94,6 +96,7 @@ const faceSearch = {
 | 
				
			|||||||
describe(FacialRecognitionService.name, () => {
 | 
					describe(FacialRecognitionService.name, () => {
 | 
				
			||||||
  let sut: FacialRecognitionService;
 | 
					  let sut: FacialRecognitionService;
 | 
				
			||||||
  let assetMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
 | 
					  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
				
			||||||
  let faceMock: jest.Mocked<IFaceRepository>;
 | 
					  let faceMock: jest.Mocked<IFaceRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let machineLearningMock: jest.Mocked<IMachineLearningRepository>;
 | 
					  let machineLearningMock: jest.Mocked<IMachineLearningRepository>;
 | 
				
			||||||
@ -104,6 +107,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
 | 
					    configMock = newSystemConfigRepositoryMock();
 | 
				
			||||||
    faceMock = newFaceRepositoryMock();
 | 
					    faceMock = newFaceRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    machineLearningMock = newMachineLearningRepositoryMock();
 | 
					    machineLearningMock = newMachineLearningRepositoryMock();
 | 
				
			||||||
@ -116,6 +120,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    sut = new FacialRecognitionService(
 | 
					    sut = new FacialRecognitionService(
 | 
				
			||||||
      assetMock,
 | 
					      assetMock,
 | 
				
			||||||
 | 
					      configMock,
 | 
				
			||||||
      faceMock,
 | 
					      faceMock,
 | 
				
			||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      machineLearningMock,
 | 
					      machineLearningMock,
 | 
				
			||||||
@ -174,7 +179,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
      machineLearningMock.detectFaces.mockResolvedValue([]);
 | 
					      machineLearningMock.detectFaces.mockResolvedValue([]);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
      await sut.handleRecognizeFaces({ id: assetStub.image.id });
 | 
					      await sut.handleRecognizeFaces({ id: assetStub.image.id });
 | 
				
			||||||
      expect(machineLearningMock.detectFaces).toHaveBeenCalledWith({
 | 
					      expect(machineLearningMock.detectFaces).toHaveBeenCalledWith('http://immich-machine-learning:3003', {
 | 
				
			||||||
        imagePath: assetStub.image.resizePath,
 | 
					        imagePath: assetStub.image.resizePath,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(faceMock.create).not.toHaveBeenCalled();
 | 
					      expect(faceMock.create).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
import { Inject, Logger } from '@nestjs/common';
 | 
					import { Inject, Logger } from '@nestjs/common';
 | 
				
			||||||
import { join } from 'path';
 | 
					import { join } from 'path';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
 | 
					 | 
				
			||||||
import { usePagination } from '../domain.util';
 | 
					import { usePagination } from '../domain.util';
 | 
				
			||||||
import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
 | 
					import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
 | 
				
			||||||
import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media';
 | 
					import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media';
 | 
				
			||||||
@ -9,14 +8,17 @@ import { IPersonRepository } from '../person/person.repository';
 | 
				
			|||||||
import { ISearchRepository } from '../search/search.repository';
 | 
					import { ISearchRepository } from '../search/search.repository';
 | 
				
			||||||
import { IMachineLearningRepository } from '../smart-info';
 | 
					import { IMachineLearningRepository } from '../smart-info';
 | 
				
			||||||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 | 
					import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 | 
				
			||||||
 | 
					import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
 | 
				
			||||||
import { AssetFaceId, IFaceRepository } from './face.repository';
 | 
					import { AssetFaceId, IFaceRepository } from './face.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class FacialRecognitionService {
 | 
					export class FacialRecognitionService {
 | 
				
			||||||
  private logger = new Logger(FacialRecognitionService.name);
 | 
					  private logger = new Logger(FacialRecognitionService.name);
 | 
				
			||||||
  private storageCore = new StorageCore();
 | 
					  private storageCore = new StorageCore();
 | 
				
			||||||
 | 
					  private configCore: SystemConfigCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
				
			||||||
    @Inject(IFaceRepository) private faceRepository: IFaceRepository,
 | 
					    @Inject(IFaceRepository) private faceRepository: IFaceRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
					    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
				
			||||||
@ -24,9 +26,16 @@ export class FacialRecognitionService {
 | 
				
			|||||||
    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
					    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
					    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
  ) {}
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.configCore = new SystemConfigCore(configRepository);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleQueueRecognizeFaces({ force }: IBaseJob) {
 | 
					  async handleQueueRecognizeFaces({ force }: IBaseJob) {
 | 
				
			||||||
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
 | 
					    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
 | 
				
			||||||
      return force
 | 
					      return force
 | 
				
			||||||
        ? this.assetRepository.getAll(pagination, { order: 'DESC' })
 | 
					        ? this.assetRepository.getAll(pagination, { order: 'DESC' })
 | 
				
			||||||
@ -49,12 +58,17 @@ export class FacialRecognitionService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleRecognizeFaces({ id }: IEntityJob) {
 | 
					  async handleRecognizeFaces({ id }: IEntityJob) {
 | 
				
			||||||
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [asset] = await this.assetRepository.getByIds([id]);
 | 
					    const [asset] = await this.assetRepository.getByIds([id]);
 | 
				
			||||||
    if (!asset || !MACHINE_LEARNING_ENABLED || !asset.resizePath) {
 | 
					    if (!asset || !asset.resizePath) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const faces = await this.machineLearning.detectFaces({ imagePath: asset.resizePath });
 | 
					    const faces = await this.machineLearning.detectFaces(machineLearning.url, { imagePath: asset.resizePath });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`);
 | 
					    this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`);
 | 
				
			||||||
    this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` })));
 | 
					    this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` })));
 | 
				
			||||||
@ -100,6 +114,11 @@ export class FacialRecognitionService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) {
 | 
					  async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) {
 | 
				
			||||||
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { assetId, personId, boundingBox, imageWidth, imageHeight } = data;
 | 
					    const { assetId, personId, boundingBox, imageWidth, imageHeight } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [asset] = await this.assetRepository.getByIds([assetId]);
 | 
					    const [asset] = await this.assetRepository.getByIds([assetId]);
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,7 @@ import { AssetType } from '@app/infra/entities';
 | 
				
			|||||||
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
					import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import { IAssetRepository, mapAsset } from '../asset';
 | 
					import { IAssetRepository, mapAsset } from '../asset';
 | 
				
			||||||
import { CommunicationEvent, ICommunicationRepository } from '../communication';
 | 
					import { CommunicationEvent, ICommunicationRepository } from '../communication';
 | 
				
			||||||
import { assertMachineLearningEnabled } from '../domain.constant';
 | 
					import { FeatureFlag, ISystemConfigRepository } from '../system-config';
 | 
				
			||||||
import { ISystemConfigRepository } from '../system-config';
 | 
					 | 
				
			||||||
import { SystemConfigCore } from '../system-config/system-config.core';
 | 
					import { SystemConfigCore } from '../system-config/system-config.core';
 | 
				
			||||||
import { JobCommand, JobName, QueueName } from './job.constants';
 | 
					import { JobCommand, JobName, QueueName } from './job.constants';
 | 
				
			||||||
import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto';
 | 
					import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto';
 | 
				
			||||||
@ -78,23 +77,25 @@ export class JobService {
 | 
				
			|||||||
        return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
 | 
					        return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case QueueName.OBJECT_TAGGING:
 | 
					      case QueueName.OBJECT_TAGGING:
 | 
				
			||||||
        assertMachineLearningEnabled();
 | 
					        await this.configCore.requireFeature(FeatureFlag.TAG_IMAGE);
 | 
				
			||||||
        return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } });
 | 
					        return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case QueueName.CLIP_ENCODING:
 | 
					      case QueueName.CLIP_ENCODING:
 | 
				
			||||||
        assertMachineLearningEnabled();
 | 
					        await this.configCore.requireFeature(FeatureFlag.CLIP_ENCODE);
 | 
				
			||||||
        return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } });
 | 
					        return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case QueueName.METADATA_EXTRACTION:
 | 
					      case QueueName.METADATA_EXTRACTION:
 | 
				
			||||||
        return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } });
 | 
					        return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case QueueName.SIDECAR:
 | 
					      case QueueName.SIDECAR:
 | 
				
			||||||
 | 
					        await this.configCore.requireFeature(FeatureFlag.SIDECAR);
 | 
				
			||||||
        return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
 | 
					        return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case QueueName.THUMBNAIL_GENERATION:
 | 
					      case QueueName.THUMBNAIL_GENERATION:
 | 
				
			||||||
        return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } });
 | 
					        return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case QueueName.RECOGNIZE_FACES:
 | 
					      case QueueName.RECOGNIZE_FACES:
 | 
				
			||||||
 | 
					        await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
 | 
				
			||||||
        return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } });
 | 
					        return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,2 @@
 | 
				
			|||||||
export * from './search-config-response.dto';
 | 
					 | 
				
			||||||
export * from './search-explore.response.dto';
 | 
					export * from './search-explore.response.dto';
 | 
				
			||||||
export * from './search-response.dto';
 | 
					export * from './search-response.dto';
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
export class SearchConfigResponseDto {
 | 
					 | 
				
			||||||
  enabled!: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,5 +1,3 @@
 | 
				
			|||||||
import { BadRequestException } from '@nestjs/common';
 | 
					 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  albumStub,
 | 
					  albumStub,
 | 
				
			||||||
  assetStub,
 | 
					  assetStub,
 | 
				
			||||||
@ -12,12 +10,14 @@ import {
 | 
				
			|||||||
  newJobRepositoryMock,
 | 
					  newJobRepositoryMock,
 | 
				
			||||||
  newMachineLearningRepositoryMock,
 | 
					  newMachineLearningRepositoryMock,
 | 
				
			||||||
  newSearchRepositoryMock,
 | 
					  newSearchRepositoryMock,
 | 
				
			||||||
 | 
					  newSystemConfigRepositoryMock,
 | 
				
			||||||
  searchStub,
 | 
					  searchStub,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { plainToInstance } from 'class-transformer';
 | 
					import { plainToInstance } from 'class-transformer';
 | 
				
			||||||
import { IAlbumRepository } from '../album/album.repository';
 | 
					import { IAlbumRepository } from '../album/album.repository';
 | 
				
			||||||
import { IAssetRepository } from '../asset/asset.repository';
 | 
					import { IAssetRepository } from '../asset/asset.repository';
 | 
				
			||||||
import { IFaceRepository } from '../facial-recognition';
 | 
					import { IFaceRepository } from '../facial-recognition';
 | 
				
			||||||
 | 
					import { ISystemConfigRepository } from '../index';
 | 
				
			||||||
import { JobName } from '../job';
 | 
					import { JobName } from '../job';
 | 
				
			||||||
import { IJobRepository } from '../job/job.repository';
 | 
					import { IJobRepository } from '../job/job.repository';
 | 
				
			||||||
import { IMachineLearningRepository } from '../smart-info';
 | 
					import { IMachineLearningRepository } from '../smart-info';
 | 
				
			||||||
@ -31,29 +31,26 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
  let sut: SearchService;
 | 
					  let sut: SearchService;
 | 
				
			||||||
  let albumMock: jest.Mocked<IAlbumRepository>;
 | 
					  let albumMock: jest.Mocked<IAlbumRepository>;
 | 
				
			||||||
  let assetMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
 | 
					  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
				
			||||||
  let faceMock: jest.Mocked<IFaceRepository>;
 | 
					  let faceMock: jest.Mocked<IFaceRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let machineMock: jest.Mocked<IMachineLearningRepository>;
 | 
					  let machineMock: jest.Mocked<IMachineLearningRepository>;
 | 
				
			||||||
  let searchMock: jest.Mocked<ISearchRepository>;
 | 
					  let searchMock: jest.Mocked<ISearchRepository>;
 | 
				
			||||||
  let configMock: jest.Mocked<ConfigService>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const makeSut = (value?: string) => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    if (value) {
 | 
					 | 
				
			||||||
      configMock.get.mockReturnValue(value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return new SearchService(albumMock, assetMock, faceMock, jobMock, machineMock, searchMock, configMock);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  beforeEach(() => {
 | 
					 | 
				
			||||||
    albumMock = newAlbumRepositoryMock();
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
 | 
					    configMock = newSystemConfigRepositoryMock();
 | 
				
			||||||
    faceMock = newFaceRepositoryMock();
 | 
					    faceMock = newFaceRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    machineMock = newMachineLearningRepositoryMock();
 | 
					    machineMock = newMachineLearningRepositoryMock();
 | 
				
			||||||
    searchMock = newSearchRepositoryMock();
 | 
					    searchMock = newSearchRepositoryMock();
 | 
				
			||||||
    configMock = { get: jest.fn() } as unknown as jest.Mocked<ConfigService>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = makeSut();
 | 
					    sut = new SearchService(albumMock, assetMock, configMock, faceMock, jobMock, machineMock, searchMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await sut.init();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  afterEach(() => {
 | 
					  afterEach(() => {
 | 
				
			||||||
@ -86,45 +83,18 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('isEnabled', () => {
 | 
					 | 
				
			||||||
    it('should be enabled by default', () => {
 | 
					 | 
				
			||||||
      expect(sut.isEnabled()).toBe(true);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should be disabled via an env variable', () => {
 | 
					 | 
				
			||||||
      const sut = makeSut('false');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(sut.isEnabled()).toBe(false);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('getConfig', () => {
 | 
					 | 
				
			||||||
    it('should return the config', () => {
 | 
					 | 
				
			||||||
      expect(sut.getConfig()).toEqual({ enabled: true });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should return the config when search is disabled', () => {
 | 
					 | 
				
			||||||
      const sut = makeSut('false');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(sut.getConfig()).toEqual({ enabled: false });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe(`init`, () => {
 | 
					  describe(`init`, () => {
 | 
				
			||||||
    it('should skip when search is disabled', async () => {
 | 
					    // it('should skip when search is disabled', async () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					    //   await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.init();
 | 
					    //   expect(searchMock.setup).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    //   expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    //   expect(jobMock.queue).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(searchMock.setup).not.toHaveBeenCalled();
 | 
					    //   sut.teardown();
 | 
				
			||||||
      expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled();
 | 
					    // });
 | 
				
			||||||
      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      sut.teardown();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip schema migration if not needed', async () => {
 | 
					    it('should skip schema migration if not needed', async () => {
 | 
				
			||||||
      searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false });
 | 
					 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(searchMock.setup).toHaveBeenCalled();
 | 
					      expect(searchMock.setup).toHaveBeenCalled();
 | 
				
			||||||
@ -145,14 +115,14 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('search', () => {
 | 
					  describe('search', () => {
 | 
				
			||||||
    it('should throw an error is search is disabled', async () => {
 | 
					    // it('should throw an error is search is disabled', async () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					    //   sut['enabled'] = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException);
 | 
					    //   await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(searchMock.searchAlbums).not.toHaveBeenCalled();
 | 
					    //   expect(searchMock.searchAlbums).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(searchMock.searchAssets).not.toHaveBeenCalled();
 | 
					    //   expect(searchMock.searchAssets).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    // });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should search assets and albums', async () => {
 | 
					    it('should search assets and albums', async () => {
 | 
				
			||||||
      searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults);
 | 
					      searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults);
 | 
				
			||||||
@ -205,7 +175,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip if search is disabled', async () => {
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleIndexAssets();
 | 
					      await sut.handleIndexAssets();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -216,7 +186,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleIndexAsset', () => {
 | 
					  describe('handleIndexAsset', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleIndexAsset({ ids: [assetStub.image.id] });
 | 
					      sut.handleIndexAsset({ ids: [assetStub.image.id] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -227,7 +197,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleIndexAlbums', () => {
 | 
					  describe('handleIndexAlbums', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleIndexAlbums();
 | 
					      sut.handleIndexAlbums();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -242,7 +212,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleIndexAlbum', () => {
 | 
					  describe('handleIndexAlbum', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleIndexAlbum({ ids: [albumStub.empty.id] });
 | 
					      sut.handleIndexAlbum({ ids: [albumStub.empty.id] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -253,7 +223,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleRemoveAlbum', () => {
 | 
					  describe('handleRemoveAlbum', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleRemoveAlbum({ ids: ['album1'] });
 | 
					      sut.handleRemoveAlbum({ ids: ['album1'] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -264,7 +234,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleRemoveAsset', () => {
 | 
					  describe('handleRemoveAsset', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleRemoveAsset({ ids: ['asset1'] });
 | 
					      sut.handleRemoveAsset({ ids: ['asset1'] });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -305,7 +275,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip if search is disabled', async () => {
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleIndexFaces();
 | 
					      await sut.handleIndexFaces();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -315,7 +285,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleIndexAsset', () => {
 | 
					  describe('handleIndexAsset', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' });
 | 
					      sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(searchMock.importFaces).not.toHaveBeenCalled();
 | 
					      expect(searchMock.importFaces).not.toHaveBeenCalled();
 | 
				
			||||||
@ -333,7 +303,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleRemoveFace', () => {
 | 
					  describe('handleRemoveFace', () => {
 | 
				
			||||||
    it('should skip if search is disabled', () => {
 | 
					    it('should skip if search is disabled', () => {
 | 
				
			||||||
      const sut = makeSut('false');
 | 
					      sut['enabled'] = false;
 | 
				
			||||||
      sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' });
 | 
					      sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,17 @@
 | 
				
			|||||||
import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities';
 | 
					import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities';
 | 
				
			||||||
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
					import { Inject, Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					 | 
				
			||||||
import { mapAlbumWithAssets } from '../album';
 | 
					import { mapAlbumWithAssets } from '../album';
 | 
				
			||||||
import { IAlbumRepository } from '../album/album.repository';
 | 
					import { IAlbumRepository } from '../album/album.repository';
 | 
				
			||||||
import { AssetResponseDto, mapAsset } from '../asset';
 | 
					import { AssetResponseDto, mapAsset } from '../asset';
 | 
				
			||||||
import { IAssetRepository } from '../asset/asset.repository';
 | 
					import { IAssetRepository } from '../asset/asset.repository';
 | 
				
			||||||
import { AuthUserDto } from '../auth';
 | 
					import { AuthUserDto } from '../auth';
 | 
				
			||||||
import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
 | 
					 | 
				
			||||||
import { usePagination } from '../domain.util';
 | 
					import { usePagination } from '../domain.util';
 | 
				
			||||||
import { AssetFaceId, IFaceRepository } from '../facial-recognition';
 | 
					import { AssetFaceId, IFaceRepository } from '../facial-recognition';
 | 
				
			||||||
import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
 | 
					import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
 | 
				
			||||||
import { IMachineLearningRepository } from '../smart-info';
 | 
					import { IMachineLearningRepository } from '../smart-info';
 | 
				
			||||||
 | 
					import { FeatureFlag, ISystemConfigRepository, SystemConfigCore } from '../system-config';
 | 
				
			||||||
import { SearchDto } from './dto';
 | 
					import { SearchDto } from './dto';
 | 
				
			||||||
import { SearchConfigResponseDto, SearchResponseDto } from './response-dto';
 | 
					import { SearchResponseDto } from './response-dto';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ISearchRepository,
 | 
					  ISearchRepository,
 | 
				
			||||||
  OwnedFaceEntity,
 | 
					  OwnedFaceEntity,
 | 
				
			||||||
@ -30,8 +29,9 @@ interface SyncQueue {
 | 
				
			|||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SearchService {
 | 
					export class SearchService {
 | 
				
			||||||
  private logger = new Logger(SearchService.name);
 | 
					  private logger = new Logger(SearchService.name);
 | 
				
			||||||
  private enabled: boolean;
 | 
					  private enabled = false;
 | 
				
			||||||
  private timer: NodeJS.Timer | null = null;
 | 
					  private timer: NodeJS.Timer | null = null;
 | 
				
			||||||
 | 
					  private configCore: SystemConfigCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private albumQueue: SyncQueue = {
 | 
					  private albumQueue: SyncQueue = {
 | 
				
			||||||
    upsert: new Set(),
 | 
					    upsert: new Set(),
 | 
				
			||||||
@ -51,16 +51,13 @@ export class SearchService {
 | 
				
			|||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
					    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
				
			||||||
    @Inject(IFaceRepository) private faceRepository: IFaceRepository,
 | 
					    @Inject(IFaceRepository) private faceRepository: IFaceRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
					    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
					    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
				
			||||||
    configService: ConfigService,
 | 
					 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.enabled = configService.get('TYPESENSE_ENABLED') !== 'false';
 | 
					    this.configCore = new SystemConfigCore(configRepository);
 | 
				
			||||||
    if (this.enabled) {
 | 
					 | 
				
			||||||
      this.timer = setInterval(() => this.flush(), 5_000);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  teardown() {
 | 
					  teardown() {
 | 
				
			||||||
@ -70,17 +67,8 @@ export class SearchService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isEnabled() {
 | 
					 | 
				
			||||||
    return this.enabled;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getConfig(): SearchConfigResponseDto {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      enabled: this.enabled,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async init() {
 | 
					  async init() {
 | 
				
			||||||
 | 
					    this.enabled = await this.configCore.hasFeature(FeatureFlag.SEARCH);
 | 
				
			||||||
    if (!this.enabled) {
 | 
					    if (!this.enabled) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -101,10 +89,13 @@ export class SearchService {
 | 
				
			|||||||
      this.logger.debug('Queueing job to re-index all faces');
 | 
					      this.logger.debug('Queueing job to re-index all faces');
 | 
				
			||||||
      await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES });
 | 
					      await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.timer = setInterval(() => this.flush(), 5_000);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
 | 
					  async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
 | 
				
			||||||
    this.assertEnabled();
 | 
					    await this.configCore.requireFeature(FeatureFlag.SEARCH);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const results = await this.searchRepository.explore(authUser.id);
 | 
					    const results = await this.searchRepository.explore(authUser.id);
 | 
				
			||||||
    const lookup = await this.getLookupMap(
 | 
					    const lookup = await this.getLookupMap(
 | 
				
			||||||
      results.reduce(
 | 
					      results.reduce(
 | 
				
			||||||
@ -126,16 +117,18 @@ export class SearchService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
 | 
					  async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
 | 
				
			||||||
    this.assertEnabled();
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    await this.configCore.requireFeature(FeatureFlag.SEARCH);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const query = dto.q || dto.query || '*';
 | 
					    const query = dto.q || dto.query || '*';
 | 
				
			||||||
    const strategy = dto.clip && MACHINE_LEARNING_ENABLED ? SearchStrategy.CLIP : SearchStrategy.TEXT;
 | 
					    const hasClip = machineLearning.enabled && machineLearning.clipEncodeEnabled;
 | 
				
			||||||
 | 
					    const strategy = dto.clip && hasClip ? SearchStrategy.CLIP : SearchStrategy.TEXT;
 | 
				
			||||||
    const filters = { userId: authUser.id, ...dto };
 | 
					    const filters = { userId: authUser.id, ...dto };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let assets: SearchResult<AssetEntity>;
 | 
					    let assets: SearchResult<AssetEntity>;
 | 
				
			||||||
    switch (strategy) {
 | 
					    switch (strategy) {
 | 
				
			||||||
      case SearchStrategy.CLIP:
 | 
					      case SearchStrategy.CLIP:
 | 
				
			||||||
        const clip = await this.machineLearning.encodeText(query);
 | 
					        const clip = await this.machineLearning.encodeText(machineLearning.url, query);
 | 
				
			||||||
        assets = await this.searchRepository.vectorSearch(clip, filters);
 | 
					        assets = await this.searchRepository.vectorSearch(clip, filters);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case SearchStrategy.TEXT:
 | 
					      case SearchStrategy.TEXT:
 | 
				
			||||||
@ -333,12 +326,6 @@ export class SearchService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private assertEnabled() {
 | 
					 | 
				
			||||||
    if (!this.enabled) {
 | 
					 | 
				
			||||||
      throw new BadRequestException('Search is disabled');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async idsToAlbums(ids: string[]): Promise<AlbumEntity[]> {
 | 
					  private async idsToAlbums(ids: string[]): Promise<AlbumEntity[]> {
 | 
				
			||||||
    const entities = await this.albumRepository.getByIds(ids);
 | 
					    const entities = await this.albumRepository.getByIds(ids);
 | 
				
			||||||
    return this.patchAlbums(entities);
 | 
					    return this.patchAlbums(entities);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { IServerVersion } from '@app/domain';
 | 
					import { FeatureFlags, IServerVersion } from '@app/domain';
 | 
				
			||||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
 | 
					import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ServerPingResponse {
 | 
					export class ServerPingResponse {
 | 
				
			||||||
@ -79,10 +79,14 @@ export class ServerMediaTypesResponseDto {
 | 
				
			|||||||
  sidecar!: string[];
 | 
					  sidecar!: string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ServerFeaturesDto {
 | 
					export class ServerFeaturesDto implements FeatureFlags {
 | 
				
			||||||
  machineLearning!: boolean;
 | 
					  clipEncode!: boolean;
 | 
				
			||||||
 | 
					  facialRecognition!: boolean;
 | 
				
			||||||
 | 
					  sidecar!: boolean;
 | 
				
			||||||
  search!: boolean;
 | 
					  search!: boolean;
 | 
				
			||||||
 | 
					  tagImage!: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO: use these instead of `POST oauth/config`
 | 
				
			||||||
  oauth!: boolean;
 | 
					  oauth!: boolean;
 | 
				
			||||||
  oauthAutoLaunch!: boolean;
 | 
					  oauthAutoLaunch!: boolean;
 | 
				
			||||||
  passwordLogin!: boolean;
 | 
					  passwordLogin!: boolean;
 | 
				
			||||||
 | 
				
			|||||||
@ -147,11 +147,14 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
    describe('getFeatures', () => {
 | 
					    describe('getFeatures', () => {
 | 
				
			||||||
      it('should respond the server features', async () => {
 | 
					      it('should respond the server features', async () => {
 | 
				
			||||||
        await expect(sut.getFeatures()).resolves.toEqual({
 | 
					        await expect(sut.getFeatures()).resolves.toEqual({
 | 
				
			||||||
          machineLearning: true,
 | 
					          clipEncode: true,
 | 
				
			||||||
 | 
					          facialRecognition: true,
 | 
				
			||||||
          oauth: false,
 | 
					          oauth: false,
 | 
				
			||||||
          oauthAutoLaunch: false,
 | 
					          oauthAutoLaunch: false,
 | 
				
			||||||
          passwordLogin: true,
 | 
					          passwordLogin: true,
 | 
				
			||||||
          search: true,
 | 
					          search: true,
 | 
				
			||||||
 | 
					          sidecar: true,
 | 
				
			||||||
 | 
					          tagImage: true,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        expect(configMock.load).toHaveBeenCalled();
 | 
					        expect(configMock.load).toHaveBeenCalled();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,8 @@
 | 
				
			|||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { MACHINE_LEARNING_ENABLED, mimeTypes, SEARCH_ENABLED, serverVersion } from '../domain.constant';
 | 
					import { mimeTypes, serverVersion } from '../domain.constant';
 | 
				
			||||||
import { asHumanReadable } from '../domain.util';
 | 
					import { asHumanReadable } from '../domain.util';
 | 
				
			||||||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 | 
					import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
 | 
				
			||||||
import { ISystemConfigRepository } from '../system-config';
 | 
					import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
 | 
				
			||||||
import { SystemConfigCore } from '../system-config/system-config.core';
 | 
					 | 
				
			||||||
import { IUserRepository, UserStatsQueryResponse } from '../user';
 | 
					import { IUserRepository, UserStatsQueryResponse } from '../user';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ServerFeaturesDto,
 | 
					  ServerFeaturesDto,
 | 
				
			||||||
@ -52,18 +51,8 @@ export class ServerInfoService {
 | 
				
			|||||||
    return serverVersion;
 | 
					    return serverVersion;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getFeatures(): Promise<ServerFeaturesDto> {
 | 
					  getFeatures(): Promise<ServerFeaturesDto> {
 | 
				
			||||||
    const config = await this.configCore.getConfig();
 | 
					    return this.configCore.getFeatures();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      machineLearning: MACHINE_LEARNING_ENABLED,
 | 
					 | 
				
			||||||
      search: SEARCH_ENABLED,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // TODO: use these instead of `POST oauth/config`
 | 
					 | 
				
			||||||
      oauth: config.oauth.enabled,
 | 
					 | 
				
			||||||
      oauthAutoLaunch: config.oauth.autoLaunch,
 | 
					 | 
				
			||||||
      passwordLogin: config.passwordLogin.enabled,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getStats(): Promise<ServerStatsResponseDto> {
 | 
					  async getStats(): Promise<ServerStatsResponseDto> {
 | 
				
			||||||
 | 
				
			|||||||
@ -20,8 +20,8 @@ export interface DetectFaceResult {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IMachineLearningRepository {
 | 
					export interface IMachineLearningRepository {
 | 
				
			||||||
  classifyImage(input: MachineLearningInput): Promise<string[]>;
 | 
					  classifyImage(url: string, input: MachineLearningInput): Promise<string[]>;
 | 
				
			||||||
  encodeImage(input: MachineLearningInput): Promise<number[]>;
 | 
					  encodeImage(url: string, input: MachineLearningInput): Promise<number[]>;
 | 
				
			||||||
  encodeText(input: string): Promise<number[]>;
 | 
					  encodeText(url: string, input: string): Promise<number[]>;
 | 
				
			||||||
  detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]>;
 | 
					  detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,9 +5,11 @@ import {
 | 
				
			|||||||
  newJobRepositoryMock,
 | 
					  newJobRepositoryMock,
 | 
				
			||||||
  newMachineLearningRepositoryMock,
 | 
					  newMachineLearningRepositoryMock,
 | 
				
			||||||
  newSmartInfoRepositoryMock,
 | 
					  newSmartInfoRepositoryMock,
 | 
				
			||||||
 | 
					  newSystemConfigRepositoryMock,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
import { IJobRepository, JobName } from '../job';
 | 
					import { IJobRepository, JobName } from '../job';
 | 
				
			||||||
 | 
					import { ISystemConfigRepository } from '../system-config';
 | 
				
			||||||
import { IMachineLearningRepository } from './machine-learning.interface';
 | 
					import { IMachineLearningRepository } from './machine-learning.interface';
 | 
				
			||||||
import { ISmartInfoRepository } from './smart-info.repository';
 | 
					import { ISmartInfoRepository } from './smart-info.repository';
 | 
				
			||||||
import { SmartInfoService } from './smart-info.service';
 | 
					import { SmartInfoService } from './smart-info.service';
 | 
				
			||||||
@ -20,16 +22,18 @@ const asset = {
 | 
				
			|||||||
describe(SmartInfoService.name, () => {
 | 
					describe(SmartInfoService.name, () => {
 | 
				
			||||||
  let sut: SmartInfoService;
 | 
					  let sut: SmartInfoService;
 | 
				
			||||||
  let assetMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
 | 
					  let configMock: jest.Mocked<ISystemConfigRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let smartMock: jest.Mocked<ISmartInfoRepository>;
 | 
					  let smartMock: jest.Mocked<ISmartInfoRepository>;
 | 
				
			||||||
  let machineMock: jest.Mocked<IMachineLearningRepository>;
 | 
					  let machineMock: jest.Mocked<IMachineLearningRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
 | 
					    configMock = newSystemConfigRepositoryMock();
 | 
				
			||||||
    smartMock = newSmartInfoRepositoryMock();
 | 
					    smartMock = newSmartInfoRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    machineMock = newMachineLearningRepositoryMock();
 | 
					    machineMock = newMachineLearningRepositoryMock();
 | 
				
			||||||
    sut = new SmartInfoService(assetMock, jobMock, smartMock, machineMock);
 | 
					    sut = new SmartInfoService(assetMock, configMock, jobMock, smartMock, machineMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assetMock.getByIds.mockResolvedValue([asset]);
 | 
					    assetMock.getByIds.mockResolvedValue([asset]);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -80,7 +84,9 @@ describe(SmartInfoService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await sut.handleClassifyImage({ id: asset.id });
 | 
					      await sut.handleClassifyImage({ id: asset.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(machineMock.classifyImage).toHaveBeenCalledWith({ imagePath: 'path/to/resize.ext' });
 | 
					      expect(machineMock.classifyImage).toHaveBeenCalledWith('http://immich-machine-learning:3003', {
 | 
				
			||||||
 | 
					        imagePath: 'path/to/resize.ext',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      expect(smartMock.upsert).toHaveBeenCalledWith({
 | 
					      expect(smartMock.upsert).toHaveBeenCalledWith({
 | 
				
			||||||
        assetId: 'asset-1',
 | 
					        assetId: 'asset-1',
 | 
				
			||||||
        tags: ['tag1', 'tag2', 'tag3'],
 | 
					        tags: ['tag1', 'tag2', 'tag3'],
 | 
				
			||||||
@ -139,7 +145,9 @@ describe(SmartInfoService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await sut.handleEncodeClip({ id: asset.id });
 | 
					      await sut.handleEncodeClip({ id: asset.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(machineMock.encodeImage).toHaveBeenCalledWith({ imagePath: 'path/to/resize.ext' });
 | 
					      expect(machineMock.encodeImage).toHaveBeenCalledWith('http://immich-machine-learning:3003', {
 | 
				
			||||||
 | 
					        imagePath: 'path/to/resize.ext',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      expect(smartMock.upsert).toHaveBeenCalledWith({
 | 
					      expect(smartMock.upsert).toHaveBeenCalledWith({
 | 
				
			||||||
        assetId: 'asset-1',
 | 
					        assetId: 'asset-1',
 | 
				
			||||||
        clipEmbedding: [0.01, 0.02, 0.03],
 | 
					        clipEmbedding: [0.01, 0.02, 0.03],
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +1,31 @@
 | 
				
			|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
 | 
					 | 
				
			||||||
import { usePagination } from '../domain.util';
 | 
					import { usePagination } from '../domain.util';
 | 
				
			||||||
import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
 | 
					import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
 | 
				
			||||||
 | 
					import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
 | 
				
			||||||
import { IMachineLearningRepository } from './machine-learning.interface';
 | 
					import { IMachineLearningRepository } from './machine-learning.interface';
 | 
				
			||||||
import { ISmartInfoRepository } from './smart-info.repository';
 | 
					import { ISmartInfoRepository } from './smart-info.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SmartInfoService {
 | 
					export class SmartInfoService {
 | 
				
			||||||
  private logger = new Logger(SmartInfoService.name);
 | 
					  private configCore: SystemConfigCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository,
 | 
					    @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository,
 | 
				
			||||||
    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
					    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
				
			||||||
  ) {}
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.configCore = new SystemConfigCore(configRepository);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleQueueObjectTagging({ force }: IBaseJob) {
 | 
					  async handleQueueObjectTagging({ force }: IBaseJob) {
 | 
				
			||||||
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.tagImageEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
 | 
					    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
 | 
				
			||||||
      return force
 | 
					      return force
 | 
				
			||||||
        ? this.assetRepository.getAll(pagination)
 | 
					        ? this.assetRepository.getAll(pagination)
 | 
				
			||||||
@ -34,19 +42,28 @@ export class SmartInfoService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleClassifyImage({ id }: IEntityJob) {
 | 
					  async handleClassifyImage({ id }: IEntityJob) {
 | 
				
			||||||
    const [asset] = await this.assetRepository.getByIds([id]);
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.tagImageEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) {
 | 
					    const [asset] = await this.assetRepository.getByIds([id]);
 | 
				
			||||||
 | 
					    if (!asset.resizePath) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const tags = await this.machineLearning.classifyImage({ imagePath: asset.resizePath });
 | 
					    const tags = await this.machineLearning.classifyImage(machineLearning.url, { imagePath: asset.resizePath });
 | 
				
			||||||
    await this.repository.upsert({ assetId: asset.id, tags });
 | 
					    await this.repository.upsert({ assetId: asset.id, tags });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleQueueEncodeClip({ force }: IBaseJob) {
 | 
					  async handleQueueEncodeClip({ force }: IBaseJob) {
 | 
				
			||||||
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.clipEncodeEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
 | 
					    const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
 | 
				
			||||||
      return force
 | 
					      return force
 | 
				
			||||||
        ? this.assetRepository.getAll(pagination)
 | 
					        ? this.assetRepository.getAll(pagination)
 | 
				
			||||||
@ -63,13 +80,17 @@ export class SmartInfoService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleEncodeClip({ id }: IEntityJob) {
 | 
					  async handleEncodeClip({ id }: IEntityJob) {
 | 
				
			||||||
    const [asset] = await this.assetRepository.getByIds([id]);
 | 
					    const { machineLearning } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					    if (!machineLearning.enabled || !machineLearning.clipEncodeEnabled) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) {
 | 
					    const [asset] = await this.assetRepository.getByIds([id]);
 | 
				
			||||||
 | 
					    if (!asset.resizePath) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const clipEmbedding = await this.machineLearning.encodeImage({ imagePath: asset.resizePath });
 | 
					    const clipEmbedding = await this.machineLearning.encodeImage(machineLearning.url, { imagePath: asset.resizePath });
 | 
				
			||||||
    await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding });
 | 
					    await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import { IsBoolean, IsUrl, ValidateIf } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SystemConfigMachineLearningDto {
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  enabled!: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsUrl({ require_tld: false })
 | 
				
			||||||
 | 
					  @ValidateIf((dto) => dto.enabled)
 | 
				
			||||||
 | 
					  url!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  clipEncodeEnabled!: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  facialRecognitionEnabled!: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  tagImageEnabled!: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,16 +4,22 @@ import { Type } from 'class-transformer';
 | 
				
			|||||||
import { IsObject, ValidateNested } from 'class-validator';
 | 
					import { IsObject, ValidateNested } from 'class-validator';
 | 
				
			||||||
import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
 | 
					import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
 | 
				
			||||||
import { SystemConfigJobDto } from './system-config-job.dto';
 | 
					import { SystemConfigJobDto } from './system-config-job.dto';
 | 
				
			||||||
 | 
					import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
 | 
				
			||||||
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
 | 
					import { SystemConfigOAuthDto } from './system-config-oauth.dto';
 | 
				
			||||||
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
 | 
					import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
 | 
				
			||||||
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
 | 
					import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class SystemConfigDto {
 | 
					export class SystemConfigDto implements SystemConfig {
 | 
				
			||||||
  @Type(() => SystemConfigFFmpegDto)
 | 
					  @Type(() => SystemConfigFFmpegDto)
 | 
				
			||||||
  @ValidateNested()
 | 
					  @ValidateNested()
 | 
				
			||||||
  @IsObject()
 | 
					  @IsObject()
 | 
				
			||||||
  ffmpeg!: SystemConfigFFmpegDto;
 | 
					  ffmpeg!: SystemConfigFFmpegDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Type(() => SystemConfigMachineLearningDto)
 | 
				
			||||||
 | 
					  @ValidateNested()
 | 
				
			||||||
 | 
					  @IsObject()
 | 
				
			||||||
 | 
					  machineLearning!: SystemConfigMachineLearningDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Type(() => SystemConfigOAuthDto)
 | 
					  @Type(() => SystemConfigOAuthDto)
 | 
				
			||||||
  @ValidateNested()
 | 
					  @ValidateNested()
 | 
				
			||||||
  @IsObject()
 | 
					  @IsObject()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
export * from './dto';
 | 
					export * from './dto';
 | 
				
			||||||
export * from './response-dto';
 | 
					export * from './response-dto';
 | 
				
			||||||
export * from './system-config.constants';
 | 
					export * from './system-config.constants';
 | 
				
			||||||
 | 
					export * from './system-config.core';
 | 
				
			||||||
export * from './system-config.repository';
 | 
					export * from './system-config.repository';
 | 
				
			||||||
export * from './system-config.service';
 | 
					export * from './system-config.service';
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import {
 | 
				
			|||||||
  TranscodePolicy,
 | 
					  TranscodePolicy,
 | 
				
			||||||
  VideoCodec,
 | 
					  VideoCodec,
 | 
				
			||||||
} from '@app/infra/entities';
 | 
					} from '@app/infra/entities';
 | 
				
			||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
 | 
					import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import * as _ from 'lodash';
 | 
					import * as _ from 'lodash';
 | 
				
			||||||
import { Subject } from 'rxjs';
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
import { DeepPartial } from 'typeorm';
 | 
					import { DeepPartial } from 'typeorm';
 | 
				
			||||||
@ -44,6 +44,13 @@ export const defaults = Object.freeze<SystemConfig>({
 | 
				
			|||||||
    [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
 | 
					    [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
 | 
				
			||||||
    [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
 | 
					    [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  machineLearning: {
 | 
				
			||||||
 | 
					    enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
 | 
				
			||||||
 | 
					    url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
 | 
				
			||||||
 | 
					    facialRecognitionEnabled: true,
 | 
				
			||||||
 | 
					    tagImageEnabled: true,
 | 
				
			||||||
 | 
					    clipEncodeEnabled: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  oauth: {
 | 
					  oauth: {
 | 
				
			||||||
    enabled: false,
 | 
					    enabled: false,
 | 
				
			||||||
    issuerUrl: '',
 | 
					    issuerUrl: '',
 | 
				
			||||||
@ -71,6 +78,19 @@ export const defaults = Object.freeze<SystemConfig>({
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum FeatureFlag {
 | 
				
			||||||
 | 
					  CLIP_ENCODE = 'clipEncode',
 | 
				
			||||||
 | 
					  FACIAL_RECOGNITION = 'facialRecognition',
 | 
				
			||||||
 | 
					  TAG_IMAGE = 'tagImage',
 | 
				
			||||||
 | 
					  SIDECAR = 'sidecar',
 | 
				
			||||||
 | 
					  SEARCH = 'search',
 | 
				
			||||||
 | 
					  OAUTH = 'oauth',
 | 
				
			||||||
 | 
					  OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch',
 | 
				
			||||||
 | 
					  PASSWORD_LOGIN = 'passwordLogin',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type FeatureFlags = Record<FeatureFlag, boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const singleton = new Subject<SystemConfig>();
 | 
					const singleton = new Subject<SystemConfig>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
@ -82,6 +102,53 @@ export class SystemConfigCore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(private repository: ISystemConfigRepository) {}
 | 
					  constructor(private repository: ISystemConfigRepository) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async requireFeature(feature: FeatureFlag) {
 | 
				
			||||||
 | 
					    const hasFeature = await this.hasFeature(feature);
 | 
				
			||||||
 | 
					    if (!hasFeature) {
 | 
				
			||||||
 | 
					      switch (feature) {
 | 
				
			||||||
 | 
					        case FeatureFlag.CLIP_ENCODE:
 | 
				
			||||||
 | 
					          throw new BadRequestException('Clip encoding is not enabled');
 | 
				
			||||||
 | 
					        case FeatureFlag.FACIAL_RECOGNITION:
 | 
				
			||||||
 | 
					          throw new BadRequestException('Facial recognition is not enabled');
 | 
				
			||||||
 | 
					        case FeatureFlag.TAG_IMAGE:
 | 
				
			||||||
 | 
					          throw new BadRequestException('Image tagging is not enabled');
 | 
				
			||||||
 | 
					        case FeatureFlag.SIDECAR:
 | 
				
			||||||
 | 
					          throw new BadRequestException('Sidecar is not enabled');
 | 
				
			||||||
 | 
					        case FeatureFlag.SEARCH:
 | 
				
			||||||
 | 
					          throw new BadRequestException('Search is not enabled');
 | 
				
			||||||
 | 
					        case FeatureFlag.OAUTH:
 | 
				
			||||||
 | 
					          throw new BadRequestException('OAuth is not enabled');
 | 
				
			||||||
 | 
					        case FeatureFlag.PASSWORD_LOGIN:
 | 
				
			||||||
 | 
					          throw new BadRequestException('Password login is not enabled');
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          throw new ForbiddenException(`Missing required feature: ${feature}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async hasFeature(feature: FeatureFlag) {
 | 
				
			||||||
 | 
					    const features = await this.getFeatures();
 | 
				
			||||||
 | 
					    return features[feature] ?? false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getFeatures(): Promise<FeatureFlags> {
 | 
				
			||||||
 | 
					    const config = await this.getConfig();
 | 
				
			||||||
 | 
					    const mlEnabled = config.machineLearning.enabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      [FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clipEncodeEnabled,
 | 
				
			||||||
 | 
					      [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognitionEnabled,
 | 
				
			||||||
 | 
					      [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.tagImageEnabled,
 | 
				
			||||||
 | 
					      [FeatureFlag.SIDECAR]: true,
 | 
				
			||||||
 | 
					      [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // TODO: use these instead of `POST oauth/config`
 | 
				
			||||||
 | 
					      [FeatureFlag.OAUTH]: config.oauth.enabled,
 | 
				
			||||||
 | 
					      [FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
 | 
				
			||||||
 | 
					      [FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public getDefaults(): SystemConfig {
 | 
					  public getDefaults(): SystemConfig {
 | 
				
			||||||
    return defaults;
 | 
					    return defaults;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -46,6 +46,13 @@ const updatedConfig = Object.freeze<SystemConfig>({
 | 
				
			|||||||
    accel: TranscodeHWAccel.DISABLED,
 | 
					    accel: TranscodeHWAccel.DISABLED,
 | 
				
			||||||
    tonemap: ToneMapping.HABLE,
 | 
					    tonemap: ToneMapping.HABLE,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  machineLearning: {
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					    url: 'http://immich-machine-learning:3003',
 | 
				
			||||||
 | 
					    facialRecognitionEnabled: true,
 | 
				
			||||||
 | 
					    tagImageEnabled: true,
 | 
				
			||||||
 | 
					    clipEncodeEnabled: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  oauth: {
 | 
					  oauth: {
 | 
				
			||||||
    autoLaunch: true,
 | 
					    autoLaunch: true,
 | 
				
			||||||
    autoRegister: true,
 | 
					    autoRegister: true,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { JobService, MACHINE_LEARNING_ENABLED, SearchService, StorageService } from '@app/domain';
 | 
					import { JobService, SearchService, ServerInfoService, StorageService } from '@app/domain';
 | 
				
			||||||
import { Injectable, Logger } from '@nestjs/common';
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import { Cron, CronExpression } from '@nestjs/schedule';
 | 
					import { Cron, CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,6 +10,7 @@ export class AppService {
 | 
				
			|||||||
    private jobService: JobService,
 | 
					    private jobService: JobService,
 | 
				
			||||||
    private searchService: SearchService,
 | 
					    private searchService: SearchService,
 | 
				
			||||||
    private storageService: StorageService,
 | 
					    private storageService: StorageService,
 | 
				
			||||||
 | 
					    private serverService: ServerInfoService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
 | 
					  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
 | 
				
			||||||
@ -20,8 +21,6 @@ export class AppService {
 | 
				
			|||||||
  async init() {
 | 
					  async init() {
 | 
				
			||||||
    this.storageService.init();
 | 
					    this.storageService.init();
 | 
				
			||||||
    await this.searchService.init();
 | 
					    await this.searchService.init();
 | 
				
			||||||
 | 
					    this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`);
 | 
				
			||||||
    this.logger.log(`Machine learning is ${MACHINE_LEARNING_ENABLED ? 'enabled' : 'disabled'}`);
 | 
					 | 
				
			||||||
    this.logger.log(`Search is ${this.searchService.isEnabled() ? 'enabled' : 'disabled'}`);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,4 @@
 | 
				
			|||||||
import {
 | 
					import { AuthUserDto, SearchDto, SearchExploreResponseDto, SearchResponseDto, SearchService } from '@app/domain';
 | 
				
			||||||
  AuthUserDto,
 | 
					 | 
				
			||||||
  SearchConfigResponseDto,
 | 
					 | 
				
			||||||
  SearchDto,
 | 
					 | 
				
			||||||
  SearchExploreResponseDto,
 | 
					 | 
				
			||||||
  SearchResponseDto,
 | 
					 | 
				
			||||||
  SearchService,
 | 
					 | 
				
			||||||
} from '@app/domain';
 | 
					 | 
				
			||||||
import { Controller, Get, Query } from '@nestjs/common';
 | 
					import { Controller, Get, Query } from '@nestjs/common';
 | 
				
			||||||
import { ApiTags } from '@nestjs/swagger';
 | 
					import { ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
import { Authenticated, AuthUser } from '../app.guard';
 | 
					import { Authenticated, AuthUser } from '../app.guard';
 | 
				
			||||||
@ -23,11 +16,6 @@ export class SearchController {
 | 
				
			|||||||
    return this.service.search(authUser, dto);
 | 
					    return this.service.search(authUser, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get('config')
 | 
					 | 
				
			||||||
  getSearchConfig(): SearchConfigResponseDto {
 | 
					 | 
				
			||||||
    return this.service.getConfig();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Get('explore')
 | 
					  @Get('explore')
 | 
				
			||||||
  getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> {
 | 
					  getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> {
 | 
				
			||||||
    return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>;
 | 
					    return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>;
 | 
				
			||||||
 | 
				
			|||||||
@ -37,6 +37,12 @@ export enum SystemConfigKey {
 | 
				
			|||||||
  JOB_SEARCH_CONCURRENCY = 'job.search.concurrency',
 | 
					  JOB_SEARCH_CONCURRENCY = 'job.search.concurrency',
 | 
				
			||||||
  JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency',
 | 
					  JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MACHINE_LEARNING_ENABLED = 'machineLearning.enabled',
 | 
				
			||||||
 | 
					  MACHINE_LEARNING_URL = 'machineLearning.url',
 | 
				
			||||||
 | 
					  MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED = 'machineLearning.facialRecognitionEnabled',
 | 
				
			||||||
 | 
					  MACHINE_LEARNING_TAG_IMAGE_ENABLED = 'machineLearning.tagImageEnabled',
 | 
				
			||||||
 | 
					  MACHINE_LEARNING_CLIP_ENCODE_ENABLED = 'machineLearning.clipEncodeEnabled',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  OAUTH_ENABLED = 'oauth.enabled',
 | 
					  OAUTH_ENABLED = 'oauth.enabled',
 | 
				
			||||||
  OAUTH_ISSUER_URL = 'oauth.issuerUrl',
 | 
					  OAUTH_ISSUER_URL = 'oauth.issuerUrl',
 | 
				
			||||||
  OAUTH_CLIENT_ID = 'oauth.clientId',
 | 
					  OAUTH_CLIENT_ID = 'oauth.clientId',
 | 
				
			||||||
@ -105,6 +111,13 @@ export interface SystemConfig {
 | 
				
			|||||||
    tonemap: ToneMapping;
 | 
					    tonemap: ToneMapping;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  job: Record<QueueName, { concurrency: number }>;
 | 
					  job: Record<QueueName, { concurrency: number }>;
 | 
				
			||||||
 | 
					  machineLearning: {
 | 
				
			||||||
 | 
					    enabled: boolean;
 | 
				
			||||||
 | 
					    url: string;
 | 
				
			||||||
 | 
					    clipEncodeEnabled: boolean;
 | 
				
			||||||
 | 
					    facialRecognitionEnabled: boolean;
 | 
				
			||||||
 | 
					    tagImageEnabled: boolean;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
  oauth: {
 | 
					  oauth: {
 | 
				
			||||||
    enabled: boolean;
 | 
					    enabled: boolean;
 | 
				
			||||||
    issuerUrl: string;
 | 
					    issuerUrl: string;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { DetectFaceResult, IMachineLearningRepository, MachineLearningInput, MACHINE_LEARNING_URL } from '@app/domain';
 | 
					import { DetectFaceResult, IMachineLearningRepository, MachineLearningInput } from '@app/domain';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
import { createReadStream } from 'fs';
 | 
					import { createReadStream } from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const client = axios.create({ baseURL: MACHINE_LEARNING_URL });
 | 
					const client = axios.create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class MachineLearningRepository implements IMachineLearningRepository {
 | 
					export class MachineLearningRepository implements IMachineLearningRepository {
 | 
				
			||||||
@ -11,19 +11,19 @@ export class MachineLearningRepository implements IMachineLearningRepository {
 | 
				
			|||||||
    return client.post<T>(endpoint, createReadStream(input.imagePath)).then((res) => res.data);
 | 
					    return client.post<T>(endpoint, createReadStream(input.imagePath)).then((res) => res.data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  classifyImage(input: MachineLearningInput): Promise<string[]> {
 | 
					  classifyImage(url: string, input: MachineLearningInput): Promise<string[]> {
 | 
				
			||||||
    return this.post<string[]>(input, '/image-classifier/tag-image');
 | 
					    return this.post<string[]>(input, `${url}/image-classifier/tag-image`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]> {
 | 
					  detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]> {
 | 
				
			||||||
    return this.post<DetectFaceResult[]>(input, '/facial-recognition/detect-faces');
 | 
					    return this.post<DetectFaceResult[]>(input, `${url}/facial-recognition/detect-faces`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  encodeImage(input: MachineLearningInput): Promise<number[]> {
 | 
					  encodeImage(url: string, input: MachineLearningInput): Promise<number[]> {
 | 
				
			||||||
    return this.post<number[]>(input, '/sentence-transformer/encode-image');
 | 
					    return this.post<number[]>(input, `${url}/sentence-transformer/encode-image`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  encodeText(input: string): Promise<number[]> {
 | 
					  encodeText(url: string, input: string): Promise<number[]> {
 | 
				
			||||||
    return client.post<number[]>('/sentence-transformer/encode-text', { text: input }).then((res) => res.data);
 | 
					    return client.post<number[]>(`${url}/sentence-transformer/encode-text`, { text: input }).then((res) => res.data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										141
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										141
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'total': number;
 | 
					    'total': number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * @export
 | 
					 | 
				
			||||||
 * @interface SearchConfigResponseDto
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export interface SearchConfigResponseDto {
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {boolean}
 | 
					 | 
				
			||||||
     * @memberof SearchConfigResponseDto
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'enabled': boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto {
 | 
				
			|||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
     * @memberof ServerFeaturesDto
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'machineLearning': boolean;
 | 
					    'clipEncode': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'facialRecognition': boolean;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {boolean}
 | 
					     * @type {boolean}
 | 
				
			||||||
@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto {
 | 
				
			|||||||
     * @memberof ServerFeaturesDto
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'search': boolean;
 | 
					    'search': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'sidecar': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof ServerFeaturesDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'tagImage': boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
@ -2611,6 +2616,12 @@ export interface SystemConfigDto {
 | 
				
			|||||||
     * @memberof SystemConfigDto
 | 
					     * @memberof SystemConfigDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'job': SystemConfigJobDto;
 | 
					    'job': SystemConfigJobDto;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {SystemConfigMachineLearningDto}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'machineLearning': SystemConfigMachineLearningDto;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {SystemConfigOAuthDto}
 | 
					     * @type {SystemConfigOAuthDto}
 | 
				
			||||||
@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'videoConversion': JobSettingsDto;
 | 
					    'videoConversion': JobSettingsDto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SystemConfigMachineLearningDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'clipEncodeEnabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'enabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'facialRecognitionEnabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'tagImageEnabled': boolean;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SystemConfigMachineLearningDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'url': string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
                url: toPathString(localVarUrlObj),
 | 
					 | 
				
			||||||
                options: localVarRequestOptions,
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					 | 
				
			||||||
            const localVarPath = `/search/config`;
 | 
					 | 
				
			||||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
					 | 
				
			||||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
					 | 
				
			||||||
            let baseOptions;
 | 
					 | 
				
			||||||
            if (configuration) {
 | 
					 | 
				
			||||||
                baseOptions = configuration.baseOptions;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
					 | 
				
			||||||
            const localVarHeaderParameter = {} as any;
 | 
					 | 
				
			||||||
            const localVarQueryParameter = {} as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication cookie required
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication api_key required
 | 
					 | 
				
			||||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // authentication bearer required
 | 
					 | 
				
			||||||
            // http bearer authentication required
 | 
					 | 
				
			||||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> {
 | 
					 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options);
 | 
					 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {string} [q] 
 | 
					         * @param {string} [q] 
 | 
				
			||||||
@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
 | 
				
			|||||||
        getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
 | 
					        getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
 | 
				
			||||||
            return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * 
 | 
					 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
         * @throws {RequiredError}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> {
 | 
					 | 
				
			||||||
            return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath));
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
					         * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
				
			||||||
@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI {
 | 
				
			|||||||
        return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
 | 
					        return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					 | 
				
			||||||
     * @throws {RequiredError}
 | 
					 | 
				
			||||||
     * @memberof SearchApi
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public getSearchConfig(options?: AxiosRequestConfig) {
 | 
					 | 
				
			||||||
        return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
					     * @param {SearchApiSearchRequest} requestParameters Request parameters.
 | 
				
			||||||
 | 
				
			|||||||
@ -70,25 +70,26 @@
 | 
				
			|||||||
      subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
 | 
					      subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
 | 
				
			||||||
      allText: 'SYNC',
 | 
					      allText: 'SYNC',
 | 
				
			||||||
      missingText: 'DISCOVER',
 | 
					      missingText: 'DISCOVER',
 | 
				
			||||||
 | 
					      disabled: !$featureFlags.sidecar,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [JobName.ObjectTagging]: {
 | 
					    [JobName.ObjectTagging]: {
 | 
				
			||||||
      icon: TagMultiple,
 | 
					      icon: TagMultiple,
 | 
				
			||||||
      title: api.getJobName(JobName.ObjectTagging),
 | 
					      title: api.getJobName(JobName.ObjectTagging),
 | 
				
			||||||
      subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
 | 
					      subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
 | 
				
			||||||
      disabled: !$featureFlags.machineLearning,
 | 
					      disabled: !$featureFlags.tagImage,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [JobName.ClipEncoding]: {
 | 
					    [JobName.ClipEncoding]: {
 | 
				
			||||||
      icon: VectorCircle,
 | 
					      icon: VectorCircle,
 | 
				
			||||||
      title: api.getJobName(JobName.ClipEncoding),
 | 
					      title: api.getJobName(JobName.ClipEncoding),
 | 
				
			||||||
      subtitle: 'Run machine learning to generate clip embeddings',
 | 
					      subtitle: 'Run machine learning to generate clip embeddings',
 | 
				
			||||||
      disabled: !$featureFlags.machineLearning,
 | 
					      disabled: !$featureFlags.clipEncode,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [JobName.RecognizeFaces]: {
 | 
					    [JobName.RecognizeFaces]: {
 | 
				
			||||||
      icon: FaceRecognition,
 | 
					      icon: FaceRecognition,
 | 
				
			||||||
      title: api.getJobName(JobName.RecognizeFaces),
 | 
					      title: api.getJobName(JobName.RecognizeFaces),
 | 
				
			||||||
      subtitle: 'Run machine learning to recognize faces',
 | 
					      subtitle: 'Run machine learning to recognize faces',
 | 
				
			||||||
      handleCommand: handleFaceCommand,
 | 
					      handleCommand: handleFaceCommand,
 | 
				
			||||||
      disabled: !$featureFlags.machineLearning,
 | 
					      disabled: !$featureFlags.facialRecognition,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [JobName.VideoConversion]: {
 | 
					    [JobName.VideoConversion]: {
 | 
				
			||||||
      icon: Video,
 | 
					      icon: Video,
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import {
 | 
				
			||||||
 | 
					    notificationController,
 | 
				
			||||||
 | 
					    NotificationType,
 | 
				
			||||||
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { api, SystemConfigDto } from '@api';
 | 
				
			||||||
 | 
					  import { isEqual } from 'lodash-es';
 | 
				
			||||||
 | 
					  import { fade } from 'svelte/transition';
 | 
				
			||||||
 | 
					  import SettingButtonsRow from '../setting-buttons-row.svelte';
 | 
				
			||||||
 | 
					  import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
 | 
				
			||||||
 | 
					  import SettingSwitch from '../setting-switch.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let config: SystemConfigDto;
 | 
				
			||||||
 | 
					  let defaultConfig: SystemConfigDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function refreshConfig() {
 | 
				
			||||||
 | 
					    [config, defaultConfig] = await Promise.all([
 | 
				
			||||||
 | 
					      api.systemConfigApi.getConfig().then((res) => res.data),
 | 
				
			||||||
 | 
					      api.systemConfigApi.getDefaults().then((res) => res.data),
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function reset() {
 | 
				
			||||||
 | 
					    const { data: resetConfig } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
					    config = resetConfig;
 | 
				
			||||||
 | 
					    notificationController.show({ message: 'Reset to the last saved settings', type: NotificationType.Info });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function saveSetting() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const { data: current } = await api.systemConfigApi.getConfig();
 | 
				
			||||||
 | 
					      await api.systemConfigApi.updateConfig({
 | 
				
			||||||
 | 
					        systemConfigDto: { ...current, machineLearning: config.machineLearning },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      await refreshConfig();
 | 
				
			||||||
 | 
					      notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      handleError(error, 'Unable to save settings');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function resetToDefault() {
 | 
				
			||||||
 | 
					    await refreshConfig();
 | 
				
			||||||
 | 
					    const { data: defaults } = await api.systemConfigApi.getDefaults();
 | 
				
			||||||
 | 
					    config = defaults;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notificationController.show({ message: 'Reset settings to defaults', type: NotificationType.Info });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="mt-2">
 | 
				
			||||||
 | 
					  {#await refreshConfig() then}
 | 
				
			||||||
 | 
					    <div in:fade={{ duration: 500 }}>
 | 
				
			||||||
 | 
					      <form autocomplete="off" on:submit|preventDefault class="mx-4 flex flex-col gap-4 py-4">
 | 
				
			||||||
 | 
					        <SettingSwitch
 | 
				
			||||||
 | 
					          title="Enabled"
 | 
				
			||||||
 | 
					          subtitle="Use machine learning features"
 | 
				
			||||||
 | 
					          bind:checked={config.machineLearning.enabled}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <hr />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <SettingInputField
 | 
				
			||||||
 | 
					          inputType={SettingInputFieldType.TEXT}
 | 
				
			||||||
 | 
					          label="URL"
 | 
				
			||||||
 | 
					          desc="URL of machine learning server"
 | 
				
			||||||
 | 
					          bind:value={config.machineLearning.url}
 | 
				
			||||||
 | 
					          required={true}
 | 
				
			||||||
 | 
					          disabled={!config.machineLearning.enabled}
 | 
				
			||||||
 | 
					          isEdited={!(config.machineLearning.url === config.machineLearning.url)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <SettingSwitch
 | 
				
			||||||
 | 
					          title="SMART SEARCH"
 | 
				
			||||||
 | 
					          subtitle="Extract CLIP embeddings for smart search"
 | 
				
			||||||
 | 
					          bind:checked={config.machineLearning.clipEncodeEnabled}
 | 
				
			||||||
 | 
					          disabled={!config.machineLearning.enabled}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <SettingSwitch
 | 
				
			||||||
 | 
					          title="FACIAL RECOGNITION"
 | 
				
			||||||
 | 
					          subtitle="Recognize and group faces in photos"
 | 
				
			||||||
 | 
					          disabled={!config.machineLearning.enabled}
 | 
				
			||||||
 | 
					          bind:checked={config.machineLearning.facialRecognitionEnabled}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <SettingSwitch
 | 
				
			||||||
 | 
					          title="IMAGE TAGGING"
 | 
				
			||||||
 | 
					          subtitle="Tag and classify images"
 | 
				
			||||||
 | 
					          disabled={!config.machineLearning.enabled}
 | 
				
			||||||
 | 
					          bind:checked={config.machineLearning.tagImageEnabled}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <SettingButtonsRow
 | 
				
			||||||
 | 
					          on:reset={reset}
 | 
				
			||||||
 | 
					          on:save={saveSetting}
 | 
				
			||||||
 | 
					          on:reset-to-default={resetToDefault}
 | 
				
			||||||
 | 
					          showResetToDefault={!isEqual(config, defaultConfig)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  {/await}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -32,9 +32,9 @@
 | 
				
			|||||||
    <input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} />
 | 
					    <input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if disabled}
 | 
					    {#if disabled}
 | 
				
			||||||
      <span class="slider-disable" />
 | 
					      <span class="slider-disable cursor-not-allowed" />
 | 
				
			||||||
    {:else}
 | 
					    {:else}
 | 
				
			||||||
      <span class="slider" />
 | 
					      <span class="slider cursor-pointer" />
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
  </label>
 | 
					  </label>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
@ -43,7 +43,6 @@
 | 
				
			|||||||
  .slider,
 | 
					  .slider,
 | 
				
			||||||
  .slider-disable {
 | 
					  .slider-disable {
 | 
				
			||||||
    position: absolute;
 | 
					    position: absolute;
 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
    top: 0;
 | 
					    top: 0;
 | 
				
			||||||
    left: 0;
 | 
					    left: 0;
 | 
				
			||||||
    right: 0;
 | 
					    right: 0;
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,10 @@ import { writable } from 'svelte/store';
 | 
				
			|||||||
export type FeatureFlags = ServerFeaturesDto;
 | 
					export type FeatureFlags = ServerFeaturesDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const featureFlags = writable<FeatureFlags>({
 | 
					export const featureFlags = writable<FeatureFlags>({
 | 
				
			||||||
  machineLearning: true,
 | 
					  clipEncode: true,
 | 
				
			||||||
 | 
					  facialRecognition: true,
 | 
				
			||||||
 | 
					  sidecar: true,
 | 
				
			||||||
 | 
					  tagImage: true,
 | 
				
			||||||
  search: true,
 | 
					  search: true,
 | 
				
			||||||
  oauth: true,
 | 
					  oauth: true,
 | 
				
			||||||
  oauthAutoLaunch: true,
 | 
					  oauthAutoLaunch: true,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,12 @@
 | 
				
			|||||||
  import { page } from '$app/stores';
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
  import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
 | 
					  import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
 | 
				
			||||||
  import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
 | 
					  import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
 | 
				
			||||||
  import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
 | 
					  import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte';
 | 
				
			||||||
  import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
 | 
					  import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
 | 
				
			||||||
  import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
 | 
					  import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
 | 
				
			||||||
  import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
 | 
					  import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
 | 
				
			||||||
  import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
 | 
					  import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
 | 
				
			||||||
 | 
					  import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
 | 
				
			||||||
  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
					  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
				
			||||||
  import { api } from '@api';
 | 
					  import { api } from '@api';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
@ -50,6 +51,10 @@
 | 
				
			|||||||
      <OAuthSettings oauthConfig={configs.oauth} />
 | 
					      <OAuthSettings oauthConfig={configs.oauth} />
 | 
				
			||||||
    </SettingAccordion>
 | 
					    </SettingAccordion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <SettingAccordion title="Machine Learning" subtitle="Manage machine learning settings">
 | 
				
			||||||
 | 
					      <MachineLearningSettings />
 | 
				
			||||||
 | 
					    </SettingAccordion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <SettingAccordion
 | 
					    <SettingAccordion
 | 
				
			||||||
      title="Storage Template"
 | 
					      title="Storage Template"
 | 
				
			||||||
      subtitle="Manage the folder structure and file name of the upload asset"
 | 
					      subtitle="Manage the folder structure and file name of the upload asset"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user