1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(server, web)!: Move reverse geocoding settings to the UI (#4222)

* feat: reverse geocoding settings

* chore: open api

* re-init geocoder if precision has been updated

* update docs

* chore: update verbiage

* fix: re-init logic

* fix: reset to default

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Daniel Dietzler 2023-09-26 09:03:57 +02:00 committed by GitHub
parent 7bc6e9ef64
commit 9bada51d56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 331 additions and 77 deletions

View File

@ -1055,6 +1055,22 @@ export interface CheckExistingAssetsResponseDto {
*/ */
'existingIds': Array<string>; 'existingIds': Array<string>;
} }
/**
*
* @export
* @enum {string}
*/
export const CitiesFile = {
Cities15000: 'cities15000',
Cities5000: 'cities5000',
Cities1000: 'cities1000',
Cities500: 'cities500'
} as const;
export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile];
/** /**
* *
* @export * @export
@ -2650,6 +2666,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'passwordLogin': boolean; 'passwordLogin': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'reverseGeocoding': boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -3093,6 +3115,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto * @memberof SystemConfigDto
*/ */
'passwordLogin': SystemConfigPasswordLoginDto; 'passwordLogin': SystemConfigPasswordLoginDto;
/**
*
* @type {SystemConfigReverseGeocodingDto}
* @memberof SystemConfigDto
*/
'reverseGeocoding': SystemConfigReverseGeocodingDto;
/** /**
* *
* @type {SystemConfigStorageTemplateDto} * @type {SystemConfigStorageTemplateDto}
@ -3438,6 +3466,27 @@ export interface SystemConfigPasswordLoginDto {
*/ */
'enabled': boolean; 'enabled': boolean;
} }
/**
*
* @export
* @interface SystemConfigReverseGeocodingDto
*/
export interface SystemConfigReverseGeocodingDto {
/**
*
* @type {CitiesFile}
* @memberof SystemConfigReverseGeocodingDto
*/
'citiesFileOverride': CitiesFile;
/**
*
* @type {boolean}
* @memberof SystemConfigReverseGeocodingDto
*/
'enabled': boolean;
}
/** /**
* *
* @export * @export

View File

@ -49,11 +49,9 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## Geocoding ## Geocoding
| Variable | Description | Default | Services | | Variable | Description | Default | Services |
| :--------------------------------- | :---------------------------------- | :--------------------------: | :------------ | | :--------------------------------- | :------------------------------- | :--------------------------: | :------------ |
| `DISABLE_REVERSE_GEOCODING` | Disable Reverse Geocoding Precision | `false` | microservices | | `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices |
| `REVERSE_GEOCODING_PRECISION` | Reverse Geocoding Precision | `3` | microservices |
| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices |
## Ports ## Ports

View File

@ -43,6 +43,7 @@ doc/CheckDuplicateAssetDto.md
doc/CheckDuplicateAssetResponseDto.md doc/CheckDuplicateAssetResponseDto.md
doc/CheckExistingAssetsDto.md doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md doc/CheckExistingAssetsResponseDto.md
doc/CitiesFile.md
doc/ClassificationConfig.md doc/ClassificationConfig.md
doc/Colorspace.md doc/Colorspace.md
doc/CreateAlbumDto.md doc/CreateAlbumDto.md
@ -126,6 +127,7 @@ doc/SystemConfigMachineLearningDto.md
doc/SystemConfigMapDto.md doc/SystemConfigMapDto.md
doc/SystemConfigOAuthDto.md doc/SystemConfigOAuthDto.md
doc/SystemConfigPasswordLoginDto.md doc/SystemConfigPasswordLoginDto.md
doc/SystemConfigReverseGeocodingDto.md
doc/SystemConfigStorageTemplateDto.md doc/SystemConfigStorageTemplateDto.md
doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThumbnailDto.md doc/SystemConfigThumbnailDto.md
@ -207,6 +209,7 @@ lib/model/check_duplicate_asset_dto.dart
lib/model/check_duplicate_asset_response_dto.dart lib/model/check_duplicate_asset_response_dto.dart
lib/model/check_existing_assets_dto.dart lib/model/check_existing_assets_dto.dart
lib/model/check_existing_assets_response_dto.dart lib/model/check_existing_assets_response_dto.dart
lib/model/cities_file.dart
lib/model/classification_config.dart lib/model/classification_config.dart
lib/model/clip_config.dart lib/model/clip_config.dart
lib/model/clip_mode.dart lib/model/clip_mode.dart
@ -284,6 +287,7 @@ lib/model/system_config_machine_learning_dto.dart
lib/model/system_config_map_dto.dart lib/model/system_config_map_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_reverse_geocoding_dto.dart
lib/model/system_config_storage_template_dto.dart lib/model/system_config_storage_template_dto.dart
lib/model/system_config_template_storage_option_dto.dart lib/model/system_config_template_storage_option_dto.dart
lib/model/system_config_thumbnail_dto.dart lib/model/system_config_thumbnail_dto.dart
@ -343,6 +347,7 @@ test/check_duplicate_asset_dto_test.dart
test/check_duplicate_asset_response_dto_test.dart test/check_duplicate_asset_response_dto_test.dart
test/check_existing_assets_dto_test.dart test/check_existing_assets_dto_test.dart
test/check_existing_assets_response_dto_test.dart test/check_existing_assets_response_dto_test.dart
test/cities_file_test.dart
test/classification_config_test.dart test/classification_config_test.dart
test/clip_config_test.dart test/clip_config_test.dart
test/clip_mode_test.dart test/clip_mode_test.dart
@ -429,6 +434,7 @@ test/system_config_machine_learning_dto_test.dart
test/system_config_map_dto_test.dart test/system_config_map_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_reverse_geocoding_dto_test.dart
test/system_config_storage_template_dto_test.dart test/system_config_storage_template_dto_test.dart
test/system_config_template_storage_option_dto_test.dart test/system_config_template_storage_option_dto_test.dart
test/system_config_thumbnail_dto_test.dart test/system_config_thumbnail_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

BIN
mobile/openapi/doc/CitiesFile.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/lib/model/cities_file.dart generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/test/cities_file_test.dart generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -5914,6 +5914,15 @@
], ],
"type": "object" "type": "object"
}, },
"CitiesFile": {
"enum": [
"cities15000",
"cities5000",
"cities1000",
"cities500"
],
"type": "string"
},
"ClassificationConfig": { "ClassificationConfig": {
"properties": { "properties": {
"enabled": { "enabled": {
@ -7229,6 +7238,9 @@
"passwordLogin": { "passwordLogin": {
"type": "boolean" "type": "boolean"
}, },
"reverseGeocoding": {
"type": "boolean"
},
"search": { "search": {
"type": "boolean" "type": "boolean"
}, },
@ -7244,6 +7256,7 @@
"configFile", "configFile",
"facialRecognition", "facialRecognition",
"map", "map",
"reverseGeocoding",
"oauth", "oauth",
"oauthAutoLaunch", "oauthAutoLaunch",
"passwordLogin", "passwordLogin",
@ -7590,6 +7603,9 @@
"passwordLogin": { "passwordLogin": {
"$ref": "#/components/schemas/SystemConfigPasswordLoginDto" "$ref": "#/components/schemas/SystemConfigPasswordLoginDto"
}, },
"reverseGeocoding": {
"$ref": "#/components/schemas/SystemConfigReverseGeocodingDto"
},
"storageTemplate": { "storageTemplate": {
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto" "$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
}, },
@ -7603,6 +7619,7 @@
"map", "map",
"oauth", "oauth",
"passwordLogin", "passwordLogin",
"reverseGeocoding",
"storageTemplate", "storageTemplate",
"job", "job",
"thumbnail" "thumbnail"
@ -7843,6 +7860,21 @@
], ],
"type": "object" "type": "object"
}, },
"SystemConfigReverseGeocodingDto": {
"properties": {
"citiesFileOverride": {
"$ref": "#/components/schemas/CitiesFile"
},
"enabled": {
"type": "boolean"
}
},
"required": [
"citiesFileOverride",
"enabled"
],
"type": "object"
},
"SystemConfigStorageTemplateDto": { "SystemConfigStorageTemplateDto": {
"properties": { "properties": {
"template": { "template": {

View File

@ -1,3 +1,5 @@
import { InitOptions } from 'local-reverse-geocoder';
export const IGeocodingRepository = 'IGeocodingRepository'; export const IGeocodingRepository = 'IGeocodingRepository';
export interface GeoPoint { export interface GeoPoint {
@ -12,7 +14,7 @@ export interface ReverseGeocodeResult {
} }
export interface IGeocodingRepository { export interface IGeocodingRepository {
init(): Promise<void>; init(options: Partial<InitOptions>): Promise<void>;
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>; reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
deleteCache(): Promise<void>; deleteCache(): Promise<void>;
} }

View File

@ -90,6 +90,7 @@ export class ServerFeaturesDto implements FeatureFlags {
configFile!: boolean; configFile!: boolean;
facialRecognition!: boolean; facialRecognition!: boolean;
map!: boolean; map!: boolean;
reverseGeocoding!: boolean;
oauth!: boolean; oauth!: boolean;
oauthAutoLaunch!: boolean; oauthAutoLaunch!: boolean;
passwordLogin!: boolean; passwordLogin!: boolean;

View File

@ -151,6 +151,7 @@ describe(ServerInfoService.name, () => {
clipEncode: true, clipEncode: true,
facialRecognition: true, facialRecognition: true,
map: true, map: true,
reverseGeocoding: true,
oauth: false, oauth: false,
oauthAutoLaunch: false, oauthAutoLaunch: false,
passwordLogin: true, passwordLogin: true,

View File

@ -0,0 +1,12 @@
import { CitiesFile } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsEnum } from 'class-validator';
export class SystemConfigReverseGeocodingDto {
@IsBoolean()
enabled!: boolean;
@IsEnum(CitiesFile)
@ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' })
citiesFileOverride!: CitiesFile;
}

View File

@ -8,6 +8,7 @@ import { SystemConfigMachineLearningDto } from './system-config-machine-learning
import { SystemConfigMapDto } from './system-config-map.dto'; import { SystemConfigMapDto } from './system-config-map.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 { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto';
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
export class SystemConfigDto implements SystemConfig { export class SystemConfigDto implements SystemConfig {
@ -36,6 +37,11 @@ export class SystemConfigDto implements SystemConfig {
@IsObject() @IsObject()
passwordLogin!: SystemConfigPasswordLoginDto; passwordLogin!: SystemConfigPasswordLoginDto;
@Type(() => SystemConfigReverseGeocodingDto)
@ValidateNested()
@IsObject()
reverseGeocoding!: SystemConfigReverseGeocodingDto;
@Type(() => SystemConfigStorageTemplateDto) @Type(() => SystemConfigStorageTemplateDto)
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()

View File

@ -1,6 +1,7 @@
import { import {
AudioCodec, AudioCodec,
CQMode, CQMode,
CitiesFile,
Colorspace, Colorspace,
SystemConfig, SystemConfig,
SystemConfigEntity, SystemConfigEntity,
@ -81,6 +82,10 @@ export const defaults = Object.freeze<SystemConfig>({
enabled: true, enabled: true,
tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
}, },
reverseGeocoding: {
enabled: true,
citiesFileOverride: CitiesFile.CITIES_500,
},
oauth: { oauth: {
enabled: false, enabled: false,
issuerUrl: '', issuerUrl: '',
@ -115,6 +120,7 @@ export enum FeatureFlag {
FACIAL_RECOGNITION = 'facialRecognition', FACIAL_RECOGNITION = 'facialRecognition',
TAG_IMAGE = 'tagImage', TAG_IMAGE = 'tagImage',
MAP = 'map', MAP = 'map',
REVERSE_GEOCODING = 'reverseGeocoding',
SIDECAR = 'sidecar', SIDECAR = 'sidecar',
SEARCH = 'search', SEARCH = 'search',
OAUTH = 'oauth', OAUTH = 'oauth',
@ -177,6 +183,7 @@ export class SystemConfigCore {
[FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled, [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
[FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled, [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled,
[FeatureFlag.MAP]: config.map.enabled, [FeatureFlag.MAP]: config.map.enabled,
[FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled,
[FeatureFlag.SIDECAR]: true, [FeatureFlag.SIDECAR]: true,
[FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false', [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',

View File

@ -1,6 +1,7 @@
import { import {
AudioCodec, AudioCodec,
CQMode, CQMode,
CitiesFile,
Colorspace, Colorspace,
SystemConfig, SystemConfig,
SystemConfigEntity, SystemConfigEntity,
@ -80,6 +81,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
enabled: true, enabled: true,
tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
}, },
reverseGeocoding: {
enabled: true,
citiesFileOverride: CitiesFile.CITIES_500,
},
oauth: { oauth: {
autoLaunch: true, autoLaunch: true,
autoRegister: true, autoRegister: true,

View File

@ -64,6 +64,9 @@ export enum SystemConfigKey {
MAP_ENABLED = 'map.enabled', MAP_ENABLED = 'map.enabled',
MAP_TILE_URL = 'map.tileUrl', MAP_TILE_URL = 'map.tileUrl',
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
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',
@ -130,6 +133,13 @@ export enum Colorspace {
P3 = 'p3', P3 = 'p3',
} }
export enum CitiesFile {
CITIES_15000 = 'cities15000',
CITIES_5000 = 'cities5000',
CITIES_1000 = 'cities1000',
CITIES_500 = 'cities500',
}
export interface SystemConfig { export interface SystemConfig {
ffmpeg: { ffmpeg: {
crf: number; crf: number;
@ -175,6 +185,10 @@ export interface SystemConfig {
enabled: boolean; enabled: boolean;
tileUrl: string; tileUrl: string;
}; };
reverseGeocoding: {
enabled: boolean;
citiesFileOverride: CitiesFile;
};
oauth: { oauth: {
enabled: boolean; enabled: boolean;
issuerUrl: string; issuerUrl: string;

View File

@ -2,7 +2,6 @@ import { QueueName } from '@app/domain';
import { RegisterQueueOptions } from '@nestjs/bullmq'; import { RegisterQueueOptions } from '@nestjs/bullmq';
import { QueueOptions } from 'bullmq'; import { QueueOptions } from 'bullmq';
import { RedisOptions } from 'ioredis'; import { RedisOptions } from 'ioredis';
import { InitOptions } from 'local-reverse-geocoder';
import { ConfigurationOptions } from 'typesense/lib/Typesense/Configuration'; import { ConfigurationOptions } from 'typesense/lib/Typesense/Configuration';
function parseRedisConfig(): RedisOptions { function parseRedisConfig(): RedisOptions {
@ -72,20 +71,5 @@ function parseTypeSenseConfig(): ConfigurationOptions {
export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig(); export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig();
function parseLocalGeocodingConfig(): InitOptions { export const REVERSE_GEOCODING_DUMP_DIRECTORY =
const precision = Number(process.env.REVERSE_GEOCODING_PRECISION); process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/';
return {
citiesFileOverride: precision ? ['cities15000', 'cities5000', 'cities1000', 'cities500'][precision] : undefined,
load: {
admin1: true,
admin2: true,
admin3And4: false,
alternateNames: false,
},
countries: [],
dumpDirectory: process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/',
};
}
export const localGeocodingConfig: InitOptions = parseLocalGeocodingConfig();

View File

@ -1,9 +1,9 @@
import { GeoPoint, IGeocodingRepository, ReverseGeocodeResult } from '@app/domain'; import { GeoPoint, IGeocodingRepository, ReverseGeocodeResult } from '@app/domain';
import { localGeocodingConfig } from '@app/infra'; import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { readdir, rm } from 'fs/promises'; import { readdir, rm } from 'fs/promises';
import { getName } from 'i18n-iso-countries'; import { getName } from 'i18n-iso-countries';
import geocoder, { AddressObject } from 'local-reverse-geocoder'; import geocoder, { AddressObject, InitOptions } from 'local-reverse-geocoder';
import path from 'path'; import path from 'path';
import { promisify } from 'util'; import { promisify } from 'util';
@ -18,19 +18,33 @@ export type GeoData = AddressObject & {
admin2Code?: AdminCode | string; admin2Code?: AdminCode | string;
}; };
const init = (): Promise<void> => new Promise<void>((resolve) => geocoder.init(localGeocodingConfig, resolve));
const lookup = promisify<GeoPoint[], number, AddressObject[][]>(geocoder.lookUp).bind(geocoder); const lookup = promisify<GeoPoint[], number, AddressObject[][]>(geocoder.lookUp).bind(geocoder);
@Injectable() @Injectable()
export class GeocodingRepository implements IGeocodingRepository { export class GeocodingRepository implements IGeocodingRepository {
private logger = new Logger(GeocodingRepository.name); private logger = new Logger(GeocodingRepository.name);
async init(): Promise<void> { async init(options: Partial<InitOptions>): Promise<void> {
await init(); return new Promise<void>((resolve) => {
geocoder.init(
{
load: {
admin1: true,
admin2: true,
admin3And4: false,
alternateNames: false,
},
countries: [],
dumpDirectory: REVERSE_GEOCODING_DUMP_DIRECTORY,
...options,
},
resolve,
);
});
} }
async deleteCache() { async deleteCache() {
const dumpDirectory = localGeocodingConfig.dumpDirectory; const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY;
if (dumpDirectory) { if (dumpDirectory) {
// delete contents // delete contents
const items = await readdir(dumpDirectory, { withFileTypes: true }); const items = await readdir(dumpDirectory, { withFileTypes: true });

View File

@ -1,4 +1,5 @@
import { import {
FeatureFlag,
IAlbumRepository, IAlbumRepository,
IAssetRepository, IAssetRepository,
IBaseJob, IBaseJob,
@ -7,17 +8,18 @@ import {
IGeocodingRepository, IGeocodingRepository,
IJobRepository, IJobRepository,
IStorageRepository, IStorageRepository,
ISystemConfigRepository,
JobName, JobName,
JOBS_ASSET_PAGINATION_SIZE, JOBS_ASSET_PAGINATION_SIZE,
QueueName, QueueName,
StorageCore, StorageCore,
StorageFolder, StorageFolder,
SystemConfigCore,
usePagination, usePagination,
WithoutProperty, WithoutProperty,
} from '@app/domain'; } from '@app/domain';
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { Inject, Logger } from '@nestjs/common'; import { Inject, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DefaultReadTaskOptions, ExifDateTime, exiftool, ReadTaskOptions, Tags } from 'exiftool-vendored'; import { DefaultReadTaskOptions, ExifDateTime, exiftool, ReadTaskOptions, Tags } from 'exiftool-vendored';
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
import * as geotz from 'geo-tz'; import * as geotz from 'geo-tz';
@ -51,8 +53,9 @@ const validate = <T>(value: T): T | null => (typeof value === 'string' ? null :
export class MetadataExtractionProcessor { export class MetadataExtractionProcessor {
private logger = new Logger(MetadataExtractionProcessor.name); private logger = new Logger(MetadataExtractionProcessor.name);
private reverseGeocodingEnabled: boolean;
private storageCore: StorageCore; private storageCore: StorageCore;
private configCore: SystemConfigCore;
private oldCities?: string;
constructor( constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@ -61,31 +64,35 @@ export class MetadataExtractionProcessor {
@Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository, @Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
configService: ConfigService,
) { ) {
this.reverseGeocodingEnabled = !configService.get('DISABLE_REVERSE_GEOCODING');
this.storageCore = new StorageCore(storageRepository); this.storageCore = new StorageCore(storageRepository);
this.configCore = new SystemConfigCore(configRepository);
this.configCore.config$.subscribe(() => this.init());
} }
async init(deleteCache = false) { async init(deleteCache = false) {
this.logger.log(`Reverse geocoding is ${this.reverseGeocodingEnabled ? 'enabled' : 'disabled'}`); const { reverseGeocoding } = await this.configCore.getConfig();
if (!this.reverseGeocodingEnabled) { const { citiesFileOverride } = reverseGeocoding;
if (!reverseGeocoding.enabled) {
return; return;
} }
try { try {
if (deleteCache) { if (deleteCache) {
await this.geocodingRepository.deleteCache(); await this.geocodingRepository.deleteCache();
} else if (this.oldCities && this.oldCities === citiesFileOverride) {
return;
} }
this.logger.log('Initializing Reverse Geocoding');
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION); await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
await this.geocodingRepository.init(); await this.geocodingRepository.init({ citiesFileOverride });
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
this.logger.log('Reverse Geocoding Initialized'); this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`);
} catch (error: any) { this.oldCities = citiesFileOverride;
} catch (error: Error | any) {
this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack); this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack);
} }
} }
@ -161,7 +168,7 @@ export class MetadataExtractionProcessor {
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntity) { private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntity) {
const { latitude, longitude } = exifData; const { latitude, longitude } = exifData;
if (!this.reverseGeocodingEnabled || !longitude || !latitude) { if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) {
return; return;
} }

View File

@ -85,6 +85,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
configFile: false, configFile: false,
facialRecognition: true, facialRecognition: true,
map: true, map: true,
reverseGeocoding: true,
oauth: false, oauth: false,
oauthAutoLaunch: false, oauthAutoLaunch: false,
passwordLogin: true, passwordLogin: true,

View File

@ -1055,6 +1055,22 @@ export interface CheckExistingAssetsResponseDto {
*/ */
'existingIds': Array<string>; 'existingIds': Array<string>;
} }
/**
*
* @export
* @enum {string}
*/
export const CitiesFile = {
Cities15000: 'cities15000',
Cities5000: 'cities5000',
Cities1000: 'cities1000',
Cities500: 'cities500'
} as const;
export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile];
/** /**
* *
* @export * @export
@ -2650,6 +2666,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'passwordLogin': boolean; 'passwordLogin': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'reverseGeocoding': boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -3093,6 +3115,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto * @memberof SystemConfigDto
*/ */
'passwordLogin': SystemConfigPasswordLoginDto; 'passwordLogin': SystemConfigPasswordLoginDto;
/**
*
* @type {SystemConfigReverseGeocodingDto}
* @memberof SystemConfigDto
*/
'reverseGeocoding': SystemConfigReverseGeocodingDto;
/** /**
* *
* @type {SystemConfigStorageTemplateDto} * @type {SystemConfigStorageTemplateDto}
@ -3438,6 +3466,27 @@ export interface SystemConfigPasswordLoginDto {
*/ */
'enabled': boolean; 'enabled': boolean;
} }
/**
*
* @export
* @interface SystemConfigReverseGeocodingDto
*/
export interface SystemConfigReverseGeocodingDto {
/**
*
* @type {CitiesFile}
* @memberof SystemConfigReverseGeocodingDto
*/
'citiesFileOverride': CitiesFile;
/**
*
* @type {boolean}
* @memberof SystemConfigReverseGeocodingDto
*/
'enabled': boolean;
}
/** /**
* *
* @export * @export

View File

@ -4,23 +4,25 @@
NotificationType, NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { api, SystemConfigMapDto } from '@api'; import { api, CitiesFile, SystemConfigDto } from '@api';
import { isEqual } from 'lodash-es'; import { cloneDeep, isEqual } from 'lodash-es';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import SettingAccordion from '../setting-accordion.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte'; import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingSelect from '../setting-select.svelte';
export let mapConfig: SystemConfigMapDto; // this is the config that is being edited export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false; export let disabled = false;
let savedConfig: SystemConfigMapDto; let savedConfig: SystemConfigDto;
let defaultConfig: SystemConfigMapDto; let defaultConfig: SystemConfigDto;
async function getConfigs() { async function refreshConfig() {
[savedConfig, defaultConfig] = await Promise.all([ [savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.map), api.systemConfigApi.getConfig().then((res) => res.data),
api.systemConfigApi.getDefaults().then((res) => res.data.map), api.systemConfigApi.getDefaults().then((res) => res.data),
]); ]);
} }
@ -28,11 +30,21 @@
try { try {
const { data: current } = await api.systemConfigApi.getConfig(); const { data: current } = await api.systemConfigApi.getConfig();
const { data: updated } = await api.systemConfigApi.updateConfig({ const { data: updated } = await api.systemConfigApi.updateConfig({
systemConfigDto: { ...current, map: mapConfig }, systemConfigDto: {
...current,
map: {
enabled: config.map.enabled,
tileUrl: config.map.tileUrl,
},
reverseGeocoding: {
enabled: config.reverseGeocoding.enabled,
citiesFileOverride: config.reverseGeocoding.citiesFileOverride,
},
},
}); });
mapConfig = { ...updated.map }; config = cloneDeep(updated);
savedConfig = { ...updated.map }; savedConfig = cloneDeep(updated);
notificationController.show({ message: 'Settings saved', type: NotificationType.Info }); notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
} catch (error) { } catch (error) {
@ -43,8 +55,8 @@
async function reset() { async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig(); const { data: resetConfig } = await api.systemConfigApi.getConfig();
mapConfig = { ...resetConfig.map }; config = cloneDeep(resetConfig);
savedConfig = { ...resetConfig.map }; savedConfig = cloneDeep(resetConfig);
notificationController.show({ notificationController.show({
message: 'Reset settings to the recent saved settings', message: 'Reset settings to the recent saved settings',
@ -55,8 +67,8 @@
async function resetToDefault() { async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults(); const { data: configs } = await api.systemConfigApi.getDefaults();
mapConfig = { ...configs.map }; config = cloneDeep(configs);
defaultConfig = { ...configs.map }; defaultConfig = cloneDeep(configs);
notificationController.show({ notificationController.show({
message: 'Reset map settings to default', message: 'Reset map settings to default',
@ -65,30 +77,81 @@
} }
</script> </script>
<div> <div class="mt-2">
{#await getConfigs() then} {#await refreshConfig() then}
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault> <form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4"> <div class="flex flex-col gap-4">
<SettingSwitch title="ENABLED" {disabled} subtitle="Enable map features" bind:checked={mapConfig.enabled} /> <SettingAccordion title="Map Settings" subtitle="Manage map settings">
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title="ENABLED"
{disabled}
subtitle="Enable map features"
bind:checked={config.map.enabled}
/>
<hr /> <hr />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="Tile URL" label="Tile URL"
desc="URL to a leaflet compatible tile server" desc="URL to a leaflet compatible tile server"
bind:value={mapConfig.tileUrl} bind:value={config.map.tileUrl}
required={true} required={true}
disabled={disabled || !mapConfig.enabled} disabled={disabled || !config.map.enabled}
isEdited={mapConfig.tileUrl !== savedConfig.tileUrl} isEdited={config.map.tileUrl !== savedConfig.map.tileUrl}
/> />
</div></SettingAccordion
>
<SettingAccordion title="Reverse Geocoding Settings">
<svelte:fragment slot="subtitle">
<p class="text-sm dark:text-immich-dark-fg">
Manage <a
href="https://immich.app/docs/features/reverse-geocoding"
class="underline"
target="_blank"
rel="noreferrer">Reverse Geocoding</a
> settings
</p>
</svelte:fragment>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title="ENABLED"
{disabled}
subtitle="Enable reverse geocoding"
bind:checked={config.reverseGeocoding.enabled}
/>
<hr />
<SettingSelect
label="Precision"
desc="Set reverse geocoding precision"
name="reverse-geocoding-precision"
bind:value={config.reverseGeocoding.citiesFileOverride}
options={[
{ value: CitiesFile.Cities500, text: 'Cities with more than 500 people' },
{ value: CitiesFile.Cities1000, text: 'Cities with more than 1000 people' },
{ value: CitiesFile.Cities5000, text: 'Cities with more than 5000 people' },
{ value: CitiesFile.Cities15000, text: 'Cities with more than 15000 people' },
]}
disabled={disabled || !config.reverseGeocoding.enabled}
isEdited={config.reverseGeocoding.citiesFileOverride !==
savedConfig.reverseGeocoding.citiesFileOverride}
/>
</div></SettingAccordion
>
<SettingButtonsRow <SettingButtonsRow
on:reset={reset} on:reset={reset}
on:save={saveSetting} on:save={saveSetting}
on:reset-to-default={resetToDefault} on:reset-to-default={resetToDefault}
showResetToDefault={!isEqual(savedConfig, defaultConfig)} showResetToDefault={!isEqual(
{ ...savedConfig.map, ...savedConfig.reverseGeocoding },
{ ...defaultConfig.map, ...defaultConfig.reverseGeocoding },
)}
{disabled} {disabled}
/> />
</div> </div>

View File

@ -14,7 +14,9 @@
{title} {title}
</h2> </h2>
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p> <slot name="subtitle">
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
</slot>
</div> </div>
<button <button

View File

@ -10,6 +10,7 @@ export const featureFlags = writable<FeatureFlags>({
sidecar: true, sidecar: true,
tagImage: true, tagImage: true,
map: true, map: true,
reverseGeocoding: true,
search: true, search: true,
oauth: false, oauth: false,
oauthAutoLaunch: false, oauthAutoLaunch: false,

View File

@ -67,12 +67,12 @@
<JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} /> <JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
</SettingAccordion> </SettingAccordion>
<SettingAccordion title="Machine Learning Settings" subtitle="Manage model settings"> <SettingAccordion title="Machine Learning Settings" subtitle="Manage machine learning features and settings">
<MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} /> <MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
</SettingAccordion> </SettingAccordion>
<SettingAccordion title="Map Settings" subtitle="Manage map settings"> <SettingAccordion title="Map & GPS Settings" subtitle="Manage map related features and setting">
<MapSettings disabled={$featureFlags.configFile} mapConfig={configs.map} /> <MapSettings disabled={$featureFlags.configFile} config={configs} />
</SettingAccordion> </SettingAccordion>
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings"> <SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">