From d954591ca85701d6febc6a8bc7d45249be4da32b Mon Sep 17 00:00:00 2001 From: Ben Beckford Date: Tue, 23 Jun 2026 16:02:04 -0700 Subject: [PATCH] feat: exif metadata workflow filter --- packages/plugin-core/manifest.json | 70 ++++++++++++++++++++ packages/plugin-core/src/index.d.ts | 1 + packages/plugin-core/src/index.ts | 99 +++++++++++++++++++---------- 3 files changed, 138 insertions(+), 32 deletions(-) diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index 0f1b88827c..ae20581618 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -203,6 +203,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.d.ts b/packages/plugin-core/src/index.d.ts index 107bcc7aa0..2f9bc1973a 100644 --- a/packages/plugin-core/src/index.d.ts +++ b/packages/plugin-core/src/index.d.ts @@ -15,6 +15,7 @@ declare module 'main' { export function assetMissingTimeZoneFilter(): I32; export function assetLocationFilter(): I32; export function assetTypeFilter(): I32; + export function assetExifFilter(): I32; // updates export function assetFavorite(): I32; diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index fa956b8cec..b95d38225c 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -1,45 +1,46 @@ import { wrapper } from '@immich/plugin-sdk'; import { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk'; -type AssetFileFilterConfig = { +type MatchValueConfig = { pattern: string; matchType?: 'contains' | 'exact' | 'regex' | 'startsWith'; caseSensitive?: boolean; }; -export const assetFileFilter = () => { - return wrapper(({ data, config }) => { - const { pattern, matchType = 'contains', caseSensitive = false } = config; - const { asset } = data; +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(); - const fileName = 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 {}; - } + 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 {}; + } + } +}; + +export const assetFileFilter = () => { + return wrapper(({ data, config }) => + matchValueResult(data.asset.originalFileName || '', config), + ); }; export const assetMissingTimeZoneFilter = () => { @@ -95,6 +96,40 @@ export const assetLocationFilter = () => { }); }; +type AssetExifFilterConfig = MatchValueConfig & { + property: + | 'make' + | 'model' + | 'exifImageWidth' + | 'exifImageHeight' + | 'fileSizeInByte' + | 'orientation' + | 'lensModel' + | 'fNumber' + | 'focalLength' + | 'iso' + | 'description' + | 'fps' + | 'exposureTime' + | 'livePhotoCID' + | 'timeZone' + | 'projectionType' + | 'profileDescription' + | 'colorspace' + | 'bitsPerSample' + | 'rating'; +}; + +export const assetExifFilter = () => { + return wrapper(({ config, data }) => { + if (!data.asset.exifInfo) { + return { workflow: { continue: false } }; + } + + return matchValueResult(String(data.asset.exifInfo[config.property] || ''), config); + }); +}; + export const assetTypeFilter = () => { return wrapper(({ config, data }) => { return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };