repository, services and validation

This commit is contained in:
Alex Tran 2023-11-12 21:34:05 -06:00
parent 2d489b33e3
commit 998dfdaeda
7 changed files with 93 additions and 36 deletions

View file

@ -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);
});
});

View file

@ -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}`);

View file

@ -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>;

View file

@ -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;
}

View file

@ -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 };
}

View file

@ -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(),
};

View file

@ -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>