mirror of
https://github.com/immich-app/immich.git
synced 2026-06-27 22:35:57 +00:00
feat: geolocation workflow filter (#28961)
* feat: geolocation workflow filter * refactor: geolocation workflow filter * feat: location filter workflow example
This commit is contained in:
@@ -55,6 +55,26 @@
|
||||
}
|
||||
],
|
||||
"uiHints": ["SmartAlbum"]
|
||||
},
|
||||
{
|
||||
"name": "location-smart-album",
|
||||
"title": "Location-based album",
|
||||
"description": "Automatically add assets taken in a specific location to an album",
|
||||
"trigger": "AssetMetadataExtraction",
|
||||
"steps": [
|
||||
{
|
||||
"method": "immich-plugin-core#assetLocationFilter",
|
||||
"config": { "region": { "city": "Vancouver", "state": "British Columbia", "country": "Canada" } }
|
||||
},
|
||||
{
|
||||
"method": "immich-plugin-core#assetAddToAlbums",
|
||||
"config": {
|
||||
"albumName": "Vancouver photos & videos",
|
||||
"albumIds": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"uiHints": ["SmartAlbum"]
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
@@ -107,6 +127,62 @@
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetLocationFilter",
|
||||
"title": "Filter assets by geolocation",
|
||||
"description": "Filter assets by where they were taken",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"region": {
|
||||
"type": "object",
|
||||
"title": "Region",
|
||||
"description": "Filter by region name",
|
||||
"properties": {
|
||||
"country": {
|
||||
"type": "string",
|
||||
"title": "Country",
|
||||
"description": "Exact name of the country the asset must be taken in"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"title": "State/province",
|
||||
"description": "Exact name of the state/province the asset must be taken in"
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"title": "City",
|
||||
"description": "Exact name of the city the asset must be taken in"
|
||||
}
|
||||
}
|
||||
},
|
||||
"coordinate": {
|
||||
"type": "object",
|
||||
"title": "Coordinate",
|
||||
"description": "Filter by distance to a coordinate",
|
||||
"properties": {
|
||||
"latitude": {
|
||||
"type": "string",
|
||||
"title": "Latitude",
|
||||
"description": "GPS latitude of a coordinate which the asset must be close to"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "string",
|
||||
"title": "Longitude",
|
||||
"description": "GPS longitude of a coordinate which the asset must be close to"
|
||||
},
|
||||
"radius": {
|
||||
"type": "number",
|
||||
"title": "Maximum distance",
|
||||
"description": "How close in kilometres the asset must be to the given point"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
{
|
||||
"name": "filterFileType",
|
||||
"title": "Filter by file type",
|
||||
|
||||
1
packages/plugin-core/src/index.d.ts
vendored
1
packages/plugin-core/src/index.d.ts
vendored
@@ -13,6 +13,7 @@ declare module 'main' {
|
||||
// filters
|
||||
export function assetFileFilter(): I32;
|
||||
export function assetMissingTimeZoneFilter(): I32;
|
||||
export function assetLocationFilter(): I32;
|
||||
|
||||
// updates
|
||||
export function assetFavorite(): I32;
|
||||
|
||||
@@ -50,6 +50,51 @@ export const assetMissingTimeZoneFilter = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const assetLocationFilter = () => {
|
||||
return wrapper<
|
||||
WorkflowType.AssetV1,
|
||||
{
|
||||
region?: { country?: string; state?: string; city?: string };
|
||||
coordinate?: { latitude?: string; longitude?: string; radius?: number };
|
||||
}
|
||||
>(({ config, data }) => {
|
||||
if (
|
||||
(config.region?.country && config.region.country !== data.asset.exifInfo?.country) ||
|
||||
(config.region?.state && config.region.state !== data.asset.exifInfo?.state) ||
|
||||
(config.region?.city && config.region.city !== data.asset.exifInfo?.city)
|
||||
) {
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
|
||||
const configLat = Number.parseFloat(config.coordinate?.latitude ?? '');
|
||||
const configLon = Number.parseFloat(config.coordinate?.longitude ?? '');
|
||||
|
||||
if (Number.isNaN(configLat) || Number.isNaN(configLat)) {
|
||||
return { workflow: { continue: true } };
|
||||
}
|
||||
|
||||
const assetLat = data.asset.exifInfo?.latitude;
|
||||
const assetLon = data.asset.exifInfo?.longitude;
|
||||
|
||||
if (assetLat === undefined || assetLat === null || assetLon === undefined || assetLon === null) {
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
|
||||
const earthDiameter = 12742;
|
||||
const deg = Math.PI / 180;
|
||||
const delta = Math.asin(
|
||||
Math.sqrt(
|
||||
Math.pow(Math.sin((assetLat * deg - configLat * deg) / 2), 2) +
|
||||
Math.cos(assetLat * deg) *
|
||||
Math.cos(configLat * deg) *
|
||||
Math.pow(Math.sin((assetLon * deg - configLon * deg) / 2), 2),
|
||||
),
|
||||
);
|
||||
|
||||
return { workflow: { continue: earthDiameter * delta <= (config.coordinate?.radius ?? 0) } };
|
||||
});
|
||||
};
|
||||
|
||||
export const assetFavorite = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
const target = config.inverse ? false : true;
|
||||
|
||||
@@ -332,4 +332,75 @@ describe('core plugin', () => {
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assetLocationFilter', () => {
|
||||
it('should favorite an asset within a given radius', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, latitude: 49.273_353_221_145_36, longitude: -123.103_871_440_787_64 });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetLocationFilter',
|
||||
config: { coordinate: { latitude: 49.288_821_679_949_29, longitude: -123.111_153_098_813_7, radius: 2 } },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
});
|
||||
|
||||
it('should not favorite asset outside a given radius', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, latitude: 49.261_266_052_570_35, longitude: -123.248_959_390_781_96 });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetLocationFilter',
|
||||
config: { coordinate: { latitude: 49.288_821_679_949_29, longitude: -123.111_153_098_813_7, radius: 10 } },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
|
||||
it('should favorite asset by location name', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, city: 'Vancouver' });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||
steps: [
|
||||
{
|
||||
method: 'immich-plugin-core#assetLocationFilter',
|
||||
config: { region: { city: 'Vancouver' } },
|
||||
},
|
||||
{
|
||||
method: 'immich-plugin-core#assetFavorite',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user