mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
refactor(server)!: move markers and style to dedicated map endpoint/controller (#9832)
* move markers and style to dedicated map endpoint * chore: open api * chore: clean up repos --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
5ef144bf79
commit
5463660746
@ -641,95 +641,6 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /asset/map-marker', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
const files = [
|
|
||||||
'formats/avif/8bit-sRGB.avif',
|
|
||||||
'formats/jpg/el_torcal_rocks.jpg',
|
|
||||||
'formats/jxl/8bit-sRGB.jxl',
|
|
||||||
'formats/heic/IMG_2682.heic',
|
|
||||||
'formats/png/density_plot.png',
|
|
||||||
'formats/raw/Nikon/D80/glarus.nef',
|
|
||||||
'formats/raw/Nikon/D700/philadelphia.nef',
|
|
||||||
'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
|
|
||||||
'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
|
|
||||||
'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
|
|
||||||
];
|
|
||||||
utils.resetEvents();
|
|
||||||
const uploadFile = async (input: string) => {
|
|
||||||
const filepath = join(testAssetDir, input);
|
|
||||||
const { id } = await utils.createAsset(admin.accessToken, {
|
|
||||||
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
|
||||||
});
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
|
|
||||||
};
|
|
||||||
const uploads = files.map((f) => uploadFile(f));
|
|
||||||
await Promise.all(uploads);
|
|
||||||
}, 30_000);
|
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/map-marker');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO archive one of these assets
|
|
||||||
it('should get map markers for all non-archived assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/map-marker')
|
|
||||||
.query({ isArchived: false })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toHaveLength(2);
|
|
||||||
expect(body).toEqual([
|
|
||||||
{
|
|
||||||
city: 'Palisade',
|
|
||||||
country: 'United States of America',
|
|
||||||
id: expect.any(String),
|
|
||||||
lat: expect.closeTo(39.115),
|
|
||||||
lon: expect.closeTo(-108.400_968),
|
|
||||||
state: 'Colorado',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
city: 'Ralston',
|
|
||||||
country: 'United States of America',
|
|
||||||
id: expect.any(String),
|
|
||||||
lat: expect.closeTo(41.2203),
|
|
||||||
lon: expect.closeTo(-96.071_625),
|
|
||||||
state: 'Nebraska',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO archive one of these assets
|
|
||||||
it('should get all map markers', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/map-marker')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([
|
|
||||||
{
|
|
||||||
city: 'Palisade',
|
|
||||||
country: 'United States of America',
|
|
||||||
id: expect.any(String),
|
|
||||||
lat: expect.closeTo(39.115),
|
|
||||||
lon: expect.closeTo(-108.400_968),
|
|
||||||
state: 'Colorado',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
city: 'Ralston',
|
|
||||||
country: 'United States of America',
|
|
||||||
id: expect.any(String),
|
|
||||||
lat: expect.closeTo(41.2203),
|
|
||||||
lon: expect.closeTo(-96.071_625),
|
|
||||||
state: 'Nebraska',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /asset', () => {
|
describe('PUT /asset', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put('/asset');
|
const { status, body } = await request(app).put('/asset');
|
||||||
|
162
e2e/src/api/specs/map.e2e-spec.ts
Normal file
162
e2e/src/api/specs/map.e2e-spec.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { basename, join } from 'node:path';
|
||||||
|
import { Socket } from 'socket.io-client';
|
||||||
|
import { createUserDto } from 'src/fixtures';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, testAssetDir, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('/map', () => {
|
||||||
|
let websocket: Socket;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
let nonAdmin: LoginResponseDto;
|
||||||
|
let asset: AssetFileUploadResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
|
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
|
|
||||||
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
|
|
||||||
|
asset = await utils.createAsset(admin.accessToken);
|
||||||
|
|
||||||
|
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
|
||||||
|
utils.resetEvents();
|
||||||
|
const uploadFile = async (input: string) => {
|
||||||
|
const filepath = join(testAssetDir, input);
|
||||||
|
const { id } = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
||||||
|
});
|
||||||
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
|
||||||
|
};
|
||||||
|
await Promise.all(files.map((f) => uploadFile(f)));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
utils.disconnectWebsocket(websocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /map/markers', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/map/markers');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO archive one of these assets
|
||||||
|
it('should get map markers for all non-archived assets', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/map/markers')
|
||||||
|
.query({ isArchived: false })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(2);
|
||||||
|
expect(body).toEqual([
|
||||||
|
{
|
||||||
|
city: 'Palisade',
|
||||||
|
country: 'United States of America',
|
||||||
|
id: expect.any(String),
|
||||||
|
lat: expect.closeTo(39.115),
|
||||||
|
lon: expect.closeTo(-108.400_968),
|
||||||
|
state: 'Colorado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
city: 'Ralston',
|
||||||
|
country: 'United States of America',
|
||||||
|
id: expect.any(String),
|
||||||
|
lat: expect.closeTo(41.2203),
|
||||||
|
lon: expect.closeTo(-96.071_625),
|
||||||
|
state: 'Nebraska',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO archive one of these assets
|
||||||
|
it('should get all map markers', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/map/markers')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([
|
||||||
|
{
|
||||||
|
city: 'Palisade',
|
||||||
|
country: 'United States of America',
|
||||||
|
id: expect.any(String),
|
||||||
|
lat: expect.closeTo(39.115),
|
||||||
|
lon: expect.closeTo(-108.400_968),
|
||||||
|
state: 'Colorado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
city: 'Ralston',
|
||||||
|
country: 'United States of America',
|
||||||
|
id: expect.any(String),
|
||||||
|
lat: expect.closeTo(41.2203),
|
||||||
|
lon: expect.closeTo(-96.071_625),
|
||||||
|
state: 'Nebraska',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /map/style.json', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/map/style.json');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow shared link access', async () => {
|
||||||
|
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||||
|
type: SharedLinkType.Individual,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
});
|
||||||
|
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if a theme is not light or dark', async () => {
|
||||||
|
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/map/style.json')
|
||||||
|
.query({ theme })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the light style.json', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/map/style.json')
|
||||||
|
.query({ theme: 'light' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the dark style.json', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/map/style.json')
|
||||||
|
.query({ theme: 'dark' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not require admin authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/map/style.json')
|
||||||
|
.query({ theme: 'dark' })
|
||||||
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,4 @@
|
|||||||
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, getConfig } from '@immich/sdk';
|
import { LoginResponseDto, getConfig } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@ -9,74 +8,10 @@ const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAu
|
|||||||
|
|
||||||
describe('/system-config', () => {
|
describe('/system-config', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonAdmin: LoginResponseDto;
|
|
||||||
let asset: AssetFileUploadResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
|
||||||
|
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /system-config/map/style.json', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/system-config/map/style.json');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow shared link access', async () => {
|
|
||||||
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: [asset.id],
|
|
||||||
});
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/system-config/map/style.json?key=${sharedLink.key}`)
|
|
||||||
.query({ theme: 'dark' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if a theme is not light or dark', async () => {
|
|
||||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/system-config/map/style.json')
|
|
||||||
.query({ theme })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the light style.json', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/system-config/map/style.json')
|
|
||||||
.query({ theme: 'light' })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the dark style.json', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/system-config/map/style.json')
|
|
||||||
.query({ theme: 'dark' })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not require admin authentication', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/system-config/map/style.json')
|
|
||||||
.query({ theme: 'dark' })
|
|
||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /system-config', () => {
|
describe('PUT /system-config', () => {
|
||||||
|
@ -46,7 +46,7 @@ class MapStateNotifier extends _$MapStateNotifier {
|
|||||||
// Fetch and save light theme
|
// Fetch and save light theme
|
||||||
final lightResponse = await ref
|
final lightResponse = await ref
|
||||||
.read(apiServiceProvider)
|
.read(apiServiceProvider)
|
||||||
.systemConfigApi
|
.mapApi
|
||||||
.getMapStyleWithHttpInfo(MapTheme.light);
|
.getMapStyleWithHttpInfo(MapTheme.light);
|
||||||
|
|
||||||
if (lightResponse.statusCode >= HttpStatus.badRequest) {
|
if (lightResponse.statusCode >= HttpStatus.badRequest) {
|
||||||
@ -74,7 +74,7 @@ class MapStateNotifier extends _$MapStateNotifier {
|
|||||||
// Fetch and save dark theme
|
// Fetch and save dark theme
|
||||||
final darkResponse = await ref
|
final darkResponse = await ref
|
||||||
.read(apiServiceProvider)
|
.read(apiServiceProvider)
|
||||||
.systemConfigApi
|
.mapApi
|
||||||
.getMapStyleWithHttpInfo(MapTheme.dark);
|
.getMapStyleWithHttpInfo(MapTheme.dark);
|
||||||
|
|
||||||
if (darkResponse.statusCode >= HttpStatus.badRequest) {
|
if (darkResponse.statusCode >= HttpStatus.badRequest) {
|
||||||
|
@ -19,6 +19,7 @@ class ApiService {
|
|||||||
late AssetApi assetApi;
|
late AssetApi assetApi;
|
||||||
late SearchApi searchApi;
|
late SearchApi searchApi;
|
||||||
late ServerInfoApi serverInfoApi;
|
late ServerInfoApi serverInfoApi;
|
||||||
|
late MapApi mapApi;
|
||||||
late PartnerApi partnerApi;
|
late PartnerApi partnerApi;
|
||||||
late PersonApi personApi;
|
late PersonApi personApi;
|
||||||
late AuditApi auditApi;
|
late AuditApi auditApi;
|
||||||
@ -50,6 +51,7 @@ class ApiService {
|
|||||||
assetApi = AssetApi(_apiClient);
|
assetApi = AssetApi(_apiClient);
|
||||||
serverInfoApi = ServerInfoApi(_apiClient);
|
serverInfoApi = ServerInfoApi(_apiClient);
|
||||||
searchApi = SearchApi(_apiClient);
|
searchApi = SearchApi(_apiClient);
|
||||||
|
mapApi = MapApi(_apiClient);
|
||||||
partnerApi = PartnerApi(_apiClient);
|
partnerApi = PartnerApi(_apiClient);
|
||||||
personApi = PersonApi(_apiClient);
|
personApi = PersonApi(_apiClient);
|
||||||
auditApi = AuditApi(_apiClient);
|
auditApi = AuditApi(_apiClient);
|
||||||
|
@ -19,7 +19,7 @@ class MapSerivce with ErrorLoggerMixin {
|
|||||||
}) async {
|
}) async {
|
||||||
return logError(
|
return logError(
|
||||||
() async {
|
() async {
|
||||||
final markers = await _apiService.assetApi.getMapMarkers(
|
final markers = await _apiService.mapApi.getMapMarkers(
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
isArchived: withArchived,
|
isArchived: withArchived,
|
||||||
withPartners: withPartners,
|
withPartners: withPartners,
|
||||||
|
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
@ -100,7 +100,6 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetApi* | [**getAssetInfo**](doc//AssetApi.md#getassetinfo) | **GET** /asset/{id} |
|
*AssetApi* | [**getAssetInfo**](doc//AssetApi.md#getassetinfo) | **GET** /asset/{id} |
|
||||||
*AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics |
|
*AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics |
|
||||||
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} |
|
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} |
|
||||||
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
|
||||||
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||||
*AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random |
|
*AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random |
|
||||||
*AssetApi* | [**replaceAsset**](doc//AssetApi.md#replaceasset) | **PUT** /asset/{id}/file |
|
*AssetApi* | [**replaceAsset**](doc//AssetApi.md#replaceasset) | **PUT** /asset/{id}/file |
|
||||||
@ -136,6 +135,8 @@ Class | Method | HTTP request | Description
|
|||||||
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /libraries/{id}/scan |
|
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /libraries/{id}/scan |
|
||||||
*LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /libraries/{id} |
|
*LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /libraries/{id} |
|
||||||
*LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /libraries/{id}/validate |
|
*LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /libraries/{id}/validate |
|
||||||
|
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |
|
||||||
|
*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |
|
||||||
*MemoryApi* | [**addMemoryAssets**](doc//MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
|
*MemoryApi* | [**addMemoryAssets**](doc//MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
|
||||||
*MemoryApi* | [**createMemory**](doc//MemoryApi.md#creatememory) | **POST** /memories |
|
*MemoryApi* | [**createMemory**](doc//MemoryApi.md#creatememory) | **POST** /memories |
|
||||||
*MemoryApi* | [**deleteMemory**](doc//MemoryApi.md#deletememory) | **DELETE** /memories/{id} |
|
*MemoryApi* | [**deleteMemory**](doc//MemoryApi.md#deletememory) | **DELETE** /memories/{id} |
|
||||||
@ -192,7 +193,6 @@ Class | Method | HTTP request | Description
|
|||||||
*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync |
|
*SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync |
|
||||||
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
|
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
|
||||||
*SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
|
*SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
|
||||||
*SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json |
|
|
||||||
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
|
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
|
||||||
*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config |
|
*SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config |
|
||||||
*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding |
|
*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding |
|
||||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -41,6 +41,7 @@ part 'api/face_api.dart';
|
|||||||
part 'api/file_report_api.dart';
|
part 'api/file_report_api.dart';
|
||||||
part 'api/job_api.dart';
|
part 'api/job_api.dart';
|
||||||
part 'api/library_api.dart';
|
part 'api/library_api.dart';
|
||||||
|
part 'api/map_api.dart';
|
||||||
part 'api/memory_api.dart';
|
part 'api/memory_api.dart';
|
||||||
part 'api/o_auth_api.dart';
|
part 'api/o_auth_api.dart';
|
||||||
part 'api/partner_api.dart';
|
part 'api/partner_api.dart';
|
||||||
|
89
mobile/openapi/lib/api/asset_api.dart
generated
89
mobile/openapi/lib/api/asset_api.dart
generated
@ -399,95 +399,6 @@ class AssetApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileCreatedAfter:
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileCreatedBefore:
|
|
||||||
///
|
|
||||||
/// * [bool] isArchived:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
|
||||||
///
|
|
||||||
/// * [bool] withPartners:
|
|
||||||
///
|
|
||||||
/// * [bool] withSharedAlbums:
|
|
||||||
Future<Response> getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final path = r'/asset/map-marker';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody;
|
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
|
||||||
final formParams = <String, String>{};
|
|
||||||
|
|
||||||
if (fileCreatedAfter != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'fileCreatedAfter', fileCreatedAfter));
|
|
||||||
}
|
|
||||||
if (fileCreatedBefore != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'fileCreatedBefore', fileCreatedBefore));
|
|
||||||
}
|
|
||||||
if (isArchived != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
|
||||||
}
|
|
||||||
if (isFavorite != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
|
||||||
}
|
|
||||||
if (withPartners != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
|
||||||
}
|
|
||||||
if (withSharedAlbums != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'withSharedAlbums', withSharedAlbums));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
|
||||||
|
|
||||||
|
|
||||||
return apiClient.invokeAPI(
|
|
||||||
path,
|
|
||||||
'GET',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileCreatedAfter:
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileCreatedBefore:
|
|
||||||
///
|
|
||||||
/// * [bool] isArchived:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
|
||||||
///
|
|
||||||
/// * [bool] withPartners:
|
|
||||||
///
|
|
||||||
/// * [bool] withSharedAlbums:
|
|
||||||
Future<List<MapMarkerResponseDto>?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async {
|
|
||||||
final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<MapMarkerResponseDto>') as List)
|
|
||||||
.cast<MapMarkerResponseDto>()
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response].
|
/// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
163
mobile/openapi/lib/api/map_api.dart
generated
Normal file
163
mobile/openapi/lib/api/map_api.dart
generated
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class MapApi {
|
||||||
|
MapApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /map/markers' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DateTime] fileCreatedAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] fileCreatedBefore:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] withPartners:
|
||||||
|
///
|
||||||
|
/// * [bool] withSharedAlbums:
|
||||||
|
Future<Response> getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/map/markers';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (fileCreatedAfter != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'fileCreatedAfter', fileCreatedAfter));
|
||||||
|
}
|
||||||
|
if (fileCreatedBefore != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'fileCreatedBefore', fileCreatedBefore));
|
||||||
|
}
|
||||||
|
if (isArchived != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
||||||
|
}
|
||||||
|
if (isFavorite != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||||
|
}
|
||||||
|
if (withPartners != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
||||||
|
}
|
||||||
|
if (withSharedAlbums != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withSharedAlbums', withSharedAlbums));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DateTime] fileCreatedAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] fileCreatedBefore:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] withPartners:
|
||||||
|
///
|
||||||
|
/// * [bool] withSharedAlbums:
|
||||||
|
Future<List<MapMarkerResponseDto>?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async {
|
||||||
|
final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<MapMarkerResponseDto>') as List)
|
||||||
|
.cast<MapMarkerResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /map/style.json' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MapTheme] theme (required):
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
Future<Response> getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/map/style.json';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (key != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
|
}
|
||||||
|
queryParams.addAll(_queryParams('', 'theme', theme));
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MapTheme] theme (required):
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
Future<Object?> getMapStyle(MapTheme theme, { String? key, }) async {
|
||||||
|
final response = await getMapStyleWithHttpInfo(theme, key: key, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
56
mobile/openapi/lib/api/system_config_api.dart
generated
56
mobile/openapi/lib/api/system_config_api.dart
generated
@ -98,62 +98,6 @@ class SystemConfigApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /system-config/map/style.json' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [MapTheme] theme (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
Future<Response> getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final path = r'/system-config/map/style.json';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody;
|
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
|
||||||
final formParams = <String, String>{};
|
|
||||||
|
|
||||||
if (key != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
|
||||||
}
|
|
||||||
queryParams.addAll(_queryParams('', 'theme', theme));
|
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
|
||||||
|
|
||||||
|
|
||||||
return apiClient.invokeAPI(
|
|
||||||
path,
|
|
||||||
'GET',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [MapTheme] theme (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
Future<Object?> getMapStyle(MapTheme theme, { String? key, }) async {
|
|
||||||
final response = await getMapStyleWithHttpInfo(theme, key: key, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
|
||||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object;
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /system-config/storage-template-options' operation and returns the [Response].
|
/// Performs an HTTP 'GET /system-config/storage-template-options' operation and returns the [Response].
|
||||||
Future<Response> getStorageTemplateOptionsWithHttpInfo() async {
|
Future<Response> getStorageTemplateOptionsWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
|
@ -1598,92 +1598,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/asset/map-marker": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getMapMarkers",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "fileCreatedAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "fileCreatedBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withPartners",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withSharedAlbums",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/MapMarkerResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Asset"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/asset/memory-lane": {
|
"/asset/memory-lane": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getMemoryLane",
|
"operationId": "getMemoryLane",
|
||||||
@ -3131,6 +3045,141 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/map/markers": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getMapMarkers",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "fileCreatedAfter",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fileCreatedBefore",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withPartners",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withSharedAlbums",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MapMarkerResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Map"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/map/style.json": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getMapStyle",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "theme",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MapTheme"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Map"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/memories": {
|
"/memories": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "searchMemories",
|
"operationId": "searchMemories",
|
||||||
@ -5512,55 +5561,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/system-config/map/style.json": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getMapStyle",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "theme",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/MapTheme"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"System Config"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/system-config/storage-template-options": {
|
"/system-config/storage-template-options": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getStorageTemplateOptions",
|
"operationId": "getStorageTemplateOptions",
|
||||||
|
@ -303,14 +303,6 @@ export type AssetJobsDto = {
|
|||||||
assetIds: string[];
|
assetIds: string[];
|
||||||
name: AssetJobName;
|
name: AssetJobName;
|
||||||
};
|
};
|
||||||
export type MapMarkerResponseDto = {
|
|
||||||
city: string | null;
|
|
||||||
country: string | null;
|
|
||||||
id: string;
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
state: string | null;
|
|
||||||
};
|
|
||||||
export type MemoryLaneResponseDto = {
|
export type MemoryLaneResponseDto = {
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
yearsAgo: number;
|
yearsAgo: number;
|
||||||
@ -516,6 +508,14 @@ export type ValidateLibraryImportPathResponseDto = {
|
|||||||
export type ValidateLibraryResponseDto = {
|
export type ValidateLibraryResponseDto = {
|
||||||
importPaths?: ValidateLibraryImportPathResponseDto[];
|
importPaths?: ValidateLibraryImportPathResponseDto[];
|
||||||
};
|
};
|
||||||
|
export type MapMarkerResponseDto = {
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
id: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
state: string | null;
|
||||||
|
};
|
||||||
export type OnThisDayDto = {
|
export type OnThisDayDto = {
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
@ -1518,28 +1518,6 @@ export function runAssetJobs({ assetJobsDto }: {
|
|||||||
body: assetJobsDto
|
body: assetJobsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners, withSharedAlbums }: {
|
|
||||||
fileCreatedAfter?: string;
|
|
||||||
fileCreatedBefore?: string;
|
|
||||||
isArchived?: boolean;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
withPartners?: boolean;
|
|
||||||
withSharedAlbums?: boolean;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: MapMarkerResponseDto[];
|
|
||||||
}>(`/asset/map-marker${QS.query(QS.explode({
|
|
||||||
fileCreatedAfter,
|
|
||||||
fileCreatedBefore,
|
|
||||||
isArchived,
|
|
||||||
isFavorite,
|
|
||||||
withPartners,
|
|
||||||
withSharedAlbums
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getMemoryLane({ day, month }: {
|
export function getMemoryLane({ day, month }: {
|
||||||
day: number;
|
day: number;
|
||||||
month: number;
|
month: number;
|
||||||
@ -1930,6 +1908,42 @@ export function validate({ id, validateLibraryDto }: {
|
|||||||
body: validateLibraryDto
|
body: validateLibraryDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners, withSharedAlbums }: {
|
||||||
|
fileCreatedAfter?: string;
|
||||||
|
fileCreatedBefore?: string;
|
||||||
|
isArchived?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
withPartners?: boolean;
|
||||||
|
withSharedAlbums?: boolean;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: MapMarkerResponseDto[];
|
||||||
|
}>(`/map/markers${QS.query(QS.explode({
|
||||||
|
fileCreatedAfter,
|
||||||
|
fileCreatedBefore,
|
||||||
|
isArchived,
|
||||||
|
isFavorite,
|
||||||
|
withPartners,
|
||||||
|
withSharedAlbums
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function getMapStyle({ key, theme }: {
|
||||||
|
key?: string;
|
||||||
|
theme: MapTheme;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: object;
|
||||||
|
}>(`/map/style.json${QS.query(QS.explode({
|
||||||
|
key,
|
||||||
|
theme
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function searchMemories(opts?: Oazapfts.RequestOpts) {
|
export function searchMemories(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
@ -2568,20 +2582,6 @@ export function getConfigDefaults(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getMapStyle({ key, theme }: {
|
|
||||||
key?: string;
|
|
||||||
theme: MapTheme;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: object;
|
|
||||||
}>(`/system-config/map/style.json${QS.query(QS.explode({
|
|
||||||
key,
|
|
||||||
theme
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) {
|
export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
@ -2977,6 +2977,10 @@ export enum JobCommand {
|
|||||||
Empty = "empty",
|
Empty = "empty",
|
||||||
ClearFailed = "clear-failed"
|
ClearFailed = "clear-failed"
|
||||||
}
|
}
|
||||||
|
export enum MapTheme {
|
||||||
|
Light = "light",
|
||||||
|
Dark = "dark"
|
||||||
|
}
|
||||||
export enum Type2 {
|
export enum Type2 {
|
||||||
OnThisDay = "on_this_day"
|
OnThisDay = "on_this_day"
|
||||||
}
|
}
|
||||||
@ -3073,10 +3077,6 @@ export enum ModelType {
|
|||||||
FacialRecognition = "facial-recognition",
|
FacialRecognition = "facial-recognition",
|
||||||
Clip = "clip"
|
Clip = "clip"
|
||||||
}
|
}
|
||||||
export enum MapTheme {
|
|
||||||
Light = "light",
|
|
||||||
Dark = "dark"
|
|
||||||
}
|
|
||||||
export enum TimeBucketSize {
|
export enum TimeBucketSize {
|
||||||
Day = "DAY",
|
Day = "DAY",
|
||||||
Month = "MONTH"
|
Month = "MONTH"
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
UpdateAssetDto,
|
UpdateAssetDto,
|
||||||
} from 'src/dtos/asset.dto';
|
} from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { Route } from 'src/middleware/file-upload.interceptor';
|
import { Route } from 'src/middleware/file-upload.interceptor';
|
||||||
@ -24,12 +24,6 @@ import { UUIDParamDto } from 'src/validation';
|
|||||||
export class AssetController {
|
export class AssetController {
|
||||||
constructor(private service: AssetService) {}
|
constructor(private service: AssetService) {}
|
||||||
|
|
||||||
@Get('map-marker')
|
|
||||||
@Authenticated()
|
|
||||||
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
|
||||||
return this.service.getMapMarkers(auth, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('memory-lane')
|
@Get('memory-lane')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||||
|
@ -13,6 +13,7 @@ import { FaceController } from 'src/controllers/face.controller';
|
|||||||
import { ReportController } from 'src/controllers/file-report.controller';
|
import { ReportController } from 'src/controllers/file-report.controller';
|
||||||
import { JobController } from 'src/controllers/job.controller';
|
import { JobController } from 'src/controllers/job.controller';
|
||||||
import { LibraryController } from 'src/controllers/library.controller';
|
import { LibraryController } from 'src/controllers/library.controller';
|
||||||
|
import { MapController } from 'src/controllers/map.controller';
|
||||||
import { MemoryController } from 'src/controllers/memory.controller';
|
import { MemoryController } from 'src/controllers/memory.controller';
|
||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
@ -45,6 +46,7 @@ export const controllers = [
|
|||||||
FaceController,
|
FaceController,
|
||||||
JobController,
|
JobController,
|
||||||
LibraryController,
|
LibraryController,
|
||||||
|
MapController,
|
||||||
MemoryController,
|
MemoryController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
|
25
server/src/controllers/map.controller.ts
Normal file
25
server/src/controllers/map.controller.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto';
|
||||||
|
import { MapThemeDto } from 'src/dtos/system-config.dto';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { MapService } from 'src/services/map.service';
|
||||||
|
|
||||||
|
@ApiTags('Map')
|
||||||
|
@Controller('map')
|
||||||
|
export class MapController {
|
||||||
|
constructor(private service: MapService) {}
|
||||||
|
|
||||||
|
@Get('markers')
|
||||||
|
@Authenticated()
|
||||||
|
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||||
|
return this.service.getMapMarkers(auth, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authenticated({ sharedLink: true })
|
||||||
|
@Get('style.json')
|
||||||
|
getMapStyle(@Query() dto: MapThemeDto) {
|
||||||
|
return this.service.getMapStyle(dto.theme);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Get, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||||
import { Authenticated } from 'src/middleware/auth.guard';
|
import { Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@ -32,10 +32,4 @@ export class SystemConfigController {
|
|||||||
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
||||||
return this.service.getStorageTemplateOptions();
|
return this.service.getStorageTemplateOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated({ sharedLink: true })
|
|
||||||
@Get('map/style.json')
|
|
||||||
getMapStyle(@Query() dto: MapThemeDto) {
|
|
||||||
return this.service.getMapStyle(dto.theme);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { AssetOrder } from 'src/entities/album.entity';
|
|||||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { ReverseGeocodeResult } from 'src/interfaces/metadata.interface';
|
|
||||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||||
import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||||
@ -22,19 +21,6 @@ export interface LivePhotoSearchOptions {
|
|||||||
type: AssetType;
|
type: AssetType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MapMarkerSearchOptions {
|
|
||||||
isArchived?: boolean;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
fileCreatedBefore?: Date;
|
|
||||||
fileCreatedAfter?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MapMarker extends ReverseGeocodeResult {
|
|
||||||
id: string;
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WithoutProperty {
|
export enum WithoutProperty {
|
||||||
THUMBNAIL = 'thumbnail',
|
THUMBNAIL = 'thumbnail',
|
||||||
ENCODED_VIDEO = 'encoded-video',
|
ENCODED_VIDEO = 'encoded-video',
|
||||||
@ -195,7 +181,6 @@ export interface IAssetRepository {
|
|||||||
softDeleteAll(ids: string[]): Promise<void>;
|
softDeleteAll(ids: string[]): Promise<void>;
|
||||||
restoreAll(ids: string[]): Promise<void>;
|
restoreAll(ids: string[]): Promise<void>;
|
||||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||||
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
|
||||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||||
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
||||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||||
|
32
server/src/interfaces/map.interface.ts
Normal file
32
server/src/interfaces/map.interface.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export const IMapRepository = 'IMapRepository';
|
||||||
|
|
||||||
|
export interface MapMarkerSearchOptions {
|
||||||
|
isArchived?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
fileCreatedBefore?: Date;
|
||||||
|
fileCreatedAfter?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoPoint {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReverseGeocodeResult {
|
||||||
|
country: string | null;
|
||||||
|
state: string | null;
|
||||||
|
city: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MapMarker extends ReverseGeocodeResult {
|
||||||
|
id: string;
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapRepository {
|
||||||
|
init(): Promise<void>;
|
||||||
|
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null>;
|
||||||
|
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||||
|
fetchStyle(url: string): Promise<any>;
|
||||||
|
}
|
@ -2,17 +2,6 @@ import { BinaryField, Tags } from 'exiftool-vendored';
|
|||||||
|
|
||||||
export const IMetadataRepository = 'IMetadataRepository';
|
export const IMetadataRepository = 'IMetadataRepository';
|
||||||
|
|
||||||
export interface GeoPoint {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReverseGeocodeResult {
|
|
||||||
country: string | null;
|
|
||||||
state: string | null;
|
|
||||||
city: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExifDuration {
|
export interface ExifDuration {
|
||||||
Value: number;
|
Value: number;
|
||||||
Scale?: number;
|
Scale?: number;
|
||||||
@ -33,9 +22,7 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetadataRepository {
|
export interface IMetadataRepository {
|
||||||
init(): Promise<void>;
|
|
||||||
teardown(): Promise<void>;
|
teardown(): Promise<void>;
|
||||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null>;
|
|
||||||
readTags(path: string): Promise<ImmichTags | null>;
|
readTags(path: string): Promise<ImmichTags | null>;
|
||||||
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
||||||
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
||||||
|
@ -5,6 +5,5 @@ export const ISystemMetadataRepository = 'ISystemMetadataRepository';
|
|||||||
export interface ISystemMetadataRepository {
|
export interface ISystemMetadataRepository {
|
||||||
get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
|
get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
|
||||||
set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
|
set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
|
||||||
fetchStyle(url: string): Promise<any>;
|
|
||||||
readFile(filename: string): Promise<string>;
|
readFile(filename: string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,6 @@ import {
|
|||||||
AssetUpdateOptions,
|
AssetUpdateOptions,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
LivePhotoSearchOptions,
|
LivePhotoSearchOptions,
|
||||||
MapMarker,
|
|
||||||
MapMarkerSearchOptions,
|
|
||||||
MonthDay,
|
MonthDay,
|
||||||
TimeBucketItem,
|
TimeBucketItem,
|
||||||
TimeBucketOptions,
|
TimeBucketOptions,
|
||||||
@ -31,7 +29,7 @@ import {
|
|||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from 'src/interfaces/asset.interface';
|
} from 'src/interfaces/asset.interface';
|
||||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||||
import { OptionalBetween, searchAssetBuilder } from 'src/utils/database';
|
import { searchAssetBuilder } from 'src/utils/database';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import {
|
import {
|
||||||
@ -547,57 +545,6 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMapMarkers(
|
|
||||||
ownerIds: string[],
|
|
||||||
albumIds: string[],
|
|
||||||
options: MapMarkerSearchOptions = {},
|
|
||||||
): Promise<MapMarker[]> {
|
|
||||||
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
|
||||||
|
|
||||||
const where = {
|
|
||||||
isVisible: true,
|
|
||||||
isArchived,
|
|
||||||
exifInfo: {
|
|
||||||
latitude: Not(IsNull()),
|
|
||||||
longitude: Not(IsNull()),
|
|
||||||
},
|
|
||||||
isFavorite,
|
|
||||||
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
|
|
||||||
};
|
|
||||||
|
|
||||||
const assets = await this.repository.find({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
exifInfo: {
|
|
||||||
city: true,
|
|
||||||
state: true,
|
|
||||||
country: true,
|
|
||||||
latitude: true,
|
|
||||||
longitude: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: [
|
|
||||||
{ ...where, ownerId: In([...ownerIds]) },
|
|
||||||
{ ...where, albums: { id: In([...albumIds]) } },
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
fileCreatedAt: 'DESC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return assets.map((asset) => ({
|
|
||||||
id: asset.id,
|
|
||||||
lat: asset.exifInfo!.latitude!,
|
|
||||||
lon: asset.exifInfo!.longitude!,
|
|
||||||
city: asset.exifInfo!.city,
|
|
||||||
state: asset.exifInfo!.state,
|
|
||||||
country: asset.exifInfo!.country,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> {
|
async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> {
|
||||||
const builder = this.repository
|
const builder = this.repository
|
||||||
.createQueryBuilder('asset')
|
.createQueryBuilder('asset')
|
||||||
|
@ -14,6 +14,7 @@ import { IJobRepository } from 'src/interfaces/job.interface';
|
|||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||||
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
import { IMemoryRepository } from 'src/interfaces/memory.interface';
|
||||||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||||
@ -46,6 +47,7 @@ import { JobRepository } from 'src/repositories/job.repository';
|
|||||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||||
import { LoggerRepository } from 'src/repositories/logger.repository';
|
import { LoggerRepository } from 'src/repositories/logger.repository';
|
||||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||||
|
import { MapRepository } from 'src/repositories/map.repository';
|
||||||
import { MediaRepository } from 'src/repositories/media.repository';
|
import { MediaRepository } from 'src/repositories/media.repository';
|
||||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
@ -64,8 +66,8 @@ import { TagRepository } from 'src/repositories/tag.repository';
|
|||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
|
|
||||||
export const repositories = [
|
export const repositories = [
|
||||||
{ provide: IActivityRepository, useClass: ActivityRepository },
|
|
||||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||||
|
{ provide: IActivityRepository, useClass: ActivityRepository },
|
||||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||||
@ -76,10 +78,12 @@ export const repositories = [
|
|||||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||||
{ provide: IEventRepository, useClass: EventRepository },
|
{ provide: IEventRepository, useClass: EventRepository },
|
||||||
{ provide: IJobRepository, useClass: JobRepository },
|
{ provide: IJobRepository, useClass: JobRepository },
|
||||||
{ provide: ILoggerRepository, useClass: LoggerRepository },
|
|
||||||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
|
||||||
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
||||||
|
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||||
|
{ provide: ILoggerRepository, useClass: LoggerRepository },
|
||||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||||
|
{ provide: IMapRepository, useClass: MapRepository },
|
||||||
|
{ provide: IMediaRepository, useClass: MediaRepository },
|
||||||
{ provide: IMemoryRepository, useClass: MemoryRepository },
|
{ provide: IMemoryRepository, useClass: MemoryRepository },
|
||||||
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
||||||
{ provide: IMetricRepository, useClass: MetricRepository },
|
{ provide: IMetricRepository, useClass: MetricRepository },
|
||||||
@ -87,13 +91,12 @@ export const repositories = [
|
|||||||
{ provide: INotificationRepository, useClass: NotificationRepository },
|
{ provide: INotificationRepository, useClass: NotificationRepository },
|
||||||
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
|
||||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
|
||||||
{ provide: ISearchRepository, useClass: SearchRepository },
|
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||||
|
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||||
{ provide: ISessionRepository, useClass: SessionRepository },
|
{ provide: ISessionRepository, useClass: SessionRepository },
|
||||||
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||||
{ provide: ITagRepository, useClass: TagRepository },
|
{ provide: ITagRepository, useClass: TagRepository },
|
||||||
{ provide: IMediaRepository, useClass: MediaRepository },
|
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
];
|
];
|
||||||
|
246
server/src/repositories/map.repository.ts
Normal file
246
server/src/repositories/map.repository.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { getName } from 'i18n-iso-countries';
|
||||||
|
import { createReadStream, existsSync } from 'node:fs';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import readLine from 'node:readline';
|
||||||
|
import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants';
|
||||||
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
|
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||||
|
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||||
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import {
|
||||||
|
GeoPoint,
|
||||||
|
IMapRepository,
|
||||||
|
MapMarker,
|
||||||
|
MapMarkerSearchOptions,
|
||||||
|
ReverseGeocodeResult,
|
||||||
|
} from 'src/interfaces/map.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { OptionalBetween } from 'src/utils/database';
|
||||||
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
|
import { DataSource, In, IsNull, Not, QueryRunner, Repository } from 'typeorm';
|
||||||
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||||
|
|
||||||
|
@Instrumentation()
|
||||||
|
@Injectable()
|
||||||
|
export class MapRepository implements IMapRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||||
|
@InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
|
||||||
|
@InjectDataSource() private dataSource: DataSource,
|
||||||
|
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
|
||||||
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(MapRepository.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
this.logger.log('Initializing metadata repository');
|
||||||
|
const geodataDate = await readFile(geodataDatePath, 'utf8');
|
||||||
|
|
||||||
|
// TODO move to service init
|
||||||
|
const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
||||||
|
if (geocodingMetadata?.lastUpdate === geodataDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.importGeodata();
|
||||||
|
|
||||||
|
await this.metadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, {
|
||||||
|
lastUpdate: geodataDate,
|
||||||
|
lastImportFileName: citiesFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log('Geodata import completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMapMarkers(
|
||||||
|
ownerIds: string[],
|
||||||
|
albumIds: string[],
|
||||||
|
options: MapMarkerSearchOptions = {},
|
||||||
|
): Promise<MapMarker[]> {
|
||||||
|
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
||||||
|
|
||||||
|
const where = {
|
||||||
|
isVisible: true,
|
||||||
|
isArchived,
|
||||||
|
exifInfo: {
|
||||||
|
latitude: Not(IsNull()),
|
||||||
|
longitude: Not(IsNull()),
|
||||||
|
},
|
||||||
|
isFavorite,
|
||||||
|
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
|
||||||
|
};
|
||||||
|
|
||||||
|
const assets = await this.assetRepository.find({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
exifInfo: {
|
||||||
|
city: true,
|
||||||
|
state: true,
|
||||||
|
country: true,
|
||||||
|
latitude: true,
|
||||||
|
longitude: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: [
|
||||||
|
{ ...where, ownerId: In([...ownerIds]) },
|
||||||
|
{ ...where, albums: { id: In([...albumIds]) } },
|
||||||
|
],
|
||||||
|
relations: {
|
||||||
|
exifInfo: true,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
fileCreatedAt: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return assets.map((asset) => ({
|
||||||
|
id: asset.id,
|
||||||
|
lat: asset.exifInfo!.latitude!,
|
||||||
|
lon: asset.exifInfo!.longitude!,
|
||||||
|
city: asset.exifInfo!.city,
|
||||||
|
state: asset.exifInfo!.state,
|
||||||
|
country: asset.exifInfo!.country,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchStyle(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to fetch data from ${url}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null> {
|
||||||
|
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
||||||
|
|
||||||
|
const response = await this.geodataPlacesRepository
|
||||||
|
.createQueryBuilder('geoplaces')
|
||||||
|
.where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point)
|
||||||
|
.orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")')
|
||||||
|
.limit(1)
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
|
||||||
|
|
||||||
|
const { countryCode, name: city, admin1Name } = response;
|
||||||
|
const country = getName(countryCode, 'en') ?? null;
|
||||||
|
const state = admin1Name;
|
||||||
|
|
||||||
|
return { country, state, city };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async importGeodata() {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
|
||||||
|
const admin1 = await this.loadAdmin(geodataAdmin1Path);
|
||||||
|
const admin2 = await this.loadAdmin(geodataAdmin2Path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
await queryRunner.manager.clear(GeodataPlacesEntity);
|
||||||
|
await this.loadCities500(queryRunner, admin1, admin2);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.fatal('Error importing geodata', error);
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadGeodataToTableFromFile(
|
||||||
|
queryRunner: QueryRunner,
|
||||||
|
lineToEntityMapper: (lineSplit: string[]) => GeodataPlacesEntity,
|
||||||
|
filePath: string,
|
||||||
|
options?: { entityFilter?: (linesplit: string[]) => boolean },
|
||||||
|
) {
|
||||||
|
const _entityFilter = options?.entityFilter ?? (() => true);
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
this.logger.error(`Geodata file ${filePath} not found`);
|
||||||
|
throw new Error(`Geodata file ${filePath} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = createReadStream(filePath);
|
||||||
|
let bufferGeodata: QueryDeepPartialEntity<GeodataPlacesEntity>[] = [];
|
||||||
|
const lineReader = readLine.createInterface({ input });
|
||||||
|
|
||||||
|
for await (const line of lineReader) {
|
||||||
|
const lineSplit = line.split('\t');
|
||||||
|
if (!_entityFilter(lineSplit)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const geoData = lineToEntityMapper(lineSplit);
|
||||||
|
bufferGeodata.push(geoData);
|
||||||
|
if (bufferGeodata.length > 1000) {
|
||||||
|
await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
|
||||||
|
bufferGeodata = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadCities500(
|
||||||
|
queryRunner: QueryRunner,
|
||||||
|
admin1Map: Map<string, string>,
|
||||||
|
admin2Map: Map<string, string>,
|
||||||
|
) {
|
||||||
|
await this.loadGeodataToTableFromFile(
|
||||||
|
queryRunner,
|
||||||
|
(lineSplit: string[]) =>
|
||||||
|
this.geodataPlacesRepository.create({
|
||||||
|
id: Number.parseInt(lineSplit[0]),
|
||||||
|
name: lineSplit[1],
|
||||||
|
alternateNames: lineSplit[3],
|
||||||
|
latitude: Number.parseFloat(lineSplit[4]),
|
||||||
|
longitude: Number.parseFloat(lineSplit[5]),
|
||||||
|
countryCode: lineSplit[8],
|
||||||
|
admin1Code: lineSplit[10],
|
||||||
|
admin2Code: lineSplit[11],
|
||||||
|
modificationDate: lineSplit[18],
|
||||||
|
admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
|
||||||
|
admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
|
||||||
|
}),
|
||||||
|
geodataCities500Path,
|
||||||
|
{ entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadAdmin(filePath: string) {
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
this.logger.error(`Geodata file ${filePath} not found`);
|
||||||
|
throw new Error(`Geodata file ${filePath} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = createReadStream(filePath);
|
||||||
|
const lineReader = readLine.createInterface({ input: input });
|
||||||
|
|
||||||
|
const adminMap = new Map<string, string>();
|
||||||
|
for await (const line of lineReader) {
|
||||||
|
const lineSplit = line.split('\t');
|
||||||
|
adminMap.set(lineSplit[0], lineSplit[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return adminMap;
|
||||||
|
}
|
||||||
|
}
|
@ -2,21 +2,13 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored';
|
import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored';
|
||||||
import geotz from 'geo-tz';
|
import geotz from 'geo-tz';
|
||||||
import { getName } from 'i18n-iso-countries';
|
|
||||||
import { createReadStream, existsSync } from 'node:fs';
|
|
||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
import readLine from 'node:readline';
|
|
||||||
import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants';
|
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from 'src/interfaces/metadata.interface';
|
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { DataSource, QueryRunner, Repository } from 'typeorm';
|
import { DataSource, Repository } from 'typeorm';
|
||||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -24,162 +16,16 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
|
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
|
||||||
@InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
|
@InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
|
||||||
@Inject(ISystemMetadataRepository)
|
|
||||||
private systemMetadataRepository: ISystemMetadataRepository,
|
|
||||||
@InjectDataSource() private dataSource: DataSource,
|
@InjectDataSource() private dataSource: DataSource,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(MetadataRepository.name);
|
this.logger.setContext(MetadataRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
|
||||||
this.logger.log('Initializing metadata repository');
|
|
||||||
const geodataDate = await readFile(geodataDatePath, 'utf8');
|
|
||||||
|
|
||||||
// TODO move to metadata service init
|
|
||||||
const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
|
||||||
if (geocodingMetadata?.lastUpdate === geodataDate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.importGeodata();
|
|
||||||
|
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, {
|
|
||||||
lastUpdate: geodataDate,
|
|
||||||
lastImportFileName: citiesFile,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log('Geodata import completed');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async importGeodata() {
|
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
|
||||||
await queryRunner.connect();
|
|
||||||
|
|
||||||
const admin1 = await this.loadAdmin(geodataAdmin1Path);
|
|
||||||
const admin2 = await this.loadAdmin(geodataAdmin2Path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
|
|
||||||
await queryRunner.manager.clear(GeodataPlacesEntity);
|
|
||||||
await this.loadCities500(queryRunner, admin1, admin2);
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.fatal('Error importing geodata', error);
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadGeodataToTableFromFile(
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
lineToEntityMapper: (lineSplit: string[]) => GeodataPlacesEntity,
|
|
||||||
filePath: string,
|
|
||||||
options?: { entityFilter?: (linesplit: string[]) => boolean },
|
|
||||||
) {
|
|
||||||
const _entityFilter = options?.entityFilter ?? (() => true);
|
|
||||||
if (!existsSync(filePath)) {
|
|
||||||
this.logger.error(`Geodata file ${filePath} not found`);
|
|
||||||
throw new Error(`Geodata file ${filePath} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = createReadStream(filePath);
|
|
||||||
let bufferGeodata: QueryDeepPartialEntity<GeodataPlacesEntity>[] = [];
|
|
||||||
const lineReader = readLine.createInterface({ input });
|
|
||||||
|
|
||||||
for await (const line of lineReader) {
|
|
||||||
const lineSplit = line.split('\t');
|
|
||||||
if (!_entityFilter(lineSplit)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const geoData = lineToEntityMapper(lineSplit);
|
|
||||||
bufferGeodata.push(geoData);
|
|
||||||
if (bufferGeodata.length > 1000) {
|
|
||||||
await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
|
|
||||||
bufferGeodata = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadCities500(
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
admin1Map: Map<string, string>,
|
|
||||||
admin2Map: Map<string, string>,
|
|
||||||
) {
|
|
||||||
await this.loadGeodataToTableFromFile(
|
|
||||||
queryRunner,
|
|
||||||
(lineSplit: string[]) =>
|
|
||||||
this.geodataPlacesRepository.create({
|
|
||||||
id: Number.parseInt(lineSplit[0]),
|
|
||||||
name: lineSplit[1],
|
|
||||||
alternateNames: lineSplit[3],
|
|
||||||
latitude: Number.parseFloat(lineSplit[4]),
|
|
||||||
longitude: Number.parseFloat(lineSplit[5]),
|
|
||||||
countryCode: lineSplit[8],
|
|
||||||
admin1Code: lineSplit[10],
|
|
||||||
admin2Code: lineSplit[11],
|
|
||||||
modificationDate: lineSplit[18],
|
|
||||||
admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
|
|
||||||
admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
|
|
||||||
}),
|
|
||||||
geodataCities500Path,
|
|
||||||
{ entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadAdmin(filePath: string) {
|
|
||||||
if (!existsSync(filePath)) {
|
|
||||||
this.logger.error(`Geodata file ${filePath} not found`);
|
|
||||||
throw new Error(`Geodata file ${filePath} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = createReadStream(filePath);
|
|
||||||
const lineReader = readLine.createInterface({ input: input });
|
|
||||||
|
|
||||||
const adminMap = new Map<string, string>();
|
|
||||||
for await (const line of lineReader) {
|
|
||||||
const lineSplit = line.split('\t');
|
|
||||||
adminMap.set(lineSplit[0], lineSplit[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return adminMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
async teardown() {
|
async teardown() {
|
||||||
await exiftool.end();
|
await exiftool.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null> {
|
|
||||||
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
|
||||||
|
|
||||||
const response = await this.geodataPlacesRepository
|
|
||||||
.createQueryBuilder('geoplaces')
|
|
||||||
.where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point)
|
|
||||||
.orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")')
|
|
||||||
.limit(1)
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
|
|
||||||
|
|
||||||
const { countryCode, name: city, admin1Name } = response;
|
|
||||||
const country = getName(countryCode, 'en') ?? null;
|
|
||||||
const state = admin1Name;
|
|
||||||
|
|
||||||
return { country, state, city };
|
|
||||||
}
|
|
||||||
|
|
||||||
readTags(path: string): Promise<ImmichTags | null> {
|
readTags(path: string): Promise<ImmichTags | null> {
|
||||||
return exiftool
|
return exiftool
|
||||||
.read(path, undefined, {
|
.read(path, undefined, {
|
||||||
|
@ -26,20 +26,6 @@ export class SystemMetadataRepository implements ISystemMetadataRepository {
|
|||||||
await this.repository.upsert({ key, value }, { conflictPaths: { key: true } });
|
await this.repository.upsert({ key, value }, { conflictPaths: { key: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchStyle(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to fetch data from ${url}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readFile(filename: string): Promise<string> {
|
readFile(filename: string): Promise<string> {
|
||||||
return readFile(filename, { encoding: 'utf8' });
|
return readFile(filename, { encoding: 'utf8' });
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
|||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@ -19,7 +18,6 @@ import { faceStub } from 'test/fixtures/face.stub';
|
|||||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
|
||||||
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
@ -162,7 +160,6 @@ describe(AssetService.name, () => {
|
|||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let partnerMock: Mocked<IPartnerRepository>;
|
let partnerMock: Mocked<IPartnerRepository>;
|
||||||
let assetStackMock: Mocked<IAssetStackRepository>;
|
let assetStackMock: Mocked<IAssetStackRepository>;
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -185,7 +182,6 @@ describe(AssetService.name, () => {
|
|||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
partnerMock = newPartnerRepositoryMock();
|
partnerMock = newPartnerRepositoryMock();
|
||||||
assetStackMock = newAssetStackRepositoryMock();
|
assetStackMock = newAssetStackRepositoryMock();
|
||||||
albumMock = newAlbumRepositoryMock();
|
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new AssetService(
|
sut = new AssetService(
|
||||||
@ -198,7 +194,6 @@ describe(AssetService.name, () => {
|
|||||||
eventMock,
|
eventMock,
|
||||||
partnerMock,
|
partnerMock,
|
||||||
assetStackMock,
|
assetStackMock,
|
||||||
albumMock,
|
|
||||||
loggerMock,
|
loggerMock,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -314,27 +309,6 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMapMarkers', () => {
|
|
||||||
it('should get geo information of assets', async () => {
|
|
||||||
const asset = assetStub.withLocation;
|
|
||||||
const marker = {
|
|
||||||
id: asset.id,
|
|
||||||
lat: asset.exifInfo!.latitude!,
|
|
||||||
lon: asset.exifInfo!.longitude!,
|
|
||||||
city: asset.exifInfo!.city,
|
|
||||||
state: asset.exifInfo!.state,
|
|
||||||
country: asset.exifInfo!.country,
|
|
||||||
};
|
|
||||||
partnerMock.getAll.mockResolvedValue([]);
|
|
||||||
assetMock.getMapMarkers.mockResolvedValue([marker]);
|
|
||||||
|
|
||||||
const markers = await sut.getMapMarkers(authStub.user1, {});
|
|
||||||
|
|
||||||
expect(markers).toHaveLength(1);
|
|
||||||
expect(markers[0]).toEqual(marker);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMemoryLane', () => {
|
describe('getMemoryLane', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vitest.useFakeTimers();
|
vitest.useFakeTimers();
|
||||||
|
@ -24,11 +24,10 @@ import {
|
|||||||
mapStats,
|
mapStats,
|
||||||
} from 'src/dtos/asset.dto';
|
} from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@ -65,7 +64,6 @@ export class AssetService {
|
|||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
|
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(AssetService.name);
|
this.logger.setContext(AssetService.name);
|
||||||
@ -153,30 +151,6 @@ export class AssetService {
|
|||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
|
||||||
const userIds: string[] = [auth.user.id];
|
|
||||||
// TODO convert to SQL join
|
|
||||||
if (options.withPartners) {
|
|
||||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
|
||||||
const partnersIds = partners
|
|
||||||
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id)
|
|
||||||
.map((partner) => partner.sharedById);
|
|
||||||
userIds.push(...partnersIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO convert to SQL join
|
|
||||||
const albumIds: string[] = [];
|
|
||||||
if (options.withSharedAlbums) {
|
|
||||||
const [ownedAlbums, sharedAlbums] = await Promise.all([
|
|
||||||
this.albumRepository.getOwned(auth.user.id),
|
|
||||||
this.albumRepository.getShared(auth.user.id),
|
|
||||||
]);
|
|
||||||
albumIds.push(...ownedAlbums.map((album) => album.id), ...sharedAlbums.map((album) => album.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.assetRepository.getMapMarkers(userIds, albumIds, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import { DownloadService } from 'src/services/download.service';
|
|||||||
import { DuplicateService } from 'src/services/duplicate.service';
|
import { DuplicateService } from 'src/services/duplicate.service';
|
||||||
import { JobService } from 'src/services/job.service';
|
import { JobService } from 'src/services/job.service';
|
||||||
import { LibraryService } from 'src/services/library.service';
|
import { LibraryService } from 'src/services/library.service';
|
||||||
|
import { MapService } from 'src/services/map.service';
|
||||||
import { MediaService } from 'src/services/media.service';
|
import { MediaService } from 'src/services/media.service';
|
||||||
import { MemoryService } from 'src/services/memory.service';
|
import { MemoryService } from 'src/services/memory.service';
|
||||||
import { MetadataService } from 'src/services/metadata.service';
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
@ -38,11 +39,10 @@ import { UserService } from 'src/services/user.service';
|
|||||||
import { VersionService } from 'src/services/version.service';
|
import { VersionService } from 'src/services/version.service';
|
||||||
|
|
||||||
export const services = [
|
export const services = [
|
||||||
ApiService,
|
|
||||||
MicroservicesService,
|
|
||||||
APIKeyService,
|
APIKeyService,
|
||||||
ActivityService,
|
ActivityService,
|
||||||
AlbumService,
|
AlbumService,
|
||||||
|
ApiService,
|
||||||
AssetMediaService,
|
AssetMediaService,
|
||||||
AssetService,
|
AssetService,
|
||||||
AssetServiceV1,
|
AssetServiceV1,
|
||||||
@ -54,9 +54,11 @@ export const services = [
|
|||||||
DuplicateService,
|
DuplicateService,
|
||||||
JobService,
|
JobService,
|
||||||
LibraryService,
|
LibraryService,
|
||||||
|
MapService,
|
||||||
MediaService,
|
MediaService,
|
||||||
MemoryService,
|
MemoryService,
|
||||||
MetadataService,
|
MetadataService,
|
||||||
|
MicroservicesService,
|
||||||
NotificationService,
|
NotificationService,
|
||||||
PartnerService,
|
PartnerService,
|
||||||
PersonService,
|
PersonService,
|
||||||
@ -73,7 +75,7 @@ export const services = [
|
|||||||
TagService,
|
TagService,
|
||||||
TimelineService,
|
TimelineService,
|
||||||
TrashService,
|
TrashService,
|
||||||
UserService,
|
|
||||||
UserAdminService,
|
UserAdminService,
|
||||||
|
UserService,
|
||||||
VersionService,
|
VersionService,
|
||||||
];
|
];
|
||||||
|
54
server/src/services/map.service.spec.ts
Normal file
54
server/src/services/map.service.spec.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { MapService } from 'src/services/map.service';
|
||||||
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
|
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
||||||
|
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||||
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
|
describe(MapService.name, () => {
|
||||||
|
let sut: MapService;
|
||||||
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
let partnerMock: Mocked<IPartnerRepository>;
|
||||||
|
let mapMock: Mocked<IMapRepository>;
|
||||||
|
let systemMetadataMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
albumMock = newAlbumRepositoryMock();
|
||||||
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
partnerMock = newPartnerRepositoryMock();
|
||||||
|
mapMock = newMapRepositoryMock();
|
||||||
|
systemMetadataMock = newSystemMetadataRepositoryMock();
|
||||||
|
|
||||||
|
sut = new MapService(albumMock, loggerMock, partnerMock, mapMock, systemMetadataMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMapMarkers', () => {
|
||||||
|
it('should get geo information of assets', async () => {
|
||||||
|
const asset = assetStub.withLocation;
|
||||||
|
const marker = {
|
||||||
|
id: asset.id,
|
||||||
|
lat: asset.exifInfo!.latitude!,
|
||||||
|
lon: asset.exifInfo!.longitude!,
|
||||||
|
city: asset.exifInfo!.city,
|
||||||
|
state: asset.exifInfo!.state,
|
||||||
|
country: asset.exifInfo!.country,
|
||||||
|
};
|
||||||
|
partnerMock.getAll.mockResolvedValue([]);
|
||||||
|
mapMock.getMapMarkers.mockResolvedValue([marker]);
|
||||||
|
|
||||||
|
const markers = await sut.getMapMarkers(authStub.user1, {});
|
||||||
|
|
||||||
|
expect(markers).toHaveLength(1);
|
||||||
|
expect(markers[0]).toEqual(marker);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
59
server/src/services/map.service.ts
Normal file
59
server/src/services/map.service.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto';
|
||||||
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
|
||||||
|
export class MapService {
|
||||||
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
|
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||||
|
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(MapService.name);
|
||||||
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||||
|
const userIds: string[] = [auth.user.id];
|
||||||
|
// TODO convert to SQL join
|
||||||
|
if (options.withPartners) {
|
||||||
|
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||||
|
const partnersIds = partners
|
||||||
|
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id)
|
||||||
|
.map((partner) => partner.sharedById);
|
||||||
|
userIds.push(...partnersIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO convert to SQL join
|
||||||
|
const albumIds: string[] = [];
|
||||||
|
if (options.withSharedAlbums) {
|
||||||
|
const [ownedAlbums, sharedAlbums] = await Promise.all([
|
||||||
|
this.albumRepository.getOwned(auth.user.id),
|
||||||
|
this.albumRepository.getShared(auth.user.id),
|
||||||
|
]);
|
||||||
|
albumIds.push(...ownedAlbums.map((album) => album.id), ...sharedAlbums.map((album) => album.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapRepository.getMapMarkers(userIds, albumIds, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMapStyle(theme: 'light' | 'dark') {
|
||||||
|
const { map } = await this.configCore.getConfig();
|
||||||
|
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
||||||
|
|
||||||
|
if (styleUrl) {
|
||||||
|
return this.mapRepository.fetchStyle(styleUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`));
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
|||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||||
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
@ -29,6 +30,7 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
|
|||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
|
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
||||||
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
||||||
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
|
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
|
||||||
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
||||||
@ -44,6 +46,7 @@ describe(MetadataService.name, () => {
|
|||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let cryptoRepository: Mocked<ICryptoRepository>;
|
let cryptoRepository: Mocked<ICryptoRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
|
let mapMock: Mocked<IMapRepository>;
|
||||||
let metadataMock: Mocked<IMetadataRepository>;
|
let metadataMock: Mocked<IMetadataRepository>;
|
||||||
let moveMock: Mocked<IMoveRepository>;
|
let moveMock: Mocked<IMoveRepository>;
|
||||||
let mediaMock: Mocked<IMediaRepository>;
|
let mediaMock: Mocked<IMediaRepository>;
|
||||||
@ -60,6 +63,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
cryptoRepository = newCryptoRepositoryMock();
|
cryptoRepository = newCryptoRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
|
mapMock = newMapRepositoryMock();
|
||||||
metadataMock = newMetadataRepositoryMock();
|
metadataMock = newMetadataRepositoryMock();
|
||||||
moveMock = newMoveRepositoryMock();
|
moveMock = newMoveRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
@ -78,6 +82,7 @@ describe(MetadataService.name, () => {
|
|||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
databaseMock,
|
databaseMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
|
mapMock,
|
||||||
mediaMock,
|
mediaMock,
|
||||||
metadataMock,
|
metadataMock,
|
||||||
moveMock,
|
moveMock,
|
||||||
@ -102,7 +107,7 @@ describe(MetadataService.name, () => {
|
|||||||
await sut.init();
|
await sut.init();
|
||||||
|
|
||||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||||
expect(metadataMock.init).toHaveBeenCalledTimes(1);
|
expect(mapMock.init).toHaveBeenCalledTimes(1);
|
||||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -112,7 +117,7 @@ describe(MetadataService.name, () => {
|
|||||||
await sut.init();
|
await sut.init();
|
||||||
|
|
||||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||||
expect(metadataMock.init).not.toHaveBeenCalled();
|
expect(mapMock.init).not.toHaveBeenCalled();
|
||||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -297,7 +302,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should apply reverse geocoding', async () => {
|
it('should apply reverse geocoding', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
||||||
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||||
metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
mapMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||||
metadataMock.readTags.mockResolvedValue({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
||||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
QueueName,
|
QueueName,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||||
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
@ -108,6 +109,7 @@ export class MetadataService {
|
|||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
|
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@ -144,7 +146,7 @@ export class MetadataService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
||||||
await this.databaseRepository.withLock(DatabaseLock.GeodataImport, () => this.repository.init());
|
await this.databaseRepository.withLock(DatabaseLock.GeodataImport, () => this.mapRepository.init());
|
||||||
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
|
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
|
||||||
|
|
||||||
this.logger.log(`Initialized local reverse geocoder`);
|
this.logger.log(`Initialized local reverse geocoder`);
|
||||||
@ -337,7 +339,7 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude });
|
const reverseGeocode = await this.mapRepository.reverseGeocode({ latitude, longitude });
|
||||||
if (!reverseGeocode) {
|
if (!reverseGeocode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ export class SystemConfigService {
|
|||||||
private core: SystemConfigCore;
|
private core: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) repository: ISystemMetadataRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||||
@ -109,17 +109,6 @@ export class SystemConfigService {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMapStyle(theme: 'light' | 'dark') {
|
|
||||||
const { map } = await this.getConfig();
|
|
||||||
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
|
||||||
|
|
||||||
if (styleUrl) {
|
|
||||||
return this.repository.fetchStyle(styleUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(await this.repository.readFile(`./resources/style-${theme}.json`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCustomCss(): Promise<string> {
|
async getCustomCss(): Promise<string> {
|
||||||
const { theme } = await this.core.getConfig();
|
const { theme } = await this.core.getConfig();
|
||||||
return theme.customCss;
|
return theme.customCss;
|
||||||
|
@ -31,7 +31,6 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
|
|||||||
update: vitest.fn(),
|
update: vitest.fn(),
|
||||||
remove: vitest.fn(),
|
remove: vitest.fn(),
|
||||||
findLivePhotoMatch: vitest.fn(),
|
findLivePhotoMatch: vitest.fn(),
|
||||||
getMapMarkers: vitest.fn(),
|
|
||||||
getStatistics: vitest.fn(),
|
getStatistics: vitest.fn(),
|
||||||
getTimeBucket: vitest.fn(),
|
getTimeBucket: vitest.fn(),
|
||||||
getTimeBuckets: vitest.fn(),
|
getTimeBuckets: vitest.fn(),
|
||||||
|
11
server/test/repositories/map.repository.mock.ts
Normal file
11
server/test/repositories/map.repository.mock.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
|
export const newMapRepositoryMock = (): Mocked<IMapRepository> => {
|
||||||
|
return {
|
||||||
|
init: vitest.fn(),
|
||||||
|
reverseGeocode: vitest.fn(),
|
||||||
|
getMapMarkers: vitest.fn(),
|
||||||
|
fetchStyle: vitest.fn(),
|
||||||
|
};
|
||||||
|
};
|
@ -3,9 +3,7 @@ import { Mocked, vitest } from 'vitest';
|
|||||||
|
|
||||||
export const newMetadataRepositoryMock = (): Mocked<IMetadataRepository> => {
|
export const newMetadataRepositoryMock = (): Mocked<IMetadataRepository> => {
|
||||||
return {
|
return {
|
||||||
init: vitest.fn(),
|
|
||||||
teardown: vitest.fn(),
|
teardown: vitest.fn(),
|
||||||
reverseGeocode: vitest.fn(),
|
|
||||||
readTags: vitest.fn(),
|
readTags: vitest.fn(),
|
||||||
writeTags: vitest.fn(),
|
writeTags: vitest.fn(),
|
||||||
extractBinaryTag: vitest.fn(),
|
extractBinaryTag: vitest.fn(),
|
||||||
|
@ -11,6 +11,5 @@ export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMet
|
|||||||
get: vitest.fn() as any,
|
get: vitest.fn() as any,
|
||||||
set: vitest.fn(),
|
set: vitest.fn(),
|
||||||
readFile: vitest.fn(),
|
readFile: vitest.fn(),
|
||||||
fetchStyle: vitest.fn(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user