feat(plugin-core): add filterByAlbum workflow step

Adds a filterByAlbum core plugin method so workflows (e.g. the
AlbumAssetAdded trigger) can be scoped to specific albums. The step
checks the asset's album membership via the searchAlbums host function
and halts the workflow when the asset is not in any of the configured
albums (or, with inverse, when it is).
This commit is contained in:
Alex Tran
2026-06-04 03:56:41 +00:00
parent 72eaba6ee2
commit b75d9b74b9
4 changed files with 103 additions and 0 deletions
+27
View File
@@ -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",
+1
View File
@@ -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;
+14
View File
@@ -50,6 +50,20 @@ export const assetMissingTimeZoneFilter = () => {
});
};
export const filterByAlbum = () => {
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; inverse?: boolean }>(({ 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<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
const target = config.inverse ? false : true;
@@ -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 });
});
});
});