From 8bbc25270331dd789e0cd7749a80b3b4e7f8fc07 Mon Sep 17 00:00:00 2001 From: Ben Beckford Date: Wed, 22 Jul 2026 02:20:57 -0700 Subject: [PATCH] feat: exif metadata workflow filter (#29295) --- packages/plugin-core/manifest.json | 70 ++++++++++++++++++++++++++ packages/plugin-core/src/index.ts | 80 ++++++++++++++++++------------ 2 files changed, 117 insertions(+), 33 deletions(-) diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index d55ad43e70..a72d60c04d 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -308,6 +308,76 @@ }, "uiHints": ["Filter"] }, + { + "name": "assetExifFilter", + "title": "Filter by EXIF metadata", + "description": "Filter assets by their EXIF properties", + "types": ["AssetV1"], + "schema": { + "type": "object", + "properties": { + "property": { + "title": "Property", + "description": "EXIF property to match", + "type": "string", + "enum": [ + "make", + "model", + "exifImageWidth", + "exifImageHeight", + "fileSizeInByte", + "orientation", + "lensModel", + "fNumber", + "focalLength", + "iso", + "description", + "fps", + "exposureTime", + "livePhotoCID", + "timeZone", + "projectionType", + "profileDescription", + "colorspace", + "bitsPerSample", + "rating" + ], + "uiHint": { + "order": 1 + } + }, + "pattern": { + "type": "string", + "title": "Pattern", + "description": "Text or regex pattern to match against property value", + "uiHint": { + "order": 2 + } + }, + "matchType": { + "type": "string", + "title": "Match type", + "enum": ["contains", "startsWith", "exact", "regex"], + "default": "contains", + "description": "Type of pattern matching to perform", + "uiHint": { + "order": 3 + } + }, + "caseSensitive": { + "type": "boolean", + "default": false, + "title": "Case sensitive", + "description": "Whether matching should be case-sensitive", + "uiHint": { + "order": 4 + } + } + }, + "required": ["property", "pattern"] + }, + "uiHints": ["Filter"] + }, { "name": "assetArchive", "title": "Archive asset", diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index d412ad7708..e3f180f98b 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -2,6 +2,42 @@ import { wrapper } from '@immich/plugin-sdk'; import { AssetVisibility } from '@immich/sdk'; import type { Manifest } from '../dist/index.d.ts'; +type MatchValueConfig = { + pattern: string; + matchType?: 'contains' | 'exact' | 'regex' | 'startsWith'; + caseSensitive?: boolean; +}; + +const matchValueResult = (value: string, config: MatchValueConfig) => { + const { pattern, matchType = 'contains', caseSensitive = false } = config; + const searchName = caseSensitive ? value : value.toLowerCase(); + const searchPattern = caseSensitive ? pattern : pattern.toLowerCase(); + + switch (matchType) { + case 'contains': { + return { workflow: { continue: searchName.includes(searchPattern) } }; + } + + case 'exact': { + return { workflow: { continue: searchName === searchPattern } }; + } + + case 'startsWith': { + return { workflow: { continue: searchName.startsWith(searchPattern) } }; + } + + case 'regex': { + const flags = caseSensitive ? '' : 'i'; + const regex = new RegExp(searchPattern, flags); + return { workflow: { continue: regex.test(value) } }; + } + + default: { + return {}; + } + } +}; + const methods = wrapper({ assetAddToAlbums: ({ config, data, functions }) => { const assetId = data.asset.id; @@ -53,39 +89,7 @@ const methods = wrapper({ } }, - assetFileFilter: ({ data, config }) => { - const { pattern, matchType = 'contains', caseSensitive = false, usePath = false } = config; - - const { asset } = data; - - const fileName = usePath ? asset.originalPath : asset.originalFileName; - const searchName = caseSensitive ? fileName : fileName.toLowerCase(); - const searchPattern = caseSensitive ? pattern : pattern.toLowerCase(); - - switch (matchType) { - case 'contains': { - return { workflow: { continue: searchName.includes(searchPattern) } }; - } - - case 'exact': { - return { workflow: { continue: searchName === searchPattern } }; - } - - case 'startsWith': { - return { workflow: { continue: searchName.startsWith(searchPattern) } }; - } - - case 'regex': { - const flags = caseSensitive ? '' : 'i'; - const regex = new RegExp(searchPattern, flags); - return { workflow: { continue: regex.test(fileName) } }; - } - - default: { - return {}; - } - } - }, + assetFileFilter: ({ data, config }) => matchValueResult(data.asset.originalFileName || '', config), assetLocationFilter: ({ config, data }) => { if ( @@ -124,6 +128,14 @@ const methods = wrapper({ return { workflow: { continue: earthDiameter * delta <= (config.coordinate?.radius ?? 0) } }; }, + assetExifFilter: ({ config, data }) => { + if (!data.asset.exifInfo || data.asset.exifInfo[config.property] === null) { + return { workflow: { continue: false } }; + } + + return matchValueResult(String(data.asset.exifInfo[config.property]), config); + }, + assetDateFilter: ({ config, data }) => { const assetDate = new Date(data.asset.localDateTime); let startDate = new Date(config.startDate.year, config.startDate.month - 1, config.startDate.day); @@ -200,6 +212,7 @@ const { assetFavorite, assetFileFilter, assetLocationFilter, + assetExifFilter, assetDateFilter, assetLock, assetMissingTimeZoneFilter, @@ -217,6 +230,7 @@ export { assetFavorite, assetFileFilter, assetLocationFilter, + assetExifFilter, assetDateFilter, assetLock, assetMissingTimeZoneFilter,