mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: plugins
chore: better types feat: plugins
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/dist
|
||||
/node_modules
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"semi": true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
outdir: 'dist',
|
||||
bundle: true,
|
||||
sourcemap: false,
|
||||
minify: false, // might want to use true for production build
|
||||
format: 'cjs', // needs to be CJS for now
|
||||
target: ['es2020'], // don't go over es2020 because quickjs doesn't support it
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"name": "immich-plugin-core",
|
||||
"version": "2.0.1",
|
||||
"title": "Immich Core Plugin",
|
||||
"description": "Core workflow capabilities for Immich",
|
||||
"author": "Immich Team",
|
||||
"wasmPath": "dist/plugin.wasm",
|
||||
"methods": [
|
||||
{
|
||||
"name": "assetFileFilter",
|
||||
"title": "Filter by filename",
|
||||
"description": "Filter assets by filename pattern using text matching or regular expressions",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"title": "Filename pattern",
|
||||
"description": "Text or regex pattern to match against filename"
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"title": "Match type",
|
||||
"enum": ["contains", "startsWith", "exact", "regex"],
|
||||
"default": "contains",
|
||||
"description": "Type of pattern matching to perform"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Case sensitive",
|
||||
"description": "Whether matching should be case-sensitive"
|
||||
}
|
||||
},
|
||||
"required": ["pattern"]
|
||||
},
|
||||
"uiHints": ["filter"]
|
||||
},
|
||||
{
|
||||
"name": "filterFileType",
|
||||
"title": "Filter by file type",
|
||||
"description": "Filter assets by file type",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileTypes": {
|
||||
"title": "File types",
|
||||
"description": "Allowed file types",
|
||||
"type": "string",
|
||||
"array": true,
|
||||
"enum": ["image", "video"]
|
||||
}
|
||||
},
|
||||
"required": ["fileTypes"]
|
||||
},
|
||||
"uiHints": ["filter"]
|
||||
},
|
||||
{
|
||||
"name": "filterPerson",
|
||||
"title": "Filter by person",
|
||||
"description": "Filter by detected person",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"properties": {
|
||||
"personIds": {
|
||||
"type": "string",
|
||||
"array": true,
|
||||
"title": "Person IDs",
|
||||
"description": "List of person to match",
|
||||
"uiHint": "personI"
|
||||
},
|
||||
"matchAny": {
|
||||
"type": "boolean",
|
||||
"title": "Match any",
|
||||
"default": true,
|
||||
"description": "Match any name (true) or require all names (false)"
|
||||
}
|
||||
},
|
||||
"required": ["personIds"]
|
||||
},
|
||||
"uiHints": ["filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetArchive",
|
||||
"title": "Archive asset",
|
||||
"description": "Change asset visibility to archive",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"properties": {
|
||||
"inverse": {
|
||||
"title": "Inverse",
|
||||
"description": "When true will unarchive any archived assets",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetLock",
|
||||
"title": "Move to locked folder",
|
||||
"description": "Change visibility to locked",
|
||||
"types": ["AssetV1"]
|
||||
},
|
||||
{
|
||||
"name": "assetTimeline",
|
||||
"title": "Move to timeline",
|
||||
"description": "Change visibility to timeline",
|
||||
"types": ["AssetV1"]
|
||||
},
|
||||
{
|
||||
"name": "assetVisibility",
|
||||
"title": "Update visibility",
|
||||
"description": "Change visibility to selected option",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"properties": {
|
||||
"visibility": {
|
||||
"title": "Visibility",
|
||||
"description": "Asset visibility",
|
||||
"type": "string",
|
||||
"enum": ["archive", "timeline", "locked"]
|
||||
}
|
||||
},
|
||||
"required": ["visibility"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetFavorite",
|
||||
"title": "Favorite",
|
||||
"description": "Favorite an asset",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inverse": {
|
||||
"type": "boolean",
|
||||
"title": "Inverse",
|
||||
"description": "Unfavorite by default, set to true to favorite instead",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetAddToAlbums",
|
||||
"title": "Add to Album(s)",
|
||||
"description": "Add asset to selected albums",
|
||||
"types": ["AssetV1"],
|
||||
"hostFunctions": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"albumIds": {
|
||||
"type": "string",
|
||||
"title": "Album IDs",
|
||||
"array": true,
|
||||
"description": "Target album IDs",
|
||||
"uiHint": "albumId"
|
||||
}
|
||||
},
|
||||
"required": ["albumIds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "noop1",
|
||||
"title": "DEV: Nested properties",
|
||||
"description": "Example configuration with nested properties",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"number1": {
|
||||
"type": "number",
|
||||
"title": "Number 1",
|
||||
"description": "Basic number"
|
||||
},
|
||||
"number2": {
|
||||
"type": "number",
|
||||
"title": "Number 2",
|
||||
"array": true,
|
||||
"description": "List of numbers"
|
||||
},
|
||||
"string1": {
|
||||
"type": "string",
|
||||
"title": "String 1",
|
||||
"description": "Basic string"
|
||||
},
|
||||
"string2": {
|
||||
"type": "string",
|
||||
"title": "String 2",
|
||||
"array": true,
|
||||
"description": "List of strings"
|
||||
},
|
||||
"string3": {
|
||||
"type": "string",
|
||||
"title": "String 3",
|
||||
"enum": ["choice-1", "choice-2"],
|
||||
"description": "Select from a list"
|
||||
},
|
||||
"nested": {
|
||||
"type": "object",
|
||||
"title": "Nested",
|
||||
"description": "Nested properties for nesting",
|
||||
"properties": {
|
||||
"nested1": {
|
||||
"type": "string",
|
||||
"title": "Nested 1",
|
||||
"description": "Nested string"
|
||||
},
|
||||
"nested2": {
|
||||
"type": "number",
|
||||
"title": "Nested 2",
|
||||
"description": "Nested number"
|
||||
},
|
||||
"nested3": {
|
||||
"type": "object",
|
||||
"title": "Nested 3",
|
||||
"description": "Nested again",
|
||||
"properties": {
|
||||
"nested4": {
|
||||
"type": "boolean",
|
||||
"title": "Nested 4",
|
||||
"description": "Nested, nested boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "noop2",
|
||||
"title": "DEV: Album pickers",
|
||||
"description": "Example configuration with album pickers",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"properties": {
|
||||
"albumId": {
|
||||
"type": "string",
|
||||
"title": "Album ID",
|
||||
"description": "Target album ID",
|
||||
"uiHint": "albumId"
|
||||
},
|
||||
"albumIds": {
|
||||
"type": "string",
|
||||
"title": "Album IDs",
|
||||
"description": "Target album IDs",
|
||||
"array": true,
|
||||
"uiHint": "albumId"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[tasks.install]
|
||||
run = "pnpm install --frozen-lockfile"
|
||||
|
||||
[tasks.build]
|
||||
depends = ["install"]
|
||||
run = "pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build"
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@immich/plugin-core",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm build:tsc && pnpm build:wasm",
|
||||
"build:tsc": "tsc --noEmit && node esbuild.js",
|
||||
"build:wasm": "extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"@immich/plugin-sdk": "workspace:*",
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^6.0.0"
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// copy from
|
||||
// import '@immich/plugin-sdk/host-functions';
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
albumAddAssets(ptr: PTR): I64;
|
||||
addAssetsToAlbums(ptr: PTR): I64;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'main' {
|
||||
export function assetFileFilter(): I32;
|
||||
export function assetFavorite(): I32;
|
||||
export function assetVisibility(): I32;
|
||||
export function assetArchive(): I32;
|
||||
export function assetLock(): I32;
|
||||
export function assetTimeline(): I32;
|
||||
export function assetTrash(): I32;
|
||||
export function assetAddToAlbums(): I32;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { AssetStatus, AssetVisibility, WorkflowType, wrapper } from '@immich/plugin-sdk';
|
||||
|
||||
type AssetFileFilterConfig = {
|
||||
pattern: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
export const assetFileFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetFileFilterConfig>(({ data, config }) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
|
||||
const { asset } = data;
|
||||
|
||||
const fileName = asset.originalFileName || '';
|
||||
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
switch (matchType) {
|
||||
case 'contains': {
|
||||
return { workflow: { continue: searchName.includes(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'exact': {
|
||||
return { workflow: { continue: searchName === searchPattern } };
|
||||
}
|
||||
|
||||
case 'startsWith': {
|
||||
return { workflow: { continue: searchName.startsWith(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'regex': {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
return { workflow: { continue: regex.test(fileName) } };
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const assetFavorite = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
const target = config.inverse ? false : true;
|
||||
if (target !== data.asset.isFavorite) {
|
||||
return {
|
||||
changes: {
|
||||
asset: { isFavorite: target },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const assetVisibility = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { visibility: AssetVisibility }>(({ config }) => ({
|
||||
changes: { asset: { visibility: config.visibility } },
|
||||
}));
|
||||
};
|
||||
|
||||
export const assetArchive = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
if (!config.inverse && data.asset.visibility !== AssetVisibility.Archive) {
|
||||
return { changes: { asset: { visibility: AssetVisibility.Archive } } };
|
||||
}
|
||||
|
||||
if (config.inverse && data.asset.visibility === AssetVisibility.Archive) {
|
||||
return { changes: { asset: { visibility: AssetVisibility.Timeline } } };
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
export const assetLock = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
if (!config.inverse && data.asset.visibility !== AssetVisibility.Locked) {
|
||||
return { changes: { asset: { visibility: AssetVisibility.Locked } } };
|
||||
}
|
||||
|
||||
if (config.inverse && data.asset.visibility === AssetVisibility.Locked) {
|
||||
return { changes: { asset: { visibility: AssetVisibility.Timeline } } };
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
export const assetTrash = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => ({
|
||||
changes: {
|
||||
asset: config.inverse
|
||||
? { deletedAt: null, status: AssetStatus.Active }
|
||||
: { deletedAt: new Date(), status: AssetStatus.Trashed },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const assetAddToAlbums = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { albumIds: string[] }>(({ config, data, functions }) => {
|
||||
if (config.albumIds.length === 1) {
|
||||
functions.albumAddAssets(config.albumIds[0], [data.asset.id]);
|
||||
return {};
|
||||
}
|
||||
|
||||
functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [data.asset.id] });
|
||||
return {};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true, // Allow JavaScript files to be compiled
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true, // Enables compatibility with Babel-style module imports
|
||||
"lib": ["es2020"], // Specify a list of library files to be included in the compilation
|
||||
"module": "nodenext", // Specify module code generation
|
||||
"moduleResolution": "nodenext",
|
||||
"noEmit": true, // Do not emit outputs (no .js or .d.ts files)
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true, // Skip type checking of declaration files
|
||||
"strict": true, // Enable all strict type-checking options
|
||||
"target": "es2020", // Specify ECMAScript target version
|
||||
"types": ["./src/index.d.ts", "./node_modules/@extism/js-pdk"] // Specify a list of type definition files to be included in the compilation
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules" // Exclude the node_modules directory
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts" // Include all TypeScript files in src directory
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user