Browse Source

repository, services and validation

Alex Tran 1 year ago
parent
commit
998dfdaeda

+ 4 - 4
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);
     });
   });

+ 11 - 1
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}`);

+ 3 - 1
server/src/domain/repositories/metadata.repository.ts

@@ -31,7 +31,9 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration'> {
 }
 
 export interface IMetadataRepository {
-  init(options: Partial<InitOptions>): Promise<void>;
+  initLocalGeocoding(options: Partial<InitOptions>): Promise<void>;
+  initMapboxGeocoding(accessToken: string): Promise<void>;
+  deinitMapboxGeocoding(): void;
   teardown(): Promise<void>;
   reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
   deleteCache(): Promise<void>;

+ 32 - 2
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;
 }

+ 34 - 18
server/src/infra/repositories/metadata.repository.ts

@@ -27,10 +27,23 @@ const lookup = promisify<GeoPoint[], number, AddressObject[][]>(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<InitOptions>): Promise<void> {
+  async initMapboxGeocoding(accessToken: string): Promise<void> {
     return new Promise<void>((resolve) => {
+      this.mapboxClient = mapboxGeocoding({ accessToken });
+      resolve();
+    });
+  }
+
+  deinitMapboxGeocoding(): void {
+    this.mapboxClient = undefined;
+  }
+
+  async initLocalGeocoding(options: Partial<InitOptions>): Promise<void> {
+    return new Promise<void>((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<ReverseGeocodeResult> {
-    console.log(point);
-    this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
+  async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
+    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 };
   }

+ 1 - 1
server/test/repositories/metadata.repository.mock.ts

@@ -4,7 +4,7 @@ export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> =>
   return {
     deleteCache: jest.fn(),
     getExifTags: jest.fn(),
-    init: jest.fn(),
+    initLocalGeocoding: jest.fn(),
     teardown: jest.fn(),
     reverseGeocode: jest.fn(),
   };

+ 8 - 9
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}
               />
+
+              <hr />
+
               <SettingSwitch
                 title="Use Mapbox"
-                {disabled}
+                disabled={!config.reverseGeocoding.enabled}
                 subtitle="Use Mapbox for reverse geocoding"
                 bind:checked={config.reverseGeocoding.useMapbox}
               />
 
               <SettingInputField
                 inputType={SettingInputFieldType.TEXT}
-                label="URL"
-                desc="URL of the machine learning server"
+                label="Mapbox Access Token"
                 bind:value={config.reverseGeocoding.mapboxAccessToken}
                 required={true}
-                disabled={disabled || !config.reverseGeocoding.useMapbox}
+                disabled={!config.reverseGeocoding.enabled || !config.reverseGeocoding.useMapbox}
                 isEdited={config.reverseGeocoding.mapboxAccessToken !== savedConfig.reverseGeocoding.mapboxAccessToken}
               />
             </div>