feat: more plugin triggers and methods (#28690)
parent
58586483dc
commit
da8505f61d
|
|
@ -24,11 +24,13 @@ class WorkflowTrigger {
|
||||||
String toJson() => value;
|
String toJson() => value;
|
||||||
|
|
||||||
static const assetCreate = WorkflowTrigger._(r'AssetCreate');
|
static const assetCreate = WorkflowTrigger._(r'AssetCreate');
|
||||||
|
static const assetMetadataExtraction = WorkflowTrigger._(r'AssetMetadataExtraction');
|
||||||
static const personRecognized = WorkflowTrigger._(r'PersonRecognized');
|
static const personRecognized = WorkflowTrigger._(r'PersonRecognized');
|
||||||
|
|
||||||
/// 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,
|
||||||
personRecognized,
|
personRecognized,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -69,6 +71,7 @@ class WorkflowTriggerTypeTransformer {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case r'AssetCreate': return WorkflowTrigger.assetCreate;
|
case r'AssetCreate': return WorkflowTrigger.assetCreate;
|
||||||
|
case r'AssetMetadataExtraction': return WorkflowTrigger.assetMetadataExtraction;
|
||||||
case r'PersonRecognized': return WorkflowTrigger.personRecognized;
|
case r'PersonRecognized': return WorkflowTrigger.personRecognized;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
|
|
|
||||||
|
|
@ -26355,6 +26355,7 @@
|
||||||
"description": "Plugin trigger type",
|
"description": "Plugin trigger type",
|
||||||
"enum": [
|
"enum": [
|
||||||
"AssetCreate",
|
"AssetCreate",
|
||||||
|
"AssetMetadataExtraction",
|
||||||
"PersonRecognized"
|
"PersonRecognized"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
"wasmPath": "dist/plugin.wasm",
|
"wasmPath": "dist/plugin.wasm",
|
||||||
"templates": [
|
"templates": [
|
||||||
{
|
{
|
||||||
"name": "auto-archive-screenshots",
|
"name": "screenshots-smart-album",
|
||||||
"title": "Auto-archive screenshots",
|
"title": "Archive screenshots",
|
||||||
"description": "Archive uploads with \"screenshot\" in the filename and optionally add them to an album",
|
"description": "Archive uploads with \"screenshot\" in the filename and optionally add them to an album",
|
||||||
"trigger": "AssetCreate",
|
"trigger": "AssetCreate",
|
||||||
"steps": [
|
"steps": [
|
||||||
|
|
@ -29,6 +29,27 @@
|
||||||
{
|
{
|
||||||
"method": "immich-plugin-core#assetAddToAlbums",
|
"method": "immich-plugin-core#assetAddToAlbums",
|
||||||
"config": {
|
"config": {
|
||||||
|
"albumName": "Screenshots",
|
||||||
|
"albumIds": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uiHints": ["SmartAlbum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "missing-timezone-smart-album",
|
||||||
|
"title": "Missing timezone",
|
||||||
|
"description": "Automatically create an album for assets without a time zone",
|
||||||
|
"trigger": "AssetMetadataExtraction",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"method": "immich-plugin-core#assetMissingTimeZoneFilter",
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "immich-plugin-core#assetAddToAlbums",
|
||||||
|
"config": {
|
||||||
|
"albumName": "Missing time zone",
|
||||||
"albumIds": []
|
"albumIds": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +89,24 @@
|
||||||
},
|
},
|
||||||
"uiHints": ["Filter"]
|
"uiHints": ["Filter"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "assetMissingTimeZoneFilter",
|
||||||
|
"title": "Filter by missing time zone",
|
||||||
|
"description": "Filter assets that have no time zone information",
|
||||||
|
"types": ["AssetV1"],
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"inverse": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Inverse",
|
||||||
|
"description": "Missing by default, set to true to filter assets with a time zone",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uiHints": ["Filter"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "filterFileType",
|
"name": "filterFileType",
|
||||||
"title": "Filter by file type",
|
"title": "Filter by file type",
|
||||||
|
|
@ -189,6 +228,12 @@
|
||||||
"array": true,
|
"array": true,
|
||||||
"description": "Target album IDs",
|
"description": "Target album IDs",
|
||||||
"uiHint": "AlbumId"
|
"uiHint": "AlbumId"
|
||||||
|
},
|
||||||
|
"albumName": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Album name",
|
||||||
|
"array": true,
|
||||||
|
"description": "Use an album with this name if one exists, otherwise create a new one"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["albumIds"]
|
"required": ["albumIds"]
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@extism/js-pdk": "^1.0.1",
|
"@extism/js-pdk": "^1.0.1",
|
||||||
|
"@immich/sdk": "workspace:*",
|
||||||
"@immich/plugin-sdk": "workspace:*",
|
"@immich/plugin-sdk": "workspace:*",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^6.0.0"
|
"typescript": "^6.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
// copy from
|
// keep in sync with plugin-sdk/host-functions.ts';
|
||||||
// import '@immich/plugin-sdk/host-functions';
|
|
||||||
declare module 'extism:host' {
|
declare module 'extism:host' {
|
||||||
interface user {
|
interface user {
|
||||||
albumAddAssets(ptr: PTR): I64;
|
searchAlbums(ptr: PTR): I64;
|
||||||
|
createAlbum(ptr: PTR): I64;
|
||||||
|
addAssetsToAlbum(ptr: PTR): I64;
|
||||||
addAssetsToAlbums(ptr: PTR): I64;
|
addAssetsToAlbums(ptr: PTR): I64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep in sync with manifest.json
|
||||||
declare module 'main' {
|
declare module 'main' {
|
||||||
|
// filters
|
||||||
export function assetFileFilter(): I32;
|
export function assetFileFilter(): I32;
|
||||||
|
export function assetMissingTimeZoneFilter(): I32;
|
||||||
|
|
||||||
|
// updates
|
||||||
export function assetFavorite(): I32;
|
export function assetFavorite(): I32;
|
||||||
export function assetVisibility(): I32;
|
export function assetVisibility(): I32;
|
||||||
export function assetArchive(): I32;
|
export function assetArchive(): I32;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { AssetStatus, AssetVisibility, WorkflowType, wrapper } from '@immich/plugin-sdk';
|
import { wrapper } from '@immich/plugin-sdk';
|
||||||
|
import { AssetVisibility, WorkflowType } from '@immich/sdk';
|
||||||
|
|
||||||
type AssetFileFilterConfig = {
|
type AssetFileFilterConfig = {
|
||||||
pattern: string;
|
pattern: string;
|
||||||
|
|
@ -41,6 +42,14 @@ export const assetFileFilter = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const assetMissingTimeZoneFilter = () => {
|
||||||
|
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||||
|
const hasTimeZone = !!data.asset?.exifInfo?.timeZone;
|
||||||
|
const needsTimeZone = config.inverse ? true : false;
|
||||||
|
return { workflow: { continue: hasTimeZone === needsTimeZone } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -89,28 +98,35 @@ export const assetLock = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assetTrash = () => {
|
export const assetTrash = () => {
|
||||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => ({
|
// TODO use trash/untrash host functions
|
||||||
changes: {
|
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
||||||
asset: config.inverse
|
|
||||||
? { deletedAt: null, status: AssetStatus.Active }
|
|
||||||
: { deletedAt: new Date().toISOString(), status: AssetStatus.Trashed },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assetAddToAlbums = () => {
|
export const assetAddToAlbums = () => {
|
||||||
return wrapper<WorkflowType.AssetV1, { albumIds: string[] }>(({ config, data, functions }) => {
|
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; albumName?: string }>(({ config, data, functions }) => {
|
||||||
|
const assetId = data.asset.id;
|
||||||
|
|
||||||
if (config.albumIds.length === 0) {
|
if (config.albumIds.length === 0) {
|
||||||
// noop
|
if (!config.albumName) {
|
||||||
return {};
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = functions.searchAlbums({ name: config.albumName });
|
||||||
|
if (!existing) {
|
||||||
|
const created = functions.createAlbum({ albumName: config.albumName, assetIds: [assetId] });
|
||||||
|
config.albumIds.push(created.id);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
config.albumIds.push(existing.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.albumIds.length === 1) {
|
if (config.albumIds.length === 1) {
|
||||||
functions.albumAddAssets(config.albumIds[0], [data.asset.id]);
|
functions.addAssetsToAlbum(config.albumIds[0], [assetId]);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [data.asset.id] });
|
functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [assetId] });
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
"name": "@immich/plugin-sdk",
|
"name": "@immich/plugin-sdk",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./host-functions": {
|
"./host-functions": {
|
||||||
|
|
@ -11,7 +10,8 @@
|
||||||
},
|
},
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
export enum WorkflowTrigger {
|
|
||||||
AssetCreate = 'AssetCreate',
|
|
||||||
PersonRecognized = 'PersonRecognized',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WorkflowType {
|
|
||||||
AssetV1 = 'AssetV1',
|
|
||||||
AssetPersonV1 = 'AssetPersonV1',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AssetType {
|
|
||||||
Image = 'IMAGE',
|
|
||||||
Video = 'VIDEO',
|
|
||||||
Audio = 'AUDIO',
|
|
||||||
Other = 'OTHER',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AssetStatus {
|
|
||||||
Active = 'active',
|
|
||||||
Trashed = 'trashed',
|
|
||||||
Deleted = 'deleted',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AssetVisibility {
|
|
||||||
Archive = 'archive',
|
|
||||||
Timeline = 'timeline',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Video part of the LivePhotos and MotionPhotos
|
|
||||||
*/
|
|
||||||
Hidden = 'hidden',
|
|
||||||
Locked = 'locked',
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,26 @@
|
||||||
import { type BulkIdResponseDto, type BulkIdsDto } from '@immich/sdk';
|
import {
|
||||||
|
getAllAlbums,
|
||||||
|
type AlbumResponseDto,
|
||||||
|
type BulkIdResponseDto,
|
||||||
|
type BulkIdsDto,
|
||||||
|
type CreateAlbumDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
|
||||||
// keep in sync with plugin-core/src/index.d.ts';
|
// keep in sync with plugin-core/src/index.d.ts';
|
||||||
declare module 'extism:host' {
|
declare module 'extism:host' {
|
||||||
interface user {
|
interface user {
|
||||||
albumAddAssets(ptr: PTR): I64;
|
searchAlbums(ptr: PTR): I64;
|
||||||
|
createAlbum(ptr: PTR): I64;
|
||||||
|
addAssetsToAlbum(ptr: PTR): I64;
|
||||||
addAssetsToAlbums(ptr: PTR): I64;
|
addAssetsToAlbums(ptr: PTR): I64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = Host.getFunctions();
|
type AlbumsToAssets = {
|
||||||
type HostFunctionName = keyof typeof host;
|
assetIds: string[];
|
||||||
|
albumIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type HostFunctionSuccessResult<T> = { success: true; response: T };
|
type HostFunctionSuccessResult<T> = { success: true; response: T };
|
||||||
type HostFunctionErrorResult = {
|
type HostFunctionErrorResult = {
|
||||||
success: false;
|
success: false;
|
||||||
|
|
@ -20,39 +31,49 @@ type HostFunctionResult<T> =
|
||||||
| HostFunctionSuccessResult<T>
|
| HostFunctionSuccessResult<T>
|
||||||
| HostFunctionErrorResult;
|
| HostFunctionErrorResult;
|
||||||
|
|
||||||
const call = <T, R>(name: HostFunctionName, authToken: string, args: T) => {
|
type QueryParams<T extends (...args: any) => any> = Parameters<T>[0];
|
||||||
const pointer1 = Memory.fromString(JSON.stringify({ authToken, args }));
|
type AlbumSearchDto = QueryParams<typeof getAllAlbums>;
|
||||||
const fn = host[name];
|
|
||||||
const handler = Memory.find(fn(pointer1.offset));
|
|
||||||
|
|
||||||
try {
|
export const hostFunctions = (authToken: string) => {
|
||||||
const result = JSON.parse(handler.readString()) as HostFunctionResult<R>;
|
const host = Host.getFunctions();
|
||||||
|
type HostFunctionName = keyof typeof host;
|
||||||
|
|
||||||
if (result.success) {
|
const call = <T, R>(name: HostFunctionName, authToken: string, args: T) => {
|
||||||
return result.response;
|
const pointer1 = Memory.fromString(JSON.stringify({ authToken, args }));
|
||||||
|
const fn = host[name];
|
||||||
|
const handler = Memory.find(fn(pointer1.offset));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(handler.readString()) as HostFunctionResult<R>;
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return result.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to call host function "${String(name)}", received ${result.status} - ${JSON.stringify(result.message)}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
handler.free();
|
||||||
|
pointer1.free();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
throw new Error(
|
return {
|
||||||
`Failed to call host function "${String(name)}", received ${result.status} - ${JSON.stringify(result.message)}`,
|
// album
|
||||||
);
|
searchAlbums: (dto: AlbumSearchDto) =>
|
||||||
} finally {
|
call<[AlbumSearchDto], AlbumResponseDto[]>('searchAlbums', authToken, [
|
||||||
handler.free();
|
dto,
|
||||||
pointer1.free();
|
]),
|
||||||
}
|
createAlbum: (dto: CreateAlbumDto) =>
|
||||||
|
call<[CreateAlbumDto], AlbumResponseDto>('createAlbum', authToken, [dto]),
|
||||||
|
addAssetsToAlbum: (albumId: string, assetIds: string[]) =>
|
||||||
|
call<[string, BulkIdsDto], BulkIdResponseDto[]>(
|
||||||
|
'addAssetsToAlbum',
|
||||||
|
authToken,
|
||||||
|
[albumId, { ids: assetIds }],
|
||||||
|
),
|
||||||
|
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
|
||||||
|
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type AlbumsToAssets = {
|
|
||||||
assetIds: string[];
|
|
||||||
albumIds: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hostFunctions = (authToken: string) => ({
|
|
||||||
albumAddAssets: (albumId: string, assetIds: string[]) =>
|
|
||||||
call<[string, BulkIdsDto], BulkIdResponseDto[]>(
|
|
||||||
'albumAddAssets',
|
|
||||||
authToken,
|
|
||||||
[albumId, { ids: assetIds }],
|
|
||||||
),
|
|
||||||
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
|
|
||||||
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from 'src/enum.js';
|
|
||||||
export * from 'src/host-functions.js';
|
export * from 'src/host-functions.js';
|
||||||
export * from 'src/sdk.js';
|
export * from 'src/sdk.js';
|
||||||
export * from 'src/types.js';
|
export * from 'src/types.js';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import type { WorkflowType } from 'src/enum.js';
|
import type { WorkflowType } from '@immich/sdk';
|
||||||
import { hostFunctions } from 'src/host-functions.js';
|
import { hostFunctions } from 'src/host-functions.js';
|
||||||
import type {
|
import type {
|
||||||
ConfigValue,
|
ConfigValue,
|
||||||
WorkflowEventPayload,
|
WorkflowEventPayload,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
|
WorkflowStepConfig,
|
||||||
} from 'src/types.js';
|
} from 'src/types.js';
|
||||||
|
|
||||||
export const wrapper = <
|
export const wrapper = <
|
||||||
|
|
@ -19,19 +20,28 @@ export const wrapper = <
|
||||||
const input = Host.inputString();
|
const input = Host.inputString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(input) as WorkflowEventPayload<T, TConfig>;
|
const payload = JSON.parse(input) as WorkflowEventPayload<T, TConfig>;
|
||||||
// const debug = event.workflow.debug ?? false;
|
const event = {
|
||||||
|
...payload,
|
||||||
|
functions: hostFunctions(payload.workflow.authToken),
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventConfigBefore = JSON.stringify(event.config);
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
`Inputs: trigger=${event.trigger}, event=${event.type}, config=${JSON.stringify(event.config)}`,
|
`Inputs: trigger=${event.trigger}, event=${event.type}, config=${eventConfigBefore}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response =
|
const response = fn(event) ?? {};
|
||||||
fn({ ...event, functions: hostFunctions(event.workflow.authToken) }) ??
|
|
||||||
{};
|
// if config changed, notify host
|
||||||
|
const eventConfigAfter = JSON.stringify(event.config);
|
||||||
|
if (!response.config && eventConfigBefore !== eventConfigAfter) {
|
||||||
|
response.config = event.config as WorkflowStepConfig;
|
||||||
|
}
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
`Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}`,
|
`Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}, config=${JSON.stringify(response.config)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = JSON.stringify(response);
|
const output = JSON.stringify(response);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import type {
|
import type { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk';
|
||||||
AssetStatus,
|
|
||||||
AssetType,
|
|
||||||
AssetVisibility,
|
|
||||||
WorkflowTrigger,
|
|
||||||
WorkflowType,
|
|
||||||
} from 'src/enum.js';
|
|
||||||
|
|
||||||
type DeepPartial<T> = T extends Date
|
type DeepPartial<T> = T extends Date
|
||||||
? T
|
? T
|
||||||
|
|
@ -21,6 +15,12 @@ export type WorkflowEventMap = {
|
||||||
|
|
||||||
export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
|
export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
|
||||||
|
|
||||||
|
export enum WorkflowTrigger {
|
||||||
|
AssetCreate = 'AssetCreate',
|
||||||
|
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
||||||
|
PersonRecognized = 'PersonRecognized',
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkflowEventPayload<
|
export type WorkflowEventPayload<
|
||||||
T extends WorkflowType = WorkflowType,
|
T extends WorkflowType = WorkflowType,
|
||||||
TConfig = WorkflowStepConfig,
|
TConfig = WorkflowStepConfig,
|
||||||
|
|
@ -48,6 +48,8 @@ export type WorkflowResponse<T extends WorkflowType = WorkflowType> = {
|
||||||
changes?: WorkflowChanges<T>;
|
changes?: WorkflowChanges<T>;
|
||||||
/** data to be passed to the next workflow step */
|
/** data to be passed to the next workflow step */
|
||||||
data?: Record<string, unknown>;
|
data?: Record<string, unknown>;
|
||||||
|
/** update step config */
|
||||||
|
config?: WorkflowStepConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowStepConfig = {
|
export type WorkflowStepConfig = {
|
||||||
|
|
@ -66,7 +68,7 @@ export type AssetV1 = {
|
||||||
asset: {
|
asset: {
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
type: AssetType;
|
type: AssetTypeEnum;
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
fileCreatedAt: string;
|
fileCreatedAt: string;
|
||||||
fileModifiedAt: string;
|
fileModifiedAt: string;
|
||||||
|
|
@ -83,7 +85,6 @@ export type AssetV1 = {
|
||||||
localDateTime: string;
|
localDateTime: string;
|
||||||
stackId: string | null;
|
stackId: string | null;
|
||||||
duplicateId: string | null;
|
duplicateId: string | null;
|
||||||
status: AssetStatus;
|
|
||||||
visibility: AssetVisibility;
|
visibility: AssetVisibility;
|
||||||
isEdited: boolean;
|
isEdited: boolean;
|
||||||
exifInfo: {
|
exifInfo: {
|
||||||
|
|
|
||||||
|
|
@ -7081,6 +7081,7 @@ export enum WorkflowType {
|
||||||
}
|
}
|
||||||
export enum WorkflowTrigger {
|
export enum WorkflowTrigger {
|
||||||
AssetCreate = "AssetCreate",
|
AssetCreate = "AssetCreate",
|
||||||
|
AssetMetadataExtraction = "AssetMetadataExtraction",
|
||||||
PersonRecognized = "PersonRecognized"
|
PersonRecognized = "PersonRecognized"
|
||||||
}
|
}
|
||||||
export enum QueueJobStatus {
|
export enum QueueJobStatus {
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,9 @@ importers:
|
||||||
'@immich/plugin-sdk':
|
'@immich/plugin-sdk':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-sdk
|
version: link:../plugin-sdk
|
||||||
|
'@immich/sdk':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../sdk
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.28.0
|
specifier: ^0.28.0
|
||||||
version: 0.28.0
|
version: 0.28.0
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { WorkflowController } from 'src/controllers/workflow.controller';
|
import { WorkflowController } from 'src/controllers/workflow.controller';
|
||||||
import { WorkflowTrigger } from 'src/enum';
|
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { WorkflowService } from 'src/services/workflow.service';
|
import { WorkflowService } from 'src/services/workflow.service';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { createZodDto } from 'nestjs-zod';
|
import { createZodDto } from 'nestjs-zod';
|
||||||
import { JsonSchemaDto } from 'src/dtos/json-schema.dto';
|
import { JsonSchemaDto } from 'src/dtos/json-schema.dto';
|
||||||
import { WorkflowTrigger, WorkflowTriggerSchema, WorkflowType, WorkflowTypeSchema } from 'src/enum';
|
import { WorkflowTriggerSchema, WorkflowType, WorkflowTypeSchema } from 'src/enum';
|
||||||
import { asPluginKey } from 'src/utils/workflow';
|
import { asPluginKey } from 'src/utils/workflow';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { WorkflowStepConfig } from '@immich/plugin-sdk';
|
import type { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { createZodDto } from 'nestjs-zod';
|
import { createZodDto } from 'nestjs-zod';
|
||||||
import { WorkflowTrigger, WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
|
import { WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
const WorkflowTriggerResponseSchema = z
|
const WorkflowTriggerResponseSchema = z
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
|
|
@ -1164,11 +1165,6 @@ export enum PluginContext {
|
||||||
|
|
||||||
export const PluginContextSchema = z.enum(PluginContext).describe('Plugin context').meta({ id: 'PluginContextType' });
|
export const PluginContextSchema = z.enum(PluginContext).describe('Plugin context').meta({ id: 'PluginContextType' });
|
||||||
|
|
||||||
export enum WorkflowTrigger {
|
|
||||||
AssetCreate = 'AssetCreate',
|
|
||||||
PersonRecognized = 'PersonRecognized',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkflowTriggerSchema = z
|
export const WorkflowTriggerSchema = z
|
||||||
.enum(WorkflowTrigger)
|
.enum(WorkflowTrigger)
|
||||||
.describe('Plugin trigger type')
|
.describe('Plugin trigger type')
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,10 @@ export class WorkflowRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateStep(id: string, dto: Updateable<WorkflowStepTable>) {
|
||||||
|
await this.db.updateTable('workflow_step').where('workflow_step.id', '=', id).set(dto).execute();
|
||||||
|
}
|
||||||
|
|
||||||
private async replaceAndReturn(tx: Kysely<DB>, workflowId: string, steps?: WorkflowStepUpsert[]) {
|
private async replaceAndReturn(tx: Kysely<DB>, workflowId: string, steps?: WorkflowStepUpsert[]) {
|
||||||
if (steps) {
|
if (steps) {
|
||||||
await tx.deleteFrom('workflow_step').where('workflowId', '=', workflowId).execute();
|
await tx.deleteFrom('workflow_step').where('workflowId', '=', workflowId).execute();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
|
@ -9,7 +10,6 @@ import {
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from '@immich/sql-tools';
|
} from '@immich/sql-tools';
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { WorkflowTrigger } from 'src/enum';
|
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
||||||
@Table('workflow')
|
@Table('workflow')
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import { CurrentPlugin } from '@extism/extism';
|
import { CurrentPlugin } from '@extism/extism';
|
||||||
import { WorkflowChanges, WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk';
|
import {
|
||||||
|
WorkflowChanges,
|
||||||
|
WorkflowEventData,
|
||||||
|
WorkflowEventPayload,
|
||||||
|
WorkflowResponse,
|
||||||
|
WorkflowTrigger,
|
||||||
|
} from '@immich/plugin-sdk';
|
||||||
import { HttpException, UnauthorizedException } from '@nestjs/common';
|
import { HttpException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { DummyValue, OnEvent, OnJob } from 'src/decorators';
|
import { DummyValue, OnEvent, OnJob } from 'src/decorators';
|
||||||
import { AlbumsAddAssetsDto } from 'src/dtos/album.dto';
|
import { AlbumsAddAssetsDto, CreateAlbumDto, GetAlbumsDto } from 'src/dtos/album.dto';
|
||||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||||
|
|
@ -15,7 +21,6 @@ import {
|
||||||
JobName,
|
JobName,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
QueueName,
|
QueueName,
|
||||||
WorkflowTrigger,
|
|
||||||
WorkflowType,
|
WorkflowType,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { ArgOf } from 'src/repositories/event.repository';
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
|
|
@ -61,7 +66,9 @@ export class WorkflowExecutionService extends BaseService {
|
||||||
|
|
||||||
const albumService = BaseService.create(AlbumService, this);
|
const albumService = BaseService.create(AlbumService, this);
|
||||||
|
|
||||||
const albumAddAssets = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) =>
|
const searchAlbums = this.wrap<[dto: GetAlbumsDto]>((authDto, args) => albumService.getAll(authDto, ...args));
|
||||||
|
const createAlbum = this.wrap<[dto: CreateAlbumDto]>((authDto, args) => albumService.create(authDto, ...args));
|
||||||
|
const addAssetsToAlbum = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) =>
|
||||||
albumService.addAssets(authDto, ...args),
|
albumService.addAssets(authDto, ...args),
|
||||||
);
|
);
|
||||||
const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) =>
|
const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) =>
|
||||||
|
|
@ -69,12 +76,16 @@ export class WorkflowExecutionService extends BaseService {
|
||||||
);
|
);
|
||||||
|
|
||||||
const functions = {
|
const functions = {
|
||||||
albumAddAssets,
|
searchAlbums,
|
||||||
|
createAlbum,
|
||||||
|
addAssetsToAlbum,
|
||||||
addAssetsToAlbums,
|
addAssetsToAlbums,
|
||||||
};
|
};
|
||||||
|
|
||||||
const stubs = {
|
const stubs: typeof functions = {
|
||||||
albumAddAssets: dummy,
|
searchAlbums: dummy,
|
||||||
|
createAlbum: dummy,
|
||||||
|
addAssetsToAlbum: dummy,
|
||||||
addAssetsToAlbums: dummy,
|
addAssetsToAlbums: dummy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -252,6 +263,17 @@ export class WorkflowExecutionService extends BaseService {
|
||||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate });
|
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'AssetMetadataExtracted' })
|
||||||
|
onAssetMetadataExtracted({ userId, assetId, source }: ArgOf<'AssetMetadataExtracted'>) {
|
||||||
|
// prevent loops
|
||||||
|
// TODO loop detection in job service directly
|
||||||
|
if (source === 'sidecar-write') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -286,6 +308,25 @@ export class WorkflowExecutionService extends BaseService {
|
||||||
await assetService.update(auth, assetId, {
|
await assetService.update(auth, assetId, {
|
||||||
isFavorite: asset.isFavorite,
|
isFavorite: asset.isFavorite,
|
||||||
visibility: asset.visibility,
|
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>;
|
} satisfies ExecuteOptions<typeof type>;
|
||||||
|
|
@ -367,6 +408,10 @@ export class WorkflowExecutionService extends BaseService {
|
||||||
({ data } = await read(type));
|
({ data } = await read(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result?.config) {
|
||||||
|
await this.workflowRepository.updateStep(step.id, { config: result.config });
|
||||||
|
}
|
||||||
|
|
||||||
const shouldContinue = result?.workflow?.continue ?? true;
|
const shouldContinue = result?.workflow?.continue ?? true;
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { WorkflowStepConfig } from '@immich/plugin-sdk';
|
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
WorkflowTriggerResponseDto,
|
WorkflowTriggerResponseDto,
|
||||||
WorkflowUpdateDto,
|
WorkflowUpdateDto,
|
||||||
} from 'src/dtos/workflow.dto';
|
} from 'src/dtos/workflow.dto';
|
||||||
import { Permission, WorkflowTrigger } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow';
|
import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { ShallowDehydrateObject } from 'kysely';
|
import { ShallowDehydrateObject } from 'kysely';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { VECTOR_EXTENSIONS } from 'src/constants';
|
import { VECTOR_EXTENSIONS } from 'src/constants';
|
||||||
|
|
@ -29,7 +30,6 @@ import {
|
||||||
TranscodeTarget,
|
TranscodeTarget,
|
||||||
UserMetadataKey,
|
UserMetadataKey,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
WorkflowTrigger,
|
|
||||||
WorkflowType,
|
WorkflowType,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
|
import { WorkflowType } from 'src/enum';
|
||||||
import { isMethodCompatible } from 'src/utils/workflow';
|
import { isMethodCompatible } from 'src/utils/workflow';
|
||||||
|
|
||||||
const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected: boolean }> = [
|
const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected: boolean }> = [
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
|
import { WorkflowType } from 'src/enum';
|
||||||
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
||||||
|
|
||||||
export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
|
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],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkflowTriggers = () =>
|
export const getWorkflowTriggers = () =>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
import { WorkflowType } from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { WorkflowStepConfig } from '@immich/plugin-sdk';
|
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||||
import { AssetVisibility, LogLevel, WorkflowTrigger } from 'src/enum';
|
import { AssetVisibility, LogLevel } from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
|
@ -12,6 +12,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { WorkflowExecutionService } from 'src/services/workflow-execution.service';
|
import { WorkflowExecutionService } from 'src/services/workflow-execution.service';
|
||||||
|
|
@ -33,8 +34,9 @@ class WorkflowTestContext extends MediumTestContext<WorkflowExecutionService> {
|
||||||
CryptoRepository,
|
CryptoRepository,
|
||||||
DatabaseRepository,
|
DatabaseRepository,
|
||||||
LoggingRepository,
|
LoggingRepository,
|
||||||
StorageRepository,
|
|
||||||
PluginRepository,
|
PluginRepository,
|
||||||
|
StorageRepository,
|
||||||
|
UserRepository,
|
||||||
WorkflowRepository,
|
WorkflowRepository,
|
||||||
],
|
],
|
||||||
mock: [ConfigRepository],
|
mock: [ConfigRepository],
|
||||||
|
|
@ -231,6 +233,52 @@ describe('core plugin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('assetAddToAlbums', () => {
|
describe('assetAddToAlbums', () => {
|
||||||
|
it('should create an album by name', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetCreate,
|
||||||
|
steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [], albumName: 'Screenshots' } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
const albums = await ctx.get(AlbumRepository).getAll(user.id);
|
||||||
|
expect(albums).toHaveLength(1);
|
||||||
|
|
||||||
|
const album = albums[0]!;
|
||||||
|
expect(album.albumName).toEqual('Screenshots');
|
||||||
|
|
||||||
|
const updated = await ctx.get(WorkflowRepository).get(workflow.id);
|
||||||
|
expect(updated?.steps[0].config).toEqual({ albumIds: [album.id], albumName: 'Screenshots' });
|
||||||
|
|
||||||
|
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use the name when there is an albumId', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||||
|
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetCreate,
|
||||||
|
steps: [
|
||||||
|
{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id], albumName: 'Screenshots' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const albums = await ctx.get(AlbumRepository).getAll(user.id);
|
||||||
|
expect(albums).toHaveLength(1);
|
||||||
|
expect(albums[0].albumName).toEqual(album.albumName);
|
||||||
|
|
||||||
|
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id);
|
||||||
|
});
|
||||||
|
|
||||||
it('should add an asset to an album', async () => {
|
it('should add an asset to an album', async () => {
|
||||||
const { user } = await ctx.newUser();
|
const { user } = await ctx.newUser();
|
||||||
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import AlbumCover from '$lib/components/album-page/AlbumCover.svelte';
|
import AlbumCover from '$lib/components/album-page/AlbumCover.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { getAlbumInfo } from '@immich/sdk';
|
import { getAlbumInfo } from '@immich/sdk';
|
||||||
import { IconButton, LoadingSpinner } from '@immich/ui';
|
import { IconButton, Text, LoadingSpinner } from '@immich/ui';
|
||||||
import { mdiTrashCanOutline } from '@mdi/js';
|
import { mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
|
@ -46,5 +46,22 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:catch}
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<Text>{$t('unknown')}</Text>
|
||||||
|
<Text color="muted" size="small" variant="italic">{albumId}</Text>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<IconButton
|
||||||
|
icon={mdiTrashCanOutline}
|
||||||
|
shape="round"
|
||||||
|
color="danger"
|
||||||
|
variant="ghost"
|
||||||
|
onclick={onDelete}
|
||||||
|
aria-label={$t('remove')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue