From 9bada51d56aeaf4929b683078600655dfbdc7216 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:03:57 +0200 Subject: [PATCH] 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 --- cli/src/api/open-api/api.ts | 49 +++++++ docs/docs/install/environment-variables.md | 8 +- mobile/openapi/.openapi-generator/FILES | 6 + mobile/openapi/README.md | Bin 20614 -> 20728 bytes mobile/openapi/doc/CitiesFile.md | Bin 0 -> 376 bytes mobile/openapi/doc/ServerFeaturesDto.md | Bin 697 -> 735 bytes mobile/openapi/doc/SystemConfigDto.md | Bin 1028 -> 1131 bytes .../doc/SystemConfigReverseGeocodingDto.md | Bin 0 -> 489 bytes mobile/openapi/lib/api.dart | Bin 6761 -> 6847 bytes mobile/openapi/lib/api_client.dart | Bin 20476 -> 20677 bytes mobile/openapi/lib/api_helper.dart | Bin 5142 -> 5240 bytes mobile/openapi/lib/model/cities_file.dart | Bin 0 -> 2856 bytes .../lib/model/server_features_dto.dart | Bin 5192 -> 5532 bytes .../openapi/lib/model/system_config_dto.dart | Bin 4818 -> 5205 bytes .../system_config_reverse_geocoding_dto.dart | Bin 0 -> 3437 bytes mobile/openapi/test/cities_file_test.dart | Bin 0 -> 417 bytes .../test/server_features_dto_test.dart | Bin 1492 -> 1607 bytes .../openapi/test/system_config_dto_test.dart | Bin 1427 -> 1569 bytes ...tem_config_reverse_geocoding_dto_test.dart | Bin 0 -> 738 bytes server/immich-openapi-specs.json | 32 +++++ .../domain/metadata/geocoding.repository.ts | 4 +- .../src/domain/server-info/server-info.dto.ts | 1 + .../server-info/server-info.service.spec.ts | 1 + .../system-config-reverse-geocoding.dto.ts | 12 ++ .../system-config/dto/system-config.dto.ts | 6 + .../system-config/system-config.core.ts | 7 + .../system-config.service.spec.ts | 5 + .../infra/entities/system-config.entity.ts | 14 ++ server/src/infra/infra.config.ts | 20 +-- .../repositories/geocoding.repository.ts | 26 +++- .../metadata-extraction.processor.ts | 31 +++-- server/test/e2e/server-info.e2e-spec.ts | 1 + web/src/api/open-api/api.ts | 49 +++++++ .../settings/map-settings/map-settings.svelte | 125 +++++++++++++----- .../settings/setting-accordion.svelte | 4 +- web/src/lib/stores/server-config.store.ts | 1 + .../routes/admin/system-settings/+page.svelte | 6 +- 37 files changed, 331 insertions(+), 77 deletions(-) create mode 100644 mobile/openapi/doc/CitiesFile.md create mode 100644 mobile/openapi/doc/SystemConfigReverseGeocodingDto.md create mode 100644 mobile/openapi/lib/model/cities_file.dart create mode 100644 mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart create mode 100644 mobile/openapi/test/cities_file_test.dart create mode 100644 mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart create mode 100644 server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index e39b6e4f12..f9c983833f 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -1055,6 +1055,22 @@ export interface CheckExistingAssetsResponseDto { */ 'existingIds': Array; } +/** + * + * @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 @@ -2650,6 +2666,12 @@ export interface ServerFeaturesDto { * @memberof ServerFeaturesDto */ 'passwordLogin': boolean; + /** + * + * @type {boolean} + * @memberof ServerFeaturesDto + */ + 'reverseGeocoding': boolean; /** * * @type {boolean} @@ -3093,6 +3115,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'passwordLogin': SystemConfigPasswordLoginDto; + /** + * + * @type {SystemConfigReverseGeocodingDto} + * @memberof SystemConfigDto + */ + 'reverseGeocoding': SystemConfigReverseGeocodingDto; /** * * @type {SystemConfigStorageTemplateDto} @@ -3438,6 +3466,27 @@ export interface SystemConfigPasswordLoginDto { */ 'enabled': boolean; } +/** + * + * @export + * @interface SystemConfigReverseGeocodingDto + */ +export interface SystemConfigReverseGeocodingDto { + /** + * + * @type {CitiesFile} + * @memberof SystemConfigReverseGeocodingDto + */ + 'citiesFileOverride': CitiesFile; + /** + * + * @type {boolean} + * @memberof SystemConfigReverseGeocodingDto + */ + 'enabled': boolean; +} + + /** * * @export diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index ed84fd439f..403ff2c363 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -49,11 +49,9 @@ These environment variables are used by the `docker-compose.yml` file and do **N ## Geocoding -| Variable | Description | Default | Services | -| :--------------------------------- | :---------------------------------- | :--------------------------: | :------------ | -| `DISABLE_REVERSE_GEOCODING` | Disable Reverse Geocoding Precision | `false` | microservices | -| `REVERSE_GEOCODING_PRECISION` | Reverse Geocoding Precision | `3` | microservices | -| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices | +| Variable | Description | Default | Services | +| :--------------------------------- | :------------------------------- | :--------------------------: | :------------ | +| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices | ## Ports diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index f10fa425c5..60dc2f1521 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -43,6 +43,7 @@ doc/CheckDuplicateAssetDto.md doc/CheckDuplicateAssetResponseDto.md doc/CheckExistingAssetsDto.md doc/CheckExistingAssetsResponseDto.md +doc/CitiesFile.md doc/ClassificationConfig.md doc/Colorspace.md doc/CreateAlbumDto.md @@ -126,6 +127,7 @@ doc/SystemConfigMachineLearningDto.md doc/SystemConfigMapDto.md doc/SystemConfigOAuthDto.md doc/SystemConfigPasswordLoginDto.md +doc/SystemConfigReverseGeocodingDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.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_existing_assets_dto.dart lib/model/check_existing_assets_response_dto.dart +lib/model/cities_file.dart lib/model/classification_config.dart lib/model/clip_config.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_o_auth_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_template_storage_option_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_existing_assets_dto_test.dart test/check_existing_assets_response_dto_test.dart +test/cities_file_test.dart test/classification_config_test.dart test/clip_config_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_o_auth_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_template_storage_option_dto_test.dart test/system_config_thumbnail_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 889f20658d889d9bbedf5ac5f1a78db9a49711fc..f1e4193fd2f66689997061659572c0a7b422ba46 100644 GIT binary patch delta 87 zcmZo$$oOL+B6UeP_79=0Ka*d;&qKWvNBQsqU%y$@wXn XdFhi6dWrF)2~K|RDY@Ck+lv(d(dQwg delta 19 bcmeydkg;tcrQcf$m`XExbdBa0l3-HmoFugIn}RD2D5CE36twg9Vcx9HA6BG*;-lPAN={~^HW3Yg7L|qBecdu40&;= zc!8w+3o{`)lYMT@0FSg*FdBp)StG>ayCXz9J)_F%k{EPJP9g0F(4$6(J>oEabCWgA z4Q0R+@>LFz4<@FzomSOsyPVgnB@HSxc5fD3Qh6tNz#zx;V#&|y`R4wgTFO#_(;kgW NKSq2R{xUBaW1rtLnD77q literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 1ff40f5ed6c27524f35228f5395ae42ba2c108c8..e332902a3e4d936ba7be99ffc26bbc0bb5adf63f 100644 GIT binary patch delta 50 zcmaE9vfp$=C>K{|NoHzsd|GBs>g0=DqMJjx7I6y}rIw`@6{p6hr{*W;r)1`(Z@wrx Gg9QNmD-+rP delta 17 YcmdmQ`qE@WDA(o^u0`CNeZ`7c07KXYivR!s diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index e20a8d6984fa6f33e2e8692b50e705c09589f5cf..c9678d94fe8492009b8c574e46071d2ef3a9de5c 100644 GIT binary patch delta 93 zcmew}pYiBI#tl|#9GNAVsl{%YIg=X=MY)_|ywu4C&ib1z)ecDt1f`ax78R$ur{*W; Zr)1`(Pj-|MpX_hXgGItpW%CE11OQ?bA{77t delta 23 fcmX@Qknzua#tl|#nCw7suut4jvu=APEED diff --git a/mobile/openapi/lib/model/cities_file.dart b/mobile/openapi/lib/model/cities_file.dart new file mode 100644 index 0000000000000000000000000000000000000000..96f5d8e573dacb2631a6484a90464aead7eed8a2 GIT binary patch literal 2856 zcmai0ZExa65dO}um`K%yNYo@JRjP93xTqm*PS>RBkq;F@*2Z3k?U`M=yADF9{P#Yy zYhyzKjTFGl`!h4oFdX)VL%MpH-v9X9JayZb5qaesY#^Y;nV zSo2dZtRKBkF5dU}Q9Ws6Wm*`U7OIq&RO!meJe8$fO6>>Dvs$i<^>12;axFG4T~$7{ z^505Xm}{}b>q=O>dubhi=m_lSn5{WbnY4Dol9G8lzyTh<Nx? zeomv^GVEzbc#2Yp-Y`98$+Cln7so;#GlbVesY1l@YRtbV7uqk%l2*n!^@v^v zJz29BjMsaRc{>@sM84RS}YYJKw~`1zzJ1kx#d)eH}*Kfu&~+7NO(w87t2j4$&!#Sru)G zuXGEf!3k!4huN6(ka}Bz@qsY2{M#PUH*GvUN{UU$$yHed#xkNyUi;;y|f$8{?j#SgaEcBX-%!B@R{IAj10i0b!wGB@T!J8CQ-k z8yuSw&Q`1T!txiq8k}W23ga5{e$#7ia>G>HPJ$b_k6WT*7Ggzu#R3i&JzGR+mm4zS zB<-HP%esr5Y?7>H{m6GrA3L^u08S5^viN`{pNb=vEIwcfr$-DSZS8Vd2(b4hKyLmR ztdlL2T~JuV^XSl(eX963U>K?X>4W~JF;MZ>OPm9&UEGdK`hlU3$G?ejgk71XIX}>6 jT%@m&6k|* literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 07a725232f7e949f9d7b4f2e1d9e88ad366ba4a6..5175b99d78d108bda649e69887b75a5898fee47e 100644 GIT binary patch delta 270 zcmX@1F-Lnt7L!0xYFTPgajJW2esX?FW?uT_0w&SP*-W=FBsSYHA7E5LSFT`dtAH-H z`7KK|Bc}c^HXBAIbhWk^CT^a diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..727e5534fde3cbb6832af75a271cab17dce7acba GIT binary patch literal 3437 zcmbtWVQW+83_wMmVqrqqdAAXz9-h7C4oIs~OzR=5RB+`gHxr z8Jdyh`-CYIz72kQJD^9gDx~63Dpiz+ z6Dj^zB^0_BY>l5KQ}}JT(zvwlBjIrX zFv5D=aZ{;61A~HZVA12Y1Ywhpd4T>s#va80Xleu|6FP{65nNwm>1UXf!W#G|tnfS! zIZx=-?i|kqaM}97@fOzNLCPFzN4xM|))CO%zQj5yW~72J^kxrVd5qOF8CT~JPQalN zv)rRR{pQX0@9r0f{+hmiZI9v{PMj3uo=6@J-RcvN?Nf&lg@F|}kTIQI%7kP3HgHRC zVs*h|t~FCzVkHtS<63BI<{cL>Xtp|sADm4cd;j97r1wT4m%vE(!?RT!_Hbfx96Nk< zyyiD&4`0=ZEQdCM8&th>u~-%HIi(SvudA{2fWo#G_4oP{W2Odrv$7x$o?!vNCKzYMD}( z`ir3BKQHGK$kFP$$-Jq#3;t-uZ>8jhL=~x>vRre2*A(5ATyb6H#&%3kti{r0P9RbQ z!jO_gA+uBuguaz#x-QMO34yqx?0R)1xxs71n(p5>$U9bqeFWR1gvY8YN!8pF>nya@ z>_(uwhz=RBBysfXcsd*!JPuH)y^N$&dQBn6(EPHy*ygSw>#9^Qa!8w#z%`k>qM=n+ z&H8q2RIZB=X7wyY7R%5<_*BoKeAOe=7}{wk!hr+lDw?0%4RZiDuBvSVctB4$05pdq z%-jM0HSl)dz`Au{>>&V_ap4^Q+kqXxje%HbZuJ`tALNBuC{2>4HFw-GF+lo zq_M0Aqc_eAH>q_T?=keIag{dTYwbGmI;Hk{^#mhy&B6-(H>u=Yq>otp#I;Aa& zR$I|hj~f`(=&%vkt`LZ|B#~}^;jI3W}hZ}8BNkcrmRopt+ z{zE)E&tsW@7#g*Ma1yvDPw0wp81}BIfkBd5Ea8hWfX?dCERqQS;gS?4;Jn;2c4ps0 C!HmZM literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/server_features_dto_test.dart b/mobile/openapi/test/server_features_dto_test.dart index 6838e12563a4efbe6663ee6e0bbf75bcc1bc387d..864a489c9063b0a2dcfa217d6acc8b7efe344626 100644 GIT binary patch delta 50 qcmcb@eVk{*W)^{>)Uwo~;#Bw4{N((U%)Io;zD$Z3f}0yzf*Aqv@Dkqu delta 12 TcmX@kbA@}uW|qxrtcr{PA$0^A diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 8951d16ca8b8a10bf560bc24db38d6f20455b2fb..1ada973063b984ec2bd764e76266d4c98f998cde 100644 GIT binary patch delta 70 ycmbQty^v?a3>KB3)Uwo~;#Bw4{N((U%)E4$l6-|CG_lEsEQ$i?f}7)6Oc(*Cml?sM6AcT5MBPO3;HlKz*=1t4UE8jP82`J|0$w1&Lm$(8 zkNKu)nxrY*y)5#x+2nEZbhnsfFrD8|29V|Om@nWV&*szD6NUxkZ7Hq4xg1_xCZVKS z8;hbcR#c?$8amxMECniTQ2W8IrlvL4A7n`NCD%^0ofj5AI4nkGOYH@fY)BQMjdXX=Op!(>l+iWA{7PzZbuNmPv@S_DJwmUQ#k;KW+O();ODV5m zNZlLlK|DI2hHVE3rlEGgBZ23&^Vp1yuBd*(PY!}JG=(Wi*O@oTr=b!WNxFXu-w6Pd zE7DQ{#Y_dl%`uJs=_p}lZB5tqd%^k`qc?!9T{Ik%Cv;Q(JShIIIrnOLpsWlr4@;YP z0lz|MEzidKH7xfj%Z; + init(options: Partial): Promise; reverseGeocode(point: GeoPoint): Promise; deleteCache(): Promise; } diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts index 105fc1bd2d..119aaf86ed 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/domain/server-info/server-info.dto.ts @@ -90,6 +90,7 @@ export class ServerFeaturesDto implements FeatureFlags { configFile!: boolean; facialRecognition!: boolean; map!: boolean; + reverseGeocoding!: boolean; oauth!: boolean; oauthAutoLaunch!: boolean; passwordLogin!: boolean; diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index 4363e379b6..7a1e8ebcee 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -151,6 +151,7 @@ describe(ServerInfoService.name, () => { clipEncode: true, facialRecognition: true, map: true, + reverseGeocoding: true, oauth: false, oauthAutoLaunch: false, passwordLogin: true, diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts new file mode 100644 index 0000000000..be20a02c79 --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts @@ -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; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index 9ec7de6225..b4099c2b22 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -8,6 +8,7 @@ import { SystemConfigMachineLearningDto } from './system-config-machine-learning import { SystemConfigMapDto } from './system-config-map.dto'; import { SystemConfigOAuthDto } from './system-config-oauth.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'; export class SystemConfigDto implements SystemConfig { @@ -36,6 +37,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() passwordLogin!: SystemConfigPasswordLoginDto; + @Type(() => SystemConfigReverseGeocodingDto) + @ValidateNested() + @IsObject() + reverseGeocoding!: SystemConfigReverseGeocodingDto; + @Type(() => SystemConfigStorageTemplateDto) @ValidateNested() @IsObject() diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 57a2f71f96..0dc35cc103 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -1,6 +1,7 @@ import { AudioCodec, CQMode, + CitiesFile, Colorspace, SystemConfig, SystemConfigEntity, @@ -81,6 +82,10 @@ export const defaults = Object.freeze({ enabled: true, tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', }, + reverseGeocoding: { + enabled: true, + citiesFileOverride: CitiesFile.CITIES_500, + }, oauth: { enabled: false, issuerUrl: '', @@ -115,6 +120,7 @@ export enum FeatureFlag { FACIAL_RECOGNITION = 'facialRecognition', TAG_IMAGE = 'tagImage', MAP = 'map', + REVERSE_GEOCODING = 'reverseGeocoding', SIDECAR = 'sidecar', SEARCH = 'search', OAUTH = 'oauth', @@ -177,6 +183,7 @@ export class SystemConfigCore { [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled, [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled, [FeatureFlag.MAP]: config.map.enabled, + [FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled, [FeatureFlag.SIDECAR]: true, [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false', diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 67484e06d4..fc50839ba7 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -1,6 +1,7 @@ import { AudioCodec, CQMode, + CitiesFile, Colorspace, SystemConfig, SystemConfigEntity, @@ -80,6 +81,10 @@ const updatedConfig = Object.freeze({ enabled: true, tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', }, + reverseGeocoding: { + enabled: true, + citiesFileOverride: CitiesFile.CITIES_500, + }, oauth: { autoLaunch: true, autoRegister: true, diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 825e22dbee..e8d9f5f1a3 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -64,6 +64,9 @@ export enum SystemConfigKey { MAP_ENABLED = 'map.enabled', MAP_TILE_URL = 'map.tileUrl', + REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled', + REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride', + OAUTH_ENABLED = 'oauth.enabled', OAUTH_ISSUER_URL = 'oauth.issuerUrl', OAUTH_CLIENT_ID = 'oauth.clientId', @@ -130,6 +133,13 @@ export enum Colorspace { P3 = 'p3', } +export enum CitiesFile { + CITIES_15000 = 'cities15000', + CITIES_5000 = 'cities5000', + CITIES_1000 = 'cities1000', + CITIES_500 = 'cities500', +} + export interface SystemConfig { ffmpeg: { crf: number; @@ -175,6 +185,10 @@ export interface SystemConfig { enabled: boolean; tileUrl: string; }; + reverseGeocoding: { + enabled: boolean; + citiesFileOverride: CitiesFile; + }; oauth: { enabled: boolean; issuerUrl: string; diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts index 81bb61bf1e..a3bcd10072 100644 --- a/server/src/infra/infra.config.ts +++ b/server/src/infra/infra.config.ts @@ -2,7 +2,6 @@ import { QueueName } from '@app/domain'; import { RegisterQueueOptions } from '@nestjs/bullmq'; import { QueueOptions } from 'bullmq'; import { RedisOptions } from 'ioredis'; -import { InitOptions } from 'local-reverse-geocoder'; import { ConfigurationOptions } from 'typesense/lib/Typesense/Configuration'; function parseRedisConfig(): RedisOptions { @@ -72,20 +71,5 @@ function parseTypeSenseConfig(): ConfigurationOptions { export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig(); -function parseLocalGeocodingConfig(): InitOptions { - const precision = Number(process.env.REVERSE_GEOCODING_PRECISION); - - 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(); +export const REVERSE_GEOCODING_DUMP_DIRECTORY = + process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/'; diff --git a/server/src/infra/repositories/geocoding.repository.ts b/server/src/infra/repositories/geocoding.repository.ts index 7b7b763756..eae72a904f 100644 --- a/server/src/infra/repositories/geocoding.repository.ts +++ b/server/src/infra/repositories/geocoding.repository.ts @@ -1,9 +1,9 @@ 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 { readdir, rm } from 'fs/promises'; 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 { promisify } from 'util'; @@ -18,19 +18,33 @@ export type GeoData = AddressObject & { admin2Code?: AdminCode | string; }; -const init = (): Promise => new Promise((resolve) => geocoder.init(localGeocodingConfig, resolve)); const lookup = promisify(geocoder.lookUp).bind(geocoder); @Injectable() export class GeocodingRepository implements IGeocodingRepository { private logger = new Logger(GeocodingRepository.name); - async init(): Promise { - await init(); + async init(options: Partial): Promise { + return new Promise((resolve) => { + geocoder.init( + { + load: { + admin1: true, + admin2: true, + admin3And4: false, + alternateNames: false, + }, + countries: [], + dumpDirectory: REVERSE_GEOCODING_DUMP_DIRECTORY, + ...options, + }, + resolve, + ); + }); } async deleteCache() { - const dumpDirectory = localGeocodingConfig.dumpDirectory; + const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY; if (dumpDirectory) { // delete contents const items = await readdir(dumpDirectory, { withFileTypes: true }); diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts index 9da6903f6b..10d1d27f27 100644 --- a/server/src/microservices/processors/metadata-extraction.processor.ts +++ b/server/src/microservices/processors/metadata-extraction.processor.ts @@ -1,4 +1,5 @@ import { + FeatureFlag, IAlbumRepository, IAssetRepository, IBaseJob, @@ -7,17 +8,18 @@ import { IGeocodingRepository, IJobRepository, IStorageRepository, + ISystemConfigRepository, JobName, JOBS_ASSET_PAGINATION_SIZE, QueueName, StorageCore, StorageFolder, + SystemConfigCore, usePagination, WithoutProperty, } from '@app/domain'; import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { Inject, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { DefaultReadTaskOptions, ExifDateTime, exiftool, ReadTaskOptions, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import * as geotz from 'geo-tz'; @@ -51,8 +53,9 @@ const validate = (value: T): T | null => (typeof value === 'string' ? null : export class MetadataExtractionProcessor { private logger = new Logger(MetadataExtractionProcessor.name); - private reverseGeocodingEnabled: boolean; private storageCore: StorageCore; + private configCore: SystemConfigCore; + private oldCities?: string; constructor( @Inject(IAssetRepository) private assetRepository: IAssetRepository, @@ -61,31 +64,35 @@ export class MetadataExtractionProcessor { @Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, - - configService: ConfigService, + @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, ) { - this.reverseGeocodingEnabled = !configService.get('DISABLE_REVERSE_GEOCODING'); this.storageCore = new StorageCore(storageRepository); + this.configCore = new SystemConfigCore(configRepository); + this.configCore.config$.subscribe(() => this.init()); } async init(deleteCache = false) { - this.logger.log(`Reverse geocoding is ${this.reverseGeocodingEnabled ? 'enabled' : 'disabled'}`); - if (!this.reverseGeocodingEnabled) { + const { reverseGeocoding } = await this.configCore.getConfig(); + const { citiesFileOverride } = reverseGeocoding; + + if (!reverseGeocoding.enabled) { return; } try { if (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.geocodingRepository.init(); + await this.geocodingRepository.init({ citiesFileOverride }); await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); - this.logger.log('Reverse Geocoding Initialized'); - } catch (error: any) { + this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`); + this.oldCities = citiesFileOverride; + } catch (error: Error | any) { 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) { const { latitude, longitude } = exifData; - if (!this.reverseGeocodingEnabled || !longitude || !latitude) { + if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) { return; } diff --git a/server/test/e2e/server-info.e2e-spec.ts b/server/test/e2e/server-info.e2e-spec.ts index e1f6b7ee50..c686b78be2 100644 --- a/server/test/e2e/server-info.e2e-spec.ts +++ b/server/test/e2e/server-info.e2e-spec.ts @@ -85,6 +85,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => { configFile: false, facialRecognition: true, map: true, + reverseGeocoding: true, oauth: false, oauthAutoLaunch: false, passwordLogin: true, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index e39b6e4f12..f9c983833f 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1055,6 +1055,22 @@ export interface CheckExistingAssetsResponseDto { */ 'existingIds': Array; } +/** + * + * @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 @@ -2650,6 +2666,12 @@ export interface ServerFeaturesDto { * @memberof ServerFeaturesDto */ 'passwordLogin': boolean; + /** + * + * @type {boolean} + * @memberof ServerFeaturesDto + */ + 'reverseGeocoding': boolean; /** * * @type {boolean} @@ -3093,6 +3115,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'passwordLogin': SystemConfigPasswordLoginDto; + /** + * + * @type {SystemConfigReverseGeocodingDto} + * @memberof SystemConfigDto + */ + 'reverseGeocoding': SystemConfigReverseGeocodingDto; /** * * @type {SystemConfigStorageTemplateDto} @@ -3438,6 +3466,27 @@ export interface SystemConfigPasswordLoginDto { */ 'enabled': boolean; } +/** + * + * @export + * @interface SystemConfigReverseGeocodingDto + */ +export interface SystemConfigReverseGeocodingDto { + /** + * + * @type {CitiesFile} + * @memberof SystemConfigReverseGeocodingDto + */ + 'citiesFileOverride': CitiesFile; + /** + * + * @type {boolean} + * @memberof SystemConfigReverseGeocodingDto + */ + 'enabled': boolean; +} + + /** * * @export diff --git a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte index dff3c65a36..df9059fced 100644 --- a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte +++ b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte @@ -4,23 +4,25 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import { handleError } from '$lib/utils/handle-error'; - import { api, SystemConfigMapDto } from '@api'; - import { isEqual } from 'lodash-es'; + import { api, CitiesFile, SystemConfigDto } from '@api'; + import { cloneDeep, isEqual } from 'lodash-es'; import { fade } from 'svelte/transition'; + import SettingAccordion from '../setting-accordion.svelte'; import SettingButtonsRow from '../setting-buttons-row.svelte'; - import SettingSwitch from '../setting-switch.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; - let savedConfig: SystemConfigMapDto; - let defaultConfig: SystemConfigMapDto; + let savedConfig: SystemConfigDto; + let defaultConfig: SystemConfigDto; - async function getConfigs() { + async function refreshConfig() { [savedConfig, defaultConfig] = await Promise.all([ - api.systemConfigApi.getConfig().then((res) => res.data.map), - api.systemConfigApi.getDefaults().then((res) => res.data.map), + api.systemConfigApi.getConfig().then((res) => res.data), + api.systemConfigApi.getDefaults().then((res) => res.data), ]); } @@ -28,11 +30,21 @@ try { const { data: current } = await api.systemConfigApi.getConfig(); 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 }; - savedConfig = { ...updated.map }; + config = cloneDeep(updated); + savedConfig = cloneDeep(updated); notificationController.show({ message: 'Settings saved', type: NotificationType.Info }); } catch (error) { @@ -43,8 +55,8 @@ async function reset() { const { data: resetConfig } = await api.systemConfigApi.getConfig(); - mapConfig = { ...resetConfig.map }; - savedConfig = { ...resetConfig.map }; + config = cloneDeep(resetConfig); + savedConfig = cloneDeep(resetConfig); notificationController.show({ message: 'Reset settings to the recent saved settings', @@ -55,8 +67,8 @@ async function resetToDefault() { const { data: configs } = await api.systemConfigApi.getDefaults(); - mapConfig = { ...configs.map }; - defaultConfig = { ...configs.map }; + config = cloneDeep(configs); + defaultConfig = cloneDeep(configs); notificationController.show({ message: 'Reset map settings to default', @@ -65,30 +77,81 @@ } -
- {#await getConfigs() then} +
+ {#await refreshConfig() then}
-
- + diff --git a/web/src/lib/components/admin-page/settings/setting-accordion.svelte b/web/src/lib/components/admin-page/settings/setting-accordion.svelte index 9acdfb392e..1d9290e617 100755 --- a/web/src/lib/components/admin-page/settings/setting-accordion.svelte +++ b/web/src/lib/components/admin-page/settings/setting-accordion.svelte @@ -14,7 +14,9 @@ {title} -

{subtitle}

+ +

{subtitle}

+