diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index f31605b12..bb733e989 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -91,7 +91,7 @@ describe(MetadataService.name, () => { await sut.init(); expect(metadataMock.deleteCache).not.toHaveBeenCalled(); expect(jobMock.pause).toHaveBeenCalledTimes(1); - expect(metadataMock.init).toHaveBeenCalledTimes(1); + expect(metadataMock.initLocalGeocoding).toHaveBeenCalledTimes(1); expect(jobMock.resume).toHaveBeenCalledTimes(1); }); @@ -100,7 +100,7 @@ describe(MetadataService.name, () => { expect(metadataMock.deleteCache).not.toHaveBeenCalled(); expect(jobMock.pause).toHaveBeenCalledTimes(1); - expect(metadataMock.init).toHaveBeenCalledTimes(1); + expect(metadataMock.initLocalGeocoding).toHaveBeenCalledTimes(1); expect(jobMock.resume).toHaveBeenCalledTimes(1); }); @@ -113,7 +113,7 @@ describe(MetadataService.name, () => { expect(metadataMock.deleteCache).not.toHaveBeenCalled(); expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 }); + expect(metadataMock.initLocalGeocoding).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 }); expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); }); @@ -122,7 +122,7 @@ describe(MetadataService.name, () => { expect(metadataMock.deleteCache).toHaveBeenCalled(); expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 }); + expect(metadataMock.initLocalGeocoding).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 }); expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); }); }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 45193c2e1..f4e7314cf 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -97,7 +97,17 @@ export class MetadataService { return; } + if (reverseGeocoding.useMapbox) { + await this.jobRepository.pause(QueueName.METADATA_EXTRACTION); + await this.repository.initMapboxGeocoding(reverseGeocoding.mapboxAccessToken); + await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); + this.logger.log(`Initialized Mapbox reverse geocoder`); + return; + } + try { + this.repository.deinitMapboxGeocoding(); + if (deleteCache) { await this.repository.deleteCache(); } else if (this.oldCities && this.oldCities === citiesFileOverride) { @@ -105,7 +115,7 @@ export class MetadataService { } await this.jobRepository.pause(QueueName.METADATA_EXTRACTION); - await this.repository.init({ citiesFileOverride }); + await this.repository.initLocalGeocoding({ citiesFileOverride }); await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`); diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index 0c3b78462..da95c218e 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -31,7 +31,9 @@ export interface ImmichTags extends Omit { } export interface IMetadataRepository { - init(options: Partial): Promise; + initLocalGeocoding(options: Partial): Promise; + initMapboxGeocoding(accessToken: string): Promise; + deinitMapboxGeocoding(): void; teardown(): Promise; reverseGeocode(point: GeoPoint): Promise; deleteCache(): Promise; 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 index 28260f266..683212fd4 100644 --- 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 @@ -1,6 +1,34 @@ import { CitiesFile } from '@app/infra/entities'; +import mapbox from '@mapbox/mapbox-sdk/services/tokens'; import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsString, + Validate, + ValidateIf, + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +const isEnabled = (config: SystemConfigReverseGeocodingDto) => config.enabled; +const useMapbox = (config: SystemConfigReverseGeocodingDto) => config.useMapbox; + +@ValidatorConstraint({ name: 'mapboxAccessToken', async: false }) +class MapboxAccessToken implements ValidatorConstraintInterface { + async validate(text: string, _: ValidationArguments) { + const mb = mapbox({ accessToken: text }); + const response = await mb.getToken().send(); + return response.body.code === 'TokenValid'; + } + + defaultMessage(args: ValidationArguments) { + // here you can provide default error message if validation failed + return 'Token invalid'; + } +} export class SystemConfigReverseGeocodingDto { @IsBoolean() @@ -11,10 +39,12 @@ export class SystemConfigReverseGeocodingDto { citiesFileOverride!: CitiesFile; @IsBoolean() + @ValidateIf(isEnabled) useMapbox!: boolean; - @ValidateIf((o) => o.useMapbox === true) + @ValidateIf(useMapbox) @IsString() @IsNotEmpty() + @Validate(MapboxAccessToken) mapboxAccessToken!: string; } diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 269063216..f48574c90 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -27,10 +27,23 @@ const lookup = promisify(geocoder.lookUp) @Injectable() export class MetadataRepository implements IMetadataRepository { private logger = new Logger(MetadataRepository.name); - private mapboxClient?: GeocodeService; + private mapboxClient?: GeocodeService = undefined; - async init(options: Partial): Promise { + async initMapboxGeocoding(accessToken: string): Promise { return new Promise((resolve) => { + this.mapboxClient = mapboxGeocoding({ accessToken }); + resolve(); + }); + } + + deinitMapboxGeocoding(): void { + this.mapboxClient = undefined; + } + + async initLocalGeocoding(options: Partial): Promise { + return new Promise((resolve) => { + this.mapboxClient = undefined; + console.log('init local geocoding', this.mapboxClient); geocoder.init( { load: { @@ -64,10 +77,15 @@ export class MetadataRepository implements IMetadataRepository { } } - async reverseGeocode(point: GeoPoint, useMapbox: boolean = false): Promise { - console.log(point); - this.logger.debug(`Request: ${point.latitude},${point.longitude}`); + async reverseGeocode(point: GeoPoint): Promise { + if (this.mapboxClient) { + return this.useMapboxGeocoding(point, this.mapboxClient); + } + return this.useLocalGeocoding(point); + } + + private async useLocalGeocoding(point: GeoPoint) { const [address] = await lookup([point], 1); this.logger.verbose(`Raw: ${JSON.stringify(address, null, 2)}`); @@ -77,37 +95,35 @@ export class MetadataRepository implements IMetadataRepository { const state = stateParts.length > 0 ? stateParts.join(', ') : null; this.logger.debug(`Normalized: ${JSON.stringify({ country, state, city })}`); - // Mapbox - this.mapboxClient = mapboxGeocoding({ - accessToken: 'pk.eyJ1IjoiYWx0cmFuMTUwMiIsImEiOiJjbDBoaXQyZGkwOTEyM2tvMzd2dzJqcXZwIn0.-Lrg7SfQVnhAwWSNV5HoSQ', - }); + return { country, state, city }; + } - const geoCodeInfo: MapiResponse = await this.mapboxClient + private async useMapboxGeocoding(point: GeoPoint, mbClient: GeocodeService) { + const geoCodeInfo: MapiResponse = await mbClient .reverseGeocode({ query: [point.longitude, point.latitude], types: ['country', 'region', 'place'], }) .send(); - console.log(geoCodeInfo.body); const res: [] = geoCodeInfo.body['features']; - let mbCity = ''; - let mbState = ''; - let mbCountry = ''; + let city = ''; + let state = ''; + let country = ''; if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]) { - mbCity = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text']; + city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text']; } if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]) { - mbState = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text']; + state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text']; } if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]) { - mbCountry = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text']; + country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text']; } - console.log('Mapbox: ', mbCity, mbState, mbCountry); + this.logger.debug(`Mapbox Normalized: ${JSON.stringify({ country, state, city })}`); return { country, state, city }; } diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 76c6f777a..f9a4ac738 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -4,7 +4,7 @@ export const newMetadataRepositoryMock = (): jest.Mocked => return { deleteCache: jest.fn(), getExifTags: jest.fn(), - init: jest.fn(), + initLocalGeocoding: jest.fn(), teardown: jest.fn(), reverseGeocode: jest.fn(), }; 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 b9f0bfd48..7f8db2a6e 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 @@ -29,9 +29,6 @@ async function saveSetting() { try { const { data: current } = await api.systemConfigApi.getConfig(); - console.log('current', current.reverseGeocoding); - - console.log('update', config.reverseGeocoding.useMapbox); const { data: updated } = await api.systemConfigApi.updateConfig({ systemConfigDto: { ...current, @@ -43,8 +40,8 @@ reverseGeocoding: { enabled: config.reverseGeocoding.enabled, citiesFileOverride: config.reverseGeocoding.citiesFileOverride, - useMapbox: config.reverseGeocoding.useMapbox, - mapboxAccessToken: config.reverseGeocoding.mapboxAccessToken, + useMapbox: config.reverseGeocoding.enabled ? config.reverseGeocoding.useMapbox : false, + mapboxAccessToken: config.reverseGeocoding.enabled ? config.reverseGeocoding.mapboxAccessToken : '', }, }, }); @@ -154,20 +151,22 @@ isEdited={config.reverseGeocoding.citiesFileOverride !== savedConfig.reverseGeocoding.citiesFileOverride} /> + +
+