mirror of
https://github.com/immich-app/immich.git
synced 2024-12-22 01:47:08 +02:00
feat(server) Remove mapbox and use local reverse geocoding (#738)
* feat: local reverse geocoding implementation, removes mapbox * Disable non-null tslintrule * Disable non-null tslintrule * Remove tsignore Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
e5459b68ff
commit
f377b64065
@ -41,12 +41,20 @@ LOG_LEVEL=simple
|
|||||||
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# MAPBOX
|
# Reverse Geocoding
|
||||||
####################################################################################
|
####################################################################################
|
||||||
|
|
||||||
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
# DISABLE_REVERSE_GEOCODING=false
|
||||||
ENABLE_MAPBOX=false
|
|
||||||
MAPBOX_KEY=
|
# Reverse geocoding is done locally which has a small impact on memory usage
|
||||||
|
# This memory usage can be altered by changing the REVERSE_GEOCODING_PRECISION variable
|
||||||
|
# This ranges from 0-3 with 3 being the most precise
|
||||||
|
# 3 - Cities > 500 population: ~200MB RAM
|
||||||
|
# 2 - Cities > 1000 population: ~150MB RAM
|
||||||
|
# 1 - Cities > 5000 population: ~80MB RAM
|
||||||
|
# 0 - Cities > 15000 population: ~40MB RAM
|
||||||
|
|
||||||
|
# REVERSE_GEOCODING_PRECISION=3
|
||||||
|
|
||||||
####################################################################################
|
####################################################################################
|
||||||
# WEB - Optional
|
# WEB - Optional
|
||||||
|
@ -94,7 +94,12 @@ export class ScheduleTasksService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const exif of exifInfo) {
|
for (const exif of exifInfo) {
|
||||||
await this.metadataExtractionQueue.add(reverseGeocodingProcessorName, { exif }, { jobId: randomUUID() });
|
await this.metadataExtractionQueue.add(
|
||||||
|
reverseGeocodingProcessorName,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
{ exifId: exif.id, latitude: exif.latitude!, longitude: exif.longitude! },
|
||||||
|
{ jobId: randomUUID() },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ import {
|
|||||||
reverseGeocodingProcessorName,
|
reverseGeocodingProcessorName,
|
||||||
IReverseGeocodingProcessor,
|
IReverseGeocodingProcessor,
|
||||||
} from '@app/job';
|
} from '@app/job';
|
||||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
|
||||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
|
||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
@ -26,12 +24,63 @@ import ffmpeg from 'fluent-ffmpeg';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Repository } from 'typeorm/repository/Repository';
|
import { Repository } from 'typeorm/repository/Repository';
|
||||||
|
import geocoder, { InitOptions } from 'local-reverse-geocoder';
|
||||||
|
import { getName } from 'i18n-iso-countries';
|
||||||
import { find } from 'geo-tz';
|
import { find } from 'geo-tz';
|
||||||
import * as luxon from 'luxon';
|
import * as luxon from 'luxon';
|
||||||
|
|
||||||
|
function geocoderInit(init: InitOptions) {
|
||||||
|
return new Promise<void>(function (resolve) {
|
||||||
|
geocoder.init(init, () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function geocoderLookup(points: { latitude: number; longitude: number }[]) {
|
||||||
|
return new Promise<GeoData>(function (resolve) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
geocoder.lookUp(points, 1, (err, addresses) => {
|
||||||
|
resolve(addresses[0][0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const geocodingPrecisionLevels = [ "cities15000", "cities5000", "cities1000", "cities500" ]
|
||||||
|
|
||||||
|
export interface AdminCode {
|
||||||
|
name: string;
|
||||||
|
asciiName: string;
|
||||||
|
geoNameId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoData {
|
||||||
|
geoNameId: string;
|
||||||
|
name: string;
|
||||||
|
asciiName: string;
|
||||||
|
alternateNames: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
featureClass: string;
|
||||||
|
featureCode: string;
|
||||||
|
countryCode: string;
|
||||||
|
cc2?: any;
|
||||||
|
admin1Code: AdminCode;
|
||||||
|
admin2Code: AdminCode;
|
||||||
|
admin3Code: string;
|
||||||
|
admin4Code?: any;
|
||||||
|
population: string;
|
||||||
|
elevation: string;
|
||||||
|
dem: string;
|
||||||
|
timezone: string;
|
||||||
|
modificationDate: string;
|
||||||
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Processor(metadataExtractionQueueName)
|
@Processor(metadataExtractionQueueName)
|
||||||
export class MetadataExtractionProcessor {
|
export class MetadataExtractionProcessor {
|
||||||
private geocodingClient?: GeocodeService;
|
private isGeocodeInitialized = false;
|
||||||
private logLevel: ImmichLogLevel;
|
private logLevel: ImmichLogLevel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -46,15 +95,44 @@ export class MetadataExtractionProcessor {
|
|||||||
|
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) {
|
if (configService.get('DISABLE_REVERSE_GEOCODING') !== 'true') {
|
||||||
this.geocodingClient = mapboxGeocoding({
|
Logger.log('Initialising Reverse Geocoding');
|
||||||
accessToken: process.env.MAPBOX_KEY,
|
geocoderInit({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
citiesFileOverride: geocodingPrecisionLevels[configService.get('REVERSE_GEOCODING_PRECISION')],
|
||||||
|
load: {
|
||||||
|
admin1: true,
|
||||||
|
admin2: true,
|
||||||
|
admin3And4: false,
|
||||||
|
alternateNames: false,
|
||||||
|
},
|
||||||
|
countries: [],
|
||||||
|
}).then(() => {
|
||||||
|
this.isGeocodeInitialized = true;
|
||||||
|
Logger.log('Reverse Geocoding Initialised');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
|
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async reverseGeocodeExif(latitude: number, longitude: number): Promise<{country: string, state: string, city: string}> {
|
||||||
|
const geoCodeInfo = await geocoderLookup([{ latitude, longitude }]);
|
||||||
|
|
||||||
|
const country = getName(geoCodeInfo.countryCode, 'en');
|
||||||
|
const city = geoCodeInfo.name;
|
||||||
|
|
||||||
|
let state = '';
|
||||||
|
if (geoCodeInfo.admin2Code.name) state += geoCodeInfo.admin2Code.name;
|
||||||
|
if (geoCodeInfo.admin1Code.name) {
|
||||||
|
if (geoCodeInfo.admin2Code.name) state += ', ';
|
||||||
|
state += geoCodeInfo.admin1Code.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { country, state, city }
|
||||||
|
}
|
||||||
|
|
||||||
@Process(exifExtractionProcessorName)
|
@Process(exifExtractionProcessorName)
|
||||||
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
|
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
|
||||||
try {
|
try {
|
||||||
@ -141,35 +219,11 @@ export class MetadataExtractionProcessor {
|
|||||||
* Get the city, state or region name of the asset
|
* Get the city, state or region name of the asset
|
||||||
* based on lat/lon GPS coordinates.
|
* based on lat/lon GPS coordinates.
|
||||||
*/
|
*/
|
||||||
if (this.geocodingClient && exifData['longitude'] && exifData['latitude']) {
|
if (this.isGeocodeInitialized && newExif.latitude && newExif.longitude) {
|
||||||
const geoCodeInfo: MapiResponse = await this.geocodingClient
|
const { country, state, city } = await this.reverseGeocodeExif(newExif.latitude, newExif.longitude);
|
||||||
.reverseGeocode({
|
newExif.country = country;
|
||||||
query: [exifData['longitude'], exifData['latitude']],
|
newExif.state = state;
|
||||||
types: ['country', 'region', 'place'],
|
newExif.city = city;
|
||||||
})
|
|
||||||
.send();
|
|
||||||
|
|
||||||
const res: [] = geoCodeInfo.body['features'];
|
|
||||||
|
|
||||||
let city = '';
|
|
||||||
let state = '';
|
|
||||||
let country = '';
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]) {
|
|
||||||
city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]) {
|
|
||||||
state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]) {
|
|
||||||
country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
newExif.city = city || null;
|
|
||||||
newExif.state = state || null;
|
|
||||||
newExif.country = country || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -204,35 +258,10 @@ export class MetadataExtractionProcessor {
|
|||||||
|
|
||||||
@Process({ name: reverseGeocodingProcessorName })
|
@Process({ name: reverseGeocodingProcessorName })
|
||||||
async reverseGeocoding(job: Job<IReverseGeocodingProcessor>) {
|
async reverseGeocoding(job: Job<IReverseGeocodingProcessor>) {
|
||||||
const { exif } = job.data;
|
if (this.isGeocodeInitialized) {
|
||||||
|
const { latitude, longitude } = job.data;
|
||||||
if (this.geocodingClient) {
|
const { country, state, city } = await this.reverseGeocodeExif(latitude, longitude);
|
||||||
const geoCodeInfo: MapiResponse = await this.geocodingClient
|
await this.exifRepository.update({ id: job.data.exifId }, { city, state, country });
|
||||||
.reverseGeocode({
|
|
||||||
query: [Number(exif.longitude), Number(exif.latitude)],
|
|
||||||
types: ['country', 'region', 'place'],
|
|
||||||
})
|
|
||||||
.send();
|
|
||||||
|
|
||||||
const res: [] = geoCodeInfo.body['features'];
|
|
||||||
|
|
||||||
let city = '';
|
|
||||||
let state = '';
|
|
||||||
let country = '';
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]) {
|
|
||||||
city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]) {
|
|
||||||
state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]) {
|
|
||||||
country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.exifRepository.update({ id: exif.id }, { city, state, country });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,35 +373,11 @@ export class MetadataExtractionProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reverse GeoCoding
|
// Reverse GeoCoding
|
||||||
if (this.geocodingClient && newExif.longitude && newExif.latitude) {
|
if (this.isGeocodeInitialized && newExif.longitude && newExif.latitude) {
|
||||||
const geoCodeInfo: MapiResponse = await this.geocodingClient
|
const { country, state, city } = await this.reverseGeocodeExif(newExif.latitude, newExif.longitude);
|
||||||
.reverseGeocode({
|
newExif.country = country;
|
||||||
query: [newExif.longitude, newExif.latitude],
|
newExif.state = state;
|
||||||
types: ['country', 'region', 'place'],
|
newExif.city = city;
|
||||||
})
|
|
||||||
.send();
|
|
||||||
|
|
||||||
const res: [] = geoCodeInfo.body['features'];
|
|
||||||
|
|
||||||
let city = '';
|
|
||||||
let state = '';
|
|
||||||
let country = '';
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]) {
|
|
||||||
city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]) {
|
|
||||||
state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]) {
|
|
||||||
country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
newExif.city = city || null;
|
|
||||||
newExif.state = state || null;
|
|
||||||
newExif.country = country || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const stream of data.streams) {
|
for (const stream of data.streams) {
|
||||||
|
@ -10,12 +10,8 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
DB_PASSWORD: Joi.string().required(),
|
DB_PASSWORD: Joi.string().required(),
|
||||||
DB_DATABASE_NAME: Joi.string().required(),
|
DB_DATABASE_NAME: Joi.string().required(),
|
||||||
JWT_SECRET: Joi.string().required(),
|
JWT_SECRET: Joi.string().required(),
|
||||||
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
|
DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
|
||||||
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
|
REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0,1,2,3).default(3),
|
||||||
is: false,
|
|
||||||
then: Joi.string().optional().allow(null, ''),
|
|
||||||
otherwise: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
|
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
|
||||||
|
|
||||||
export interface IExifExtractionProcessor {
|
export interface IExifExtractionProcessor {
|
||||||
/**
|
/**
|
||||||
@ -36,10 +35,9 @@ export interface IVideoLengthExtractionProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IReverseGeocodingProcessor {
|
export interface IReverseGeocodingProcessor {
|
||||||
/**
|
exifId: string;
|
||||||
* The Asset entity that was saved in the database
|
latitude: number;
|
||||||
*/
|
longitude: number;
|
||||||
exif: ExifEntity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IMetadataExtractionJob =
|
export type IMetadataExtractionJob =
|
||||||
|
1967
server/package-lock.json
generated
1967
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,6 @@
|
|||||||
"api:generate": "npm run api:typescript && npm run api:dart"
|
"api:generate": "npm run api:typescript && npm run api:dart"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-sdk": "^0.13.3",
|
|
||||||
"@nestjs/bull": "^0.5.5",
|
"@nestjs/bull": "^0.5.5",
|
||||||
"@nestjs/common": "^8.4.7",
|
"@nestjs/common": "^8.4.7",
|
||||||
"@nestjs/config": "^2.1.0",
|
"@nestjs/config": "^2.1.0",
|
||||||
@ -54,7 +53,9 @@
|
|||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"geo-tz": "^7.0.2",
|
"geo-tz": "^7.0.2",
|
||||||
|
"i18n-iso-countries": "^7.5.0",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
|
"local-reverse-geocoder": "^0.12.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.0.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
@ -85,7 +86,6 @@
|
|||||||
"@types/imagemin": "^8.0.0",
|
"@types/imagemin": "^8.0.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/mapbox__mapbox-sdk": "^0.13.4",
|
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
|
Loading…
Reference in New Issue
Block a user