diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json index 48b4bee2c8..6c1d1e97c1 100644 --- a/packages/plugin-core/manifest.json +++ b/packages/plugin-core/manifest.json @@ -152,6 +152,33 @@ }, "uiHints": ["Filter"] }, + { + "name": "filterByAlbum", + "title": "Filter by album", + "description": "Continue only when the asset belongs to one of the selected albums", + "types": ["AssetV1"], + "hostFunctions": true, + "schema": { + "type": "object", + "properties": { + "albumIds": { + "type": "string", + "array": true, + "title": "Album IDs", + "description": "Albums to match against", + "uiHint": "AlbumId" + }, + "inverse": { + "type": "boolean", + "title": "Inverse", + "description": "Continue only when the asset is NOT in the selected albums", + "default": false + } + }, + "required": ["albumIds"] + }, + "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 170fa13102..918af49b7f 100644 --- a/packages/plugin-core/src/index.d.ts +++ b/packages/plugin-core/src/index.d.ts @@ -13,6 +13,7 @@ declare module 'main' { // filters export function assetFileFilter(): I32; export function assetMissingTimeZoneFilter(): I32; + export function filterByAlbum(): I32; // updates export function assetFavorite(): I32; diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index bcb05cfa19..2dc6d5ba03 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -50,6 +50,20 @@ export const assetMissingTimeZoneFilter = () => { }); }; +export const filterByAlbum = () => { + return wrapper(({ config, data, functions }) => { + const { albumIds = [], inverse = false } = config; + if (albumIds.length === 0) { + return {}; + } + + const albums = functions.searchAlbums({ assetId: data.asset.id }); + const isMember = albums.some((album) => albumIds.includes(album.id)); + + return { workflow: { continue: isMember !== inverse } }; + }); +}; + export const assetFavorite = () => { return wrapper(({ config, data }) => { const target = config.inverse ? false : true; diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 2bb9de6af1..8740bc85c5 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -332,4 +332,65 @@ describe('core plugin', () => { await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id); }); }); + + describe('filterByAlbum', () => { + it('should continue when the asset is in a selected album', async () => { + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { album } = await ctx.newAlbum({ ownerId: user.id }, [asset.id]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AlbumAssetAdded, + steps: [ + { method: 'immich-plugin-core#filterByAlbum', config: { albumIds: [album.id] } }, + { 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 stop when the asset is not in a selected album', async () => { + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const [{ album }, { album: other }] = await Promise.all([ + ctx.newAlbum({ ownerId: user.id }, [asset.id]), + ctx.newAlbum({ ownerId: user.id }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AlbumAssetAdded, + steps: [ + { method: 'immich-plugin-core#filterByAlbum', config: { albumIds: [other.id] } }, + { 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 continue when no albums are configured', async () => { + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AlbumAssetAdded, + steps: [ + { method: 'immich-plugin-core#filterByAlbum', config: { albumIds: [] } }, + { 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 }); + }); + }); });