mirror of
https://github.com/immich-app/immich
synced 2026-08-29 13:15:45 +00:00
wip(server): asset added to album workflow trigger
This commit is contained in:
parent
4988c0a4c2
commit
223f4986f9
13 changed files with 145 additions and 39 deletions
|
|
@ -2136,6 +2136,8 @@
|
|||
"trash_page_info": "Trashed items will be permanently deleted after {days} days",
|
||||
"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_metadata_extraction": "Asset Metadata Extraction",
|
||||
"trigger_asset_metadata_extraction_description": "Triggered when the EXIF metadata of an asset is extracted",
|
||||
"trigger_asset_uploaded": "Asset Upload",
|
||||
|
|
|
|||
|
|
@ -19316,6 +19316,7 @@
|
|||
"OcrQueueAll",
|
||||
"Ocr",
|
||||
"WorkflowAssetTrigger",
|
||||
"WorkflowAlbumAssetTrigger",
|
||||
"IntegrityUntrackedFilesQueueAll",
|
||||
"IntegrityUntrackedFiles",
|
||||
"IntegrityUntrackedRefresh",
|
||||
|
|
@ -28031,7 +28032,8 @@
|
|||
"description": "Plugin trigger type",
|
||||
"enum": [
|
||||
"AssetCreate",
|
||||
"AssetMetadataExtraction"
|
||||
"AssetMetadataExtraction",
|
||||
"AlbumAssetAdded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -28058,7 +28060,8 @@
|
|||
"WorkflowType": {
|
||||
"description": "Workflow type",
|
||||
"enum": [
|
||||
"AssetV1"
|
||||
"AssetV1",
|
||||
"AlbumAssetV1"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type DeepPartial<T> = T extends Date
|
|||
|
||||
export type WorkflowEventMap = {
|
||||
[WorkflowType.AssetV1]: AssetV1;
|
||||
[WorkflowType.AlbumAssetV1]: AlbumAssetV1;
|
||||
// [WorkflowType.AssetPersonV1]: AssetPersonV1;
|
||||
} & { [K in WorkflowType]: unknown };
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
|
|||
export enum WorkflowTrigger {
|
||||
AssetCreate = 'AssetCreate',
|
||||
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
||||
AlbumAssetAdded = 'AlbumAssetAdded',
|
||||
// PersonRecognized = 'PersonRecognized',
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +125,12 @@ export type AssetV1 = {
|
|||
};
|
||||
};
|
||||
|
||||
export type AlbumAssetV1 = AssetV1 & {
|
||||
album: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
// export type AssetPersonV1 = AssetV1 & {
|
||||
// person: {
|
||||
// id: string;
|
||||
|
|
|
|||
|
|
@ -7419,11 +7419,13 @@ export enum PartnerDirection {
|
|||
SharedWith = "shared-with"
|
||||
}
|
||||
export enum WorkflowType {
|
||||
AssetV1 = "AssetV1"
|
||||
AssetV1 = "AssetV1",
|
||||
AlbumAssetV1 = "AlbumAssetV1"
|
||||
}
|
||||
export enum WorkflowTrigger {
|
||||
AssetCreate = "AssetCreate",
|
||||
AssetMetadataExtraction = "AssetMetadataExtraction"
|
||||
AssetMetadataExtraction = "AssetMetadataExtraction",
|
||||
AlbumAssetAdded = "AlbumAssetAdded"
|
||||
}
|
||||
export enum QueueJobStatus {
|
||||
Active = "active",
|
||||
|
|
|
|||
|
|
@ -903,6 +903,7 @@ export enum JobName {
|
|||
|
||||
// Workflow
|
||||
WorkflowAssetTrigger = 'WorkflowAssetTrigger',
|
||||
WorkflowAlbumAssetTrigger = 'WorkflowAlbumAssetTrigger',
|
||||
|
||||
// Integrity
|
||||
IntegrityUntrackedFilesQueueAll = 'IntegrityUntrackedFilesQueueAll',
|
||||
|
|
@ -1225,6 +1226,7 @@ export const WorkflowTriggerSchema = z
|
|||
|
||||
export enum WorkflowType {
|
||||
AssetV1 = 'AssetV1',
|
||||
AlbumAssetV1 = 'AlbumAssetV1',
|
||||
// AssetPersonV1 = 'AssetPersonV1',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ select
|
|||
"workflow"."enabled",
|
||||
"workflow"."createdAt",
|
||||
"workflow"."updatedAt",
|
||||
"workflow"."ownerId",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
|
|
@ -43,6 +44,7 @@ select
|
|||
"workflow"."enabled",
|
||||
"workflow"."createdAt",
|
||||
"workflow"."updatedAt",
|
||||
"workflow"."ownerId",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ type EventMap = {
|
|||
// album events
|
||||
AlbumUpdate: [{ id: string; userIds: string[]; recipientIds: string[] }];
|
||||
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
||||
AlbumAssetsAdded: [{ albumId: string; userIds: string[]; recipientIds: string[]; assetIds: string[] }];
|
||||
|
||||
// asset events
|
||||
AssetCreate: [{ asset: Pick<Asset, 'id' | 'ownerId'>; file?: UploadFile }];
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export class WorkflowRepository {
|
|||
'workflow.enabled',
|
||||
'workflow.createdAt',
|
||||
'workflow.updatedAt',
|
||||
'workflow.ownerId',
|
||||
])
|
||||
.select((eb) => [
|
||||
jsonArrayFrom(
|
||||
|
|
|
|||
|
|
@ -193,6 +193,12 @@ export class AlbumService extends BaseService {
|
|||
const userIds = album.albumUsers.map(({ user }) => user.id);
|
||||
const recipientIds = userIds.filter((userId) => userId !== auth.user.id);
|
||||
await this.eventRepository.emit('AlbumUpdate', { id, userIds, recipientIds });
|
||||
await this.eventRepository.emit('AlbumAssetsAdded', {
|
||||
albumId: id,
|
||||
userIds,
|
||||
recipientIds,
|
||||
assetIds: results.filter((a) => a.success).map((a) => a.id),
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
|
|
@ -221,7 +227,8 @@ export class AlbumService extends BaseService {
|
|||
}
|
||||
|
||||
const albumAssetValues: { albumId: string; assetId: string }[] = [];
|
||||
const events: { id: string; userIds: string[]; recipientIds: string[] }[] = [];
|
||||
const updateEvents: { id: string; userIds: string[]; recipientIds: string[] }[] = [];
|
||||
const addedEvents: { albumId: string; userIds: string[]; recipientIds: string[]; assetIds: string[] }[] = [];
|
||||
for (const albumId of allowedAlbumIds) {
|
||||
const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]);
|
||||
const notPresentAssetIds = [...allowedAssetIds.difference(existingAssetIds)];
|
||||
|
|
@ -246,13 +253,17 @@ export class AlbumService extends BaseService {
|
|||
);
|
||||
const userIds = album.albumUsers.map(({ user }) => user.id);
|
||||
const recipientIds = userIds.filter((userId) => userId !== auth.user.id);
|
||||
events.push({ id: albumId, userIds, recipientIds });
|
||||
updateEvents.push({ id: albumId, userIds, recipientIds });
|
||||
addedEvents.push({ albumId, userIds, recipientIds, assetIds: notPresentAssetIds });
|
||||
}
|
||||
|
||||
await this.albumRepository.addAssetIdsToAlbums(albumAssetValues);
|
||||
for (const event of events) {
|
||||
for (const event of updateEvents) {
|
||||
await this.eventRepository.emit('AlbumUpdate', event);
|
||||
}
|
||||
for (const event of addedEvents) {
|
||||
await this.eventRepository.emit('AlbumAssetsAdded', event);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { ArgOf } from 'src/repositories/event.repository';
|
|||
import { AlbumService } from 'src/services/album.service';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobOf } from 'src/types';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
|
||||
const dummy = () => {
|
||||
throw new Error(
|
||||
|
|
@ -42,6 +42,8 @@ type ExecuteOptions<T extends WorkflowType> = {
|
|||
|
||||
type AssetTrigger = { userId: string; assetId: string; trigger: WorkflowTrigger };
|
||||
|
||||
type AlbumAssetTrigger = { userIds: string[]; albumId: string; assetIds: string[]; trigger: WorkflowTrigger };
|
||||
|
||||
type HostContext = {
|
||||
allowedHosts: string[];
|
||||
};
|
||||
|
|
@ -309,6 +311,16 @@ export class WorkflowExecutionService extends BaseService {
|
|||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AlbumAssetsAdded' })
|
||||
onAlbumAssetsAdded({ albumId, userIds, recipientIds, assetIds }: ArgOf<'AlbumAssetsAdded'>) {
|
||||
return this.onAlbumAssetTrigger({
|
||||
albumId,
|
||||
userIds: [...userIds, ...recipientIds],
|
||||
assetIds,
|
||||
trigger: WorkflowTrigger.AlbumAssetAdded,
|
||||
});
|
||||
}
|
||||
|
||||
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
||||
const items = await this.workflowRepository.search({ userId, trigger });
|
||||
await this.jobRepository.queueAll(
|
||||
|
|
@ -319,11 +331,64 @@ export class WorkflowExecutionService extends BaseService {
|
|||
);
|
||||
}
|
||||
|
||||
private async onAlbumAssetTrigger({ albumId, userIds, assetIds, trigger }: AlbumAssetTrigger) {
|
||||
let jobs: JobItem[] = [];
|
||||
for (const userId of userIds) {
|
||||
const items = await this.workflowRepository.search({ userId, trigger });
|
||||
if (!items.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let batch: JobItem[] = items.flatMap(({ id: workflowId }) =>
|
||||
assetIds.map((assetId) => ({
|
||||
name: JobName.WorkflowAlbumAssetTrigger,
|
||||
data: { workflowId, assetId, albumId, trigger },
|
||||
})),
|
||||
);
|
||||
jobs.push(...batch);
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
}
|
||||
|
||||
private writeAssetV1(assetId: string, type: WorkflowType) {
|
||||
const assetService = BaseService.create(AssetService, this);
|
||||
|
||||
return async (auth: AuthDto, changes: WorkflowChanges<typeof type>) => {
|
||||
const asset = changes.asset;
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
await assetService.update(auth, assetId, {
|
||||
isFavorite: asset.isFavorite,
|
||||
visibility: asset.visibility,
|
||||
dateTimeOriginal: asset.exifInfo?.dateTimeOriginal ?? undefined,
|
||||
// TODO allow setting to null
|
||||
longitude: asset.exifInfo?.longitude ?? undefined,
|
||||
// TODO allow setting to null
|
||||
latitude: asset.exifInfo?.latitude ?? undefined,
|
||||
// TODO allow setting to null
|
||||
description: asset.exifInfo?.description ?? undefined,
|
||||
rating: asset.exifInfo?.rating,
|
||||
|
||||
// TODO add to update dto
|
||||
// make: asset.exifInfo?.make,
|
||||
// model: asset.exifInfo?.model,
|
||||
// city: asset.exifInfo?.city,
|
||||
// state: asset.exifInfo?.state,
|
||||
// country: asset.exifInfo?.country,
|
||||
// lensModel: asset.exifInfo?.lensModel,
|
||||
// fNumber: asset.exifInfo?.fNumber,
|
||||
// fps: asset.exifInfo?.fps,
|
||||
// iso: asset.exifInfo?.iso,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.WorkflowAssetTrigger, queue: QueueName.Workflow })
|
||||
handleAssetTrigger({ workflowId, assetId }: JobOf<JobName.WorkflowAssetTrigger>) {
|
||||
return this.execute(workflowId, (type) => {
|
||||
const assetService = BaseService.create(AssetService, this);
|
||||
|
||||
switch (type) {
|
||||
case WorkflowType.AssetV1: {
|
||||
return {
|
||||
|
|
@ -334,35 +399,35 @@ export class WorkflowExecutionService extends BaseService {
|
|||
authUserId: asset.ownerId,
|
||||
};
|
||||
},
|
||||
write: this.writeAssetV1(assetId, type),
|
||||
} satisfies ExecuteOptions<typeof type>;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.WorkflowAlbumAssetTrigger, queue: QueueName.Workflow })
|
||||
handleAlbumAssetTrigger({ workflowId, assetId, albumId }: JobOf<JobName.WorkflowAlbumAssetTrigger>) {
|
||||
return this.execute(workflowId, (type) => {
|
||||
switch (type) {
|
||||
case WorkflowType.AssetV1: {
|
||||
return {
|
||||
read: async () => {
|
||||
const asset = await this.workflowRepository.getForAssetV1(assetId);
|
||||
const workflow = await this.workflowRepository.get(workflowId);
|
||||
|
||||
return {
|
||||
data: { asset, album: { id: albumId } } as any,
|
||||
authUserId: workflow!.ownerId,
|
||||
};
|
||||
},
|
||||
write: async (auth, changes) => {
|
||||
const asset = changes.asset;
|
||||
if (!asset) {
|
||||
return;
|
||||
const asset = await this.workflowRepository.getForAssetV1(assetId);
|
||||
const workflow = await this.workflowRepository.get(workflowId);
|
||||
|
||||
if (asset.ownerId === workflow!.ownerId) {
|
||||
await this.writeAssetV1(assetId, type)(auth, changes);
|
||||
}
|
||||
|
||||
await assetService.update(auth, assetId, {
|
||||
isFavorite: asset.isFavorite,
|
||||
visibility: asset.visibility,
|
||||
dateTimeOriginal: asset.exifInfo?.dateTimeOriginal ?? undefined,
|
||||
// TODO allow setting to null
|
||||
longitude: asset.exifInfo?.longitude ?? undefined,
|
||||
// TODO allow setting to null
|
||||
latitude: asset.exifInfo?.latitude ?? undefined,
|
||||
// TODO allow setting to null
|
||||
description: asset.exifInfo?.description ?? undefined,
|
||||
rating: asset.exifInfo?.rating,
|
||||
|
||||
// TODO add to update dto
|
||||
// make: asset.exifInfo?.make,
|
||||
// model: asset.exifInfo?.model,
|
||||
// city: asset.exifInfo?.city,
|
||||
// state: asset.exifInfo?.state,
|
||||
// country: asset.exifInfo?.country,
|
||||
// lensModel: asset.exifInfo?.lensModel,
|
||||
// fNumber: asset.exifInfo?.fNumber,
|
||||
// fps: asset.exifInfo?.fps,
|
||||
// iso: asset.exifInfo?.iso,
|
||||
});
|
||||
},
|
||||
} satisfies ExecuteOptions<typeof type>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -458,6 +458,7 @@ export type JobItem =
|
|||
|
||||
// Workflow
|
||||
| { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string } }
|
||||
| { name: JobName.WorkflowAlbumAssetTrigger; data: { workflowId: string; assetId: string; albumId: string } }
|
||||
|
||||
// Integrity
|
||||
| { name: JobName.IntegrityUntrackedFilesQueueAll; data?: IIntegrityJob }
|
||||
|
|
|
|||
|
|
@ -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.AlbumAssetV1],
|
||||
};
|
||||
|
||||
export const getWorkflowTriggers = () =>
|
||||
|
|
@ -14,6 +15,7 @@ export const getWorkflowTriggers = () =>
|
|||
/** some types extend other types and have implied compatibility */
|
||||
const inferredMap: Record<WorkflowType, WorkflowType[]> = {
|
||||
[WorkflowType.AssetV1]: [],
|
||||
[WorkflowType.AlbumAssetV1]: [WorkflowType.AssetV1],
|
||||
// [WorkflowType.AssetPersonV1]: [WorkflowType.AssetV1],
|
||||
};
|
||||
|
||||
|
|
@ -29,8 +31,8 @@ const withImpliedItems = (type: WorkflowType): WorkflowType[] => {
|
|||
|
||||
export const isMethodCompatible = (pluginMethod: { types: WorkflowType[] }, trigger: WorkflowTrigger) => {
|
||||
const validTypes = triggerMap[trigger];
|
||||
const pluginCompatibility = pluginMethod.types.map((type) => withImpliedItems(type));
|
||||
for (const requested of validTypes) {
|
||||
const pluginCompatibility = validTypes.map((type) => withImpliedItems(type));
|
||||
for (const requested of pluginMethod.types) {
|
||||
for (const pluginCompatibilityGroup of pluginCompatibility) {
|
||||
if (pluginCompatibilityGroup.includes(requested)) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ export const getTriggerName = ($t: MessageFormatter, type: WorkflowTrigger) => {
|
|||
case WorkflowTrigger.AssetMetadataExtraction: {
|
||||
return $t('trigger_asset_metadata_extraction');
|
||||
}
|
||||
case WorkflowTrigger.AlbumAssetAdded: {
|
||||
return $t('trigger_album_asset_added');
|
||||
}
|
||||
default: {
|
||||
return type;
|
||||
}
|
||||
|
|
@ -30,6 +33,9 @@ export const getTriggerDescription = ($t: MessageFormatter, type: WorkflowTrigge
|
|||
case WorkflowTrigger.AssetMetadataExtraction: {
|
||||
return $t('trigger_asset_metadata_extraction_description');
|
||||
}
|
||||
case WorkflowTrigger.AlbumAssetAdded: {
|
||||
return $t('trigger_album_asset_added_description');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue