mirror of
https://github.com/immich-app/immich.git
synced 2026-06-22 14:52:17 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b75d9b74b9 | |||
| 72eaba6ee2 | |||
| 57d77ad5da | |||
| 18ec30c3b8 |
@@ -2364,6 +2364,8 @@
|
||||
"trash_page_title": "Trash ({count})",
|
||||
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||
"trigger": "Trigger",
|
||||
"trigger_album_asset_added": "Asset Added to Album",
|
||||
"trigger_album_asset_added_description": "Triggered when an asset is added to an album",
|
||||
"trigger_asset_uploaded": "Asset Upload",
|
||||
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
||||
"trigger_description": "An event that kicks off the workflow",
|
||||
|
||||
+3
@@ -26,12 +26,14 @@ class WorkflowTrigger {
|
||||
static const assetCreate = WorkflowTrigger._(r'AssetCreate');
|
||||
static const assetMetadataExtraction = WorkflowTrigger._(r'AssetMetadataExtraction');
|
||||
static const personRecognized = WorkflowTrigger._(r'PersonRecognized');
|
||||
static const albumAssetAdded = WorkflowTrigger._(r'AlbumAssetAdded');
|
||||
|
||||
/// List of all possible values in this [enum][WorkflowTrigger].
|
||||
static const values = <WorkflowTrigger>[
|
||||
assetCreate,
|
||||
assetMetadataExtraction,
|
||||
personRecognized,
|
||||
albumAssetAdded,
|
||||
];
|
||||
|
||||
static WorkflowTrigger? fromJson(dynamic value) => WorkflowTriggerTypeTransformer().decode(value);
|
||||
@@ -73,6 +75,7 @@ class WorkflowTriggerTypeTransformer {
|
||||
case r'AssetCreate': return WorkflowTrigger.assetCreate;
|
||||
case r'AssetMetadataExtraction': return WorkflowTrigger.assetMetadataExtraction;
|
||||
case r'PersonRecognized': return WorkflowTrigger.personRecognized;
|
||||
case r'AlbumAssetAdded': return WorkflowTrigger.albumAssetAdded;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
@@ -26811,7 +26811,8 @@
|
||||
"enum": [
|
||||
"AssetCreate",
|
||||
"AssetMetadataExtraction",
|
||||
"PersonRecognized"
|
||||
"PersonRecognized",
|
||||
"AlbumAssetAdded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum WorkflowTrigger {
|
||||
AssetCreate = 'AssetCreate',
|
||||
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
||||
PersonRecognized = 'PersonRecognized',
|
||||
AlbumAssetAdded = 'AlbumAssetAdded',
|
||||
}
|
||||
|
||||
export type WorkflowEventPayload<
|
||||
|
||||
@@ -7184,7 +7184,8 @@ export enum WorkflowType {
|
||||
export enum WorkflowTrigger {
|
||||
AssetCreate = "AssetCreate",
|
||||
AssetMetadataExtraction = "AssetMetadataExtraction",
|
||||
PersonRecognized = "PersonRecognized"
|
||||
PersonRecognized = "PersonRecognized",
|
||||
AlbumAssetAdded = "AlbumAssetAdded"
|
||||
}
|
||||
export enum QueueJobStatus {
|
||||
Active = "active",
|
||||
|
||||
@@ -40,6 +40,7 @@ type EventMap = {
|
||||
// album events
|
||||
AlbumUpdate: [{ id: string; recipientId: string }];
|
||||
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
||||
AlbumAssetAdd: [{ albumId: string; assetId: string; userId: string }];
|
||||
|
||||
// asset events
|
||||
AssetCreate: [{ asset: Asset; file: UploadFile }];
|
||||
|
||||
@@ -742,6 +742,9 @@ describe(AlbumService.name, () => {
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]);
|
||||
for (const assetId of [asset1.id, asset2.id, asset3.id]) {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumAssetAdd', { albumId: album.id, assetId, userId: owner.id });
|
||||
}
|
||||
});
|
||||
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
@@ -1055,6 +1058,16 @@ describe(AlbumService.name, () => {
|
||||
id: album2.id,
|
||||
recipientId: owner2.id,
|
||||
});
|
||||
for (const { albumId, assetId } of [
|
||||
{ albumId: album1.id, assetId: asset1.id },
|
||||
{ albumId: album1.id, assetId: asset2.id },
|
||||
{ albumId: album1.id, assetId: asset3.id },
|
||||
{ albumId: album2.id, assetId: asset1.id },
|
||||
{ albumId: album2.id, assetId: asset2.id },
|
||||
{ albumId: album2.id, assetId: asset3.id },
|
||||
]) {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumAssetAdd', { albumId, assetId, userId: user.id });
|
||||
}
|
||||
});
|
||||
|
||||
it('should not allow a shared user with viewer access to add assets', async () => {
|
||||
|
||||
@@ -201,6 +201,12 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.eventRepository.emit('AlbumAssetAdd', { albumId: id, assetId, userId: auth.user.id });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -261,6 +267,10 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
for (const { albumId, assetId } of albumAssetValues) {
|
||||
await this.eventRepository.emit('AlbumAssetAdd', { albumId, assetId, userId: auth.user.id });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,6 +274,11 @@ export class WorkflowExecutionService extends BaseService {
|
||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AlbumAssetAdd' })
|
||||
onAlbumAssetAdd({ userId, assetId }: ArgOf<'AlbumAssetAdd'>) {
|
||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AlbumAssetAdded });
|
||||
}
|
||||
|
||||
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
||||
const items = await this.workflowRepository.search({ userId, trigger });
|
||||
await this.jobRepository.queueAll(
|
||||
|
||||
@@ -28,6 +28,16 @@ const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected:
|
||||
types: [WorkflowType.AssetV1, WorkflowType.AssetPersonV1],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
trigger: WorkflowTrigger.AlbumAssetAdded,
|
||||
types: [WorkflowType.AssetV1],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
trigger: WorkflowTrigger.AlbumAssetAdded,
|
||||
types: [WorkflowType.AssetPersonV1],
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe(isMethodCompatible.name, () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
|
||||
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
||||
[WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
|
||||
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
|
||||
[WorkflowTrigger.AlbumAssetAdded]: [WorkflowType.AssetV1],
|
||||
};
|
||||
|
||||
export const getWorkflowTriggers = () =>
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,9 @@ export const getTriggerName = ($t: MessageFormatter, type: WorkflowTrigger) => {
|
||||
case WorkflowTrigger.PersonRecognized: {
|
||||
return $t('trigger_person_recognized');
|
||||
}
|
||||
case WorkflowTrigger.AlbumAssetAdded: {
|
||||
return $t('trigger_album_asset_added');
|
||||
}
|
||||
default: {
|
||||
return type;
|
||||
}
|
||||
@@ -23,6 +26,9 @@ export const getTriggerDescription = ($t: MessageFormatter, type: WorkflowTrigge
|
||||
case WorkflowTrigger.PersonRecognized: {
|
||||
return $t('trigger_person_recognized_description');
|
||||
}
|
||||
case WorkflowTrigger.AlbumAssetAdded: {
|
||||
return $t('trigger_album_asset_added_description');
|
||||
}
|
||||
default: {
|
||||
return type;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user