feat: workflow filter by missing EXIF property (#32713)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> (cherry picked from commit a1a80cb1c416d3a480963a78d7b9df329d867bf6)
This commit is contained in:
@@ -407,7 +407,7 @@
|
|||||||
"matchType": {
|
"matchType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Match type",
|
"title": "Match type",
|
||||||
"enum": ["contains", "startsWith", "exact", "regex"],
|
"enum": ["contains", "startsWith", "exact", "regex", "present", "missing"],
|
||||||
"default": "contains",
|
"default": "contains",
|
||||||
"description": "Type of pattern matching to perform",
|
"description": "Type of pattern matching to perform",
|
||||||
"uiHint": {
|
"uiHint": {
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["property", "pattern"]
|
"required": ["property"]
|
||||||
},
|
},
|
||||||
"uiHints": ["Filter"]
|
"uiHints": ["Filter"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ import { AssetVisibility } from '@immich/sdk';
|
|||||||
import type { Manifest } from '../dist/index.d.ts';
|
import type { Manifest } from '../dist/index.d.ts';
|
||||||
|
|
||||||
type MatchValueConfig = {
|
type MatchValueConfig = {
|
||||||
pattern: string;
|
pattern?: string;
|
||||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith' | 'missing' | 'present';
|
||||||
caseSensitive?: boolean;
|
caseSensitive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchValueResult = (value: string, config: MatchValueConfig) => {
|
const matchValueResult = (value: string | null, config: MatchValueConfig) => {
|
||||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
const { pattern = '', matchType = 'contains', caseSensitive = false } = config;
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return { workflow: { continue: matchType === 'missing' } };
|
||||||
|
}
|
||||||
|
|
||||||
const searchName = caseSensitive ? value : value.toLowerCase();
|
const searchName = caseSensitive ? value : value.toLowerCase();
|
||||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||||
|
|
||||||
@@ -32,6 +37,15 @@ const matchValueResult = (value: string, config: MatchValueConfig) => {
|
|||||||
return { workflow: { continue: regex.test(value) } };
|
return { workflow: { continue: regex.test(value) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'present': {
|
||||||
|
return { workflow: { continue: true } };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'missing': {
|
||||||
|
// value is not null at this point
|
||||||
|
return { workflow: { continue: false } };
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -135,11 +149,10 @@ const methods = wrapper<Manifest>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
assetExifFilter: ({ config, data }) => {
|
assetExifFilter: ({ config, data }) => {
|
||||||
if (!data.asset.exifInfo || data.asset.exifInfo[config.property] === null) {
|
let value = data.asset.exifInfo?.[config.property];
|
||||||
return { workflow: { continue: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchValueResult(String(data.asset.exifInfo[config.property]), config);
|
// Empty EXIF value is treated as missing to mirror behavior of the web UI
|
||||||
|
return matchValueResult(value === null || value === undefined || value === '' ? null : String(value), config);
|
||||||
},
|
},
|
||||||
|
|
||||||
assetDateFilter: ({ config, data }) => {
|
assetDateFilter: ({ config, data }) => {
|
||||||
|
|||||||
@@ -530,6 +530,112 @@ describe('core plugin', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('assetExifFilter', () => {
|
||||||
|
describe('missing', () => {
|
||||||
|
it('should favorite asset if it has no orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: null });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'missing' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 if it has orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: '2' });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'missing' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('present', () => {
|
||||||
|
it('should favorite asset if it has orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: '2' });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'present' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 if it has no orientation', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
|
||||||
|
await ctx.newExif({ assetId: asset.id, orientation: null });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetMetadataExtraction,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#assetExifFilter',
|
||||||
|
config: { property: 'orientation', matchType: 'present' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('assetDateFilter', () => {
|
describe('assetDateFilter', () => {
|
||||||
it('should favorite assets created during the first 7 days of a specific year and month', async () => {
|
it('should favorite assets created during the first 7 days of a specific year and month', async () => {
|
||||||
const { user } = await ctx.newUser();
|
const { user } = await ctx.newUser();
|
||||||
|
|||||||
Reference in New Issue
Block a user