mirror of
https://github.com/immich-app/immich.git
synced 2026-07-07 04:57:05 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b75d9b74b9 | |||
| 72eaba6ee2 | |||
| 57d77ad5da | |||
| 18ec30c3b8 |
@@ -2364,6 +2364,8 @@
|
|||||||
"trash_page_title": "Trash ({count})",
|
"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}}.",
|
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||||
"trigger": "Trigger",
|
"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": "Asset Upload",
|
||||||
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
||||||
"trigger_description": "An event that kicks off the workflow",
|
"trigger_description": "An event that kicks off the workflow",
|
||||||
|
|||||||
+3
@@ -26,12 +26,14 @@ class WorkflowTrigger {
|
|||||||
static const assetCreate = WorkflowTrigger._(r'AssetCreate');
|
static const assetCreate = WorkflowTrigger._(r'AssetCreate');
|
||||||
static const assetMetadataExtraction = WorkflowTrigger._(r'AssetMetadataExtraction');
|
static const assetMetadataExtraction = WorkflowTrigger._(r'AssetMetadataExtraction');
|
||||||
static const personRecognized = WorkflowTrigger._(r'PersonRecognized');
|
static const personRecognized = WorkflowTrigger._(r'PersonRecognized');
|
||||||
|
static const albumAssetAdded = WorkflowTrigger._(r'AlbumAssetAdded');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][WorkflowTrigger].
|
/// List of all possible values in this [enum][WorkflowTrigger].
|
||||||
static const values = <WorkflowTrigger>[
|
static const values = <WorkflowTrigger>[
|
||||||
assetCreate,
|
assetCreate,
|
||||||
assetMetadataExtraction,
|
assetMetadataExtraction,
|
||||||
personRecognized,
|
personRecognized,
|
||||||
|
albumAssetAdded,
|
||||||
];
|
];
|
||||||
|
|
||||||
static WorkflowTrigger? fromJson(dynamic value) => WorkflowTriggerTypeTransformer().decode(value);
|
static WorkflowTrigger? fromJson(dynamic value) => WorkflowTriggerTypeTransformer().decode(value);
|
||||||
@@ -73,6 +75,7 @@ class WorkflowTriggerTypeTransformer {
|
|||||||
case r'AssetCreate': return WorkflowTrigger.assetCreate;
|
case r'AssetCreate': return WorkflowTrigger.assetCreate;
|
||||||
case r'AssetMetadataExtraction': return WorkflowTrigger.assetMetadataExtraction;
|
case r'AssetMetadataExtraction': return WorkflowTrigger.assetMetadataExtraction;
|
||||||
case r'PersonRecognized': return WorkflowTrigger.personRecognized;
|
case r'PersonRecognized': return WorkflowTrigger.personRecognized;
|
||||||
|
case r'AlbumAssetAdded': return WorkflowTrigger.albumAssetAdded;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
@@ -26811,7 +26811,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"AssetCreate",
|
"AssetCreate",
|
||||||
"AssetMetadataExtraction",
|
"AssetMetadataExtraction",
|
||||||
"PersonRecognized"
|
"PersonRecognized",
|
||||||
|
"AlbumAssetAdded"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -152,6 +152,33 @@
|
|||||||
},
|
},
|
||||||
"uiHints": ["Filter"]
|
"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",
|
"name": "assetArchive",
|
||||||
"title": "Archive asset",
|
"title": "Archive asset",
|
||||||
|
|||||||
Vendored
+1
@@ -13,6 +13,7 @@ declare module 'main' {
|
|||||||
// filters
|
// filters
|
||||||
export function assetFileFilter(): I32;
|
export function assetFileFilter(): I32;
|
||||||
export function assetMissingTimeZoneFilter(): I32;
|
export function assetMissingTimeZoneFilter(): I32;
|
||||||
|
export function filterByAlbum(): I32;
|
||||||
|
|
||||||
// updates
|
// updates
|
||||||
export function assetFavorite(): I32;
|
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 = () => {
|
export const assetFavorite = () => {
|
||||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||||
const target = config.inverse ? false : true;
|
const target = config.inverse ? false : true;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export enum WorkflowTrigger {
|
|||||||
AssetCreate = 'AssetCreate',
|
AssetCreate = 'AssetCreate',
|
||||||
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
||||||
PersonRecognized = 'PersonRecognized',
|
PersonRecognized = 'PersonRecognized',
|
||||||
|
AlbumAssetAdded = 'AlbumAssetAdded',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowEventPayload<
|
export type WorkflowEventPayload<
|
||||||
|
|||||||
@@ -7184,7 +7184,8 @@ export enum WorkflowType {
|
|||||||
export enum WorkflowTrigger {
|
export enum WorkflowTrigger {
|
||||||
AssetCreate = "AssetCreate",
|
AssetCreate = "AssetCreate",
|
||||||
AssetMetadataExtraction = "AssetMetadataExtraction",
|
AssetMetadataExtraction = "AssetMetadataExtraction",
|
||||||
PersonRecognized = "PersonRecognized"
|
PersonRecognized = "PersonRecognized",
|
||||||
|
AlbumAssetAdded = "AlbumAssetAdded"
|
||||||
}
|
}
|
||||||
export enum QueueJobStatus {
|
export enum QueueJobStatus {
|
||||||
Active = "active",
|
Active = "active",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type EventMap = {
|
|||||||
// album events
|
// album events
|
||||||
AlbumUpdate: [{ id: string; recipientId: string }];
|
AlbumUpdate: [{ id: string; recipientId: string }];
|
||||||
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
||||||
|
AlbumAssetAdd: [{ albumId: string; assetId: string; userId: string }];
|
||||||
|
|
||||||
// asset events
|
// asset events
|
||||||
AssetCreate: [{ asset: Asset; file: UploadFile }];
|
AssetCreate: [{ asset: Asset; file: UploadFile }];
|
||||||
|
|||||||
@@ -742,6 +742,9 @@ describe(AlbumService.name, () => {
|
|||||||
owner.id,
|
owner.id,
|
||||||
);
|
);
|
||||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.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 () => {
|
it('should not set the thumbnail if the album has one already', async () => {
|
||||||
@@ -1055,6 +1058,16 @@ describe(AlbumService.name, () => {
|
|||||||
id: album2.id,
|
id: album2.id,
|
||||||
recipientId: owner2.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 () => {
|
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;
|
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;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,11 @@ export class WorkflowExecutionService extends BaseService {
|
|||||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
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) {
|
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
||||||
const items = await this.workflowRepository.search({ userId, trigger });
|
const items = await this.workflowRepository.search({ userId, trigger });
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected:
|
|||||||
types: [WorkflowType.AssetV1, WorkflowType.AssetPersonV1],
|
types: [WorkflowType.AssetV1, WorkflowType.AssetPersonV1],
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
trigger: WorkflowTrigger.AlbumAssetAdded,
|
||||||
|
types: [WorkflowType.AssetV1],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: WorkflowTrigger.AlbumAssetAdded,
|
||||||
|
types: [WorkflowType.AssetPersonV1],
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe(isMethodCompatible.name, () => {
|
describe(isMethodCompatible.name, () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
|
|||||||
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
||||||
[WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
|
[WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
|
||||||
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
|
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
|
||||||
|
[WorkflowTrigger.AlbumAssetAdded]: [WorkflowType.AssetV1],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkflowTriggers = () =>
|
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);
|
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: {
|
case WorkflowTrigger.PersonRecognized: {
|
||||||
return $t('trigger_person_recognized');
|
return $t('trigger_person_recognized');
|
||||||
}
|
}
|
||||||
|
case WorkflowTrigger.AlbumAssetAdded: {
|
||||||
|
return $t('trigger_album_asset_added');
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
@@ -23,6 +26,9 @@ export const getTriggerDescription = ($t: MessageFormatter, type: WorkflowTrigge
|
|||||||
case WorkflowTrigger.PersonRecognized: {
|
case WorkflowTrigger.PersonRecognized: {
|
||||||
return $t('trigger_person_recognized_description');
|
return $t('trigger_person_recognized_description');
|
||||||
}
|
}
|
||||||
|
case WorkflowTrigger.AlbumAssetAdded: {
|
||||||
|
return $t('trigger_album_asset_added_description');
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user