Plugin

HomeGallery can be extended via plugins. This feature is quite new and it is in an experimental state. Currently the plugin feature supports meta data extraction and database creator.

Visit Internals and the FAQ to get familiar with the basic architecture and design decisions if you plan to develop and fix as plugin. See also Development for full blown development.

In this section assumes that you setup the gallery with a gallery configuration correctly. Please visit Install section if not done yet.

Note

To develop a plugin it is recommended to run the gallery from sources via git or at least from the tar.gz ball. The tar.gz ball can be downloaded from dl.home-gallery.org/dist.

Note

Extending the web application is not yet supported. This feature is planed for the future. For the time being please visit Development to extend the web application.

Plugin Quickstart

1# Create a vanilla plugin
2./gallery.js plugin create --name acme
3# Trigger an import to extract and add plugin to the database
4./gallery.js --log trace run import
5# Check database with new plugin properties
6zcat ~/.config/home-gallery/database.db | jq .data[].plugin.acme

Disable built in plugins

Build in plugins can be disabled in the configuration gallery.config.yml.

1pluginManager:
2  disabled:
3    - geoAddressExtractor
4  disabledExtractors:
5    - video
6  disabledDatabaseMappers:
7    - objectMapper
8    - facesMapper

See ./gallery plugin ls to list available plugins to deactivate. Use --long argument to see detailed list

 1./gallery.js plugin ls
 2List Plugins: 12 available
 3- metaExtractor v1.0.0
 4- imageResizeExtractor v1.0.0
 5- heicPreviewExtractor v1.0.0
 6- embeddedRawPreviewExtractor v1.0.0
 7- imagePreviewExtractor v1.0.0
 8- videoFrameExtractor v1.0.0
 9- videoPosterExtractor v1.0.0
10- vibrantExtractor v1.0.0
11- geoAddressExtractor v1.0.0
12- aiExtractor v1.0.0
13- videoExtractor v1.0.0
14- baseMapper v1.0.0

After a plugin has been deactivated the database needs to be rebuilt to apply changes.

1# Rebuild database to apply disabled plugins
2./gallery.js database

Plugin Structure

The plugin defines basic information and an entry point.

The entry file must export an object with name, version property and an initialize function. Optionally a requires array can define dependencies to other plugins. The dependency can contain also a semantic version like other@1.2.3.

The asynchronous initialize function is called with the plugin manager and must return an array with different plugin modules.

 1const factory = async manager => {
 2  const log = manager.createLogger('plugin.acme.factory')
 3  log.trace(`Initialize plugin factory`)
 4
 5  return {
 6    getExtractors() {
 7      return []
 8    },
 9    getDatabaseMappers() {
10      return []
11    },
12    getQueryPlugins() {
13      return []
14    },
15  }
16}
17
18const plugin = {
19  name: 'Acme Plugin',
20  version: '1.0',
21  requires: [],
22  async initialize(manager) {
23    const log = manager.createLogger('plugin.acme')
24    log.trace(`Initialize Acme plugin`)
25
26    return factory(manager)
27  }
28}
29
30export default plugin

The manager offers access to the gallery config and the context properties and can create logger instances.

1type TManager = {
2  getApiVersion(): string
3  getConfig(): TGalleryConfig
4  createLogger(module: string): TLogger
5  getContext(): TGalleryContext
6}

Plugins can store properties and objects in the context, namespaeced by plugin.<pluginName>.

Extractor plugin

A extractor creates meta information from the original file or form other extractor files.

As example: The Exif extractor reads the image and provides exif data as meta data to the storage. The geo reverse plugin reads the exif meta data, requests the address from a remote service and stores these address as further meta information.

Further example: The image resizer reads the image and stores the preview file in the storage. The AI extractor reads a small preview image, sends it to the api service and stores similarity vectors as new meta data.

The extractor has following phases

  1. meta

  2. raw

  3. file

The meta phase reads basic meta data from files for each file.

The raw phase receives a file grouped by sidecars and can extract images from raw files. The assumption is that a raw file extraction is expensive and should only be executed if no image sidecar is available.

The file phase is called again for each file (sidecar files are flatten again).

Therefore the extracor object has a name and phase property and a create function. The async create function returns:

  • a extractor function (entry) => Promise<void> or

  • a task object with optional test?: (entry) => boolean, a required task: (entry) => Promise<void> and optional end: () => Promise<void> function or

  • a stream Transform object

 1const factory = async manager => {
 2  const acmeExtractor = await extractor(manager)
 3  return {
 4    getExtractors() {
 5      return [extractor]
 6    },
 7    // ...
 8  }
 9}
10
11const extractor = manager => ({
12  name: 'acmeExtractor',
13  phase: 'file',
14
15  async create(storage) {
16    const pluginConfig = manager.getConfig().plugin?.acme || {}
17    // plugins can provide properties or functions on the context
18    const suffix = 'acme.json'
19
20    const created = new Date().toISOString()
21    const value = 'Acme'
22    // Read property from plugin's configuration plugin.acme.property for customization
23    const property = pluginConfig.property || 'defaultValue'
24
25    const log = manager.createLogger('plugin.acme.extractor')
26    log.debug(`Creating Acme extractor task`)
27
28    return {
29      test(entry) {
30        // Execute task if the storage file is not present
31        return !storage.hasFile(entry, suffix)
32      },
33      async task(entry) {
34        log.debug(`Processing ${entry}`)
35        const data = { created, value, property }
36        // Write plugin data to storage. Data can be a buffer, string or object
37        return storage.writeFile(entry, suffix, data)
38      }
39    }
40  }
41
42})

The storage object has functions to read data from and write data to the object storage.

 1type TStorage = {
 2  // Evaluates if the entry has given storage file
 3  hasFile(entry, suffix): boolean
 4  // Reads a file from the storage
 5  readFile(entry, suffix): Promise<Buffer | any>
 6  // Write a extracted data to the storage.
 7  //
 8  // If the suffix ends on `.json` or `.json.gz` the data is automatically serialized and compressed.
 9  // The storage file is added to the entry `.files` array and the json data is added to the `.meta` object
10  writeFile(entry, suffix, data): Promise<void>
11  // Copy a local file to the storage
12  copyFile(entry, suffix, file): Promise<void>
13  // Creates a symbolic link from a local file
14  symlink(entry, suffix, file): Promise<any>
15  // Removes a file from the storage directory
16  removeFile(entry, suffix): Promise<any>
17  // Creates a local file handle new or existing storage files.
18  //
19  // The file handle should be committed or released after usage
20  createLocalFile(entry, suffix): Promise<TLocalStorageFile>
21  // Create local directory to create files
22  createLocalDir(): Promise<TLocalStorageDir>
23}

Database plugin

The database plugin maps important meta data from the extrator to a database entry.

The mapping is synchronous. Asynchronous stuff belongs to the extractor.

 1const factory = async manager => {
 2  const acmeDatabaseMapper = databaseMapper(manager)
 3  return {
 4    getDatabaseMappers() {
 5      return [acmeDatabaseMapper]
 6    },
 7    // ...
 8  }
 9}
10
11const databaseMapper = manager => ({
12  name: 'acmeMapper',
13  order: 1,
14
15  mapEntry(entry, media) {
16    const log = manager.createLogger('plugin.acmeMapper')
17    log.info(`Map database entry: ${entry}`)
18
19    // Use somehow the data from the extractor task
20    media.plugin.acme = entry.meta.acme
21  }
22
23})

Query plugin

The query plugin extends the query language and can extend the simple text search, manipulates the query abstract syntax tree (AST) or defines new comparator keys.

The textFn function extends the simple text search if only an identifier is search.

The transformRules array contains rules of query AST manipulation on top to down traversal. A transform rule can create new query AST nodes to insert expressions.

After the AST is build, the filter and sort function is created from bottom to top. The queryHandler is called in a chain if a comparison or function key could not be resolved. If the plugin can handle the query AST node it must return true to skip further chain evaluation.

 1const factory = async manager => {
 2  const acmeQueryPlugin = queryPlugin(manager)
 3  return {
 4    getQueryPlugins() {
 5      return [acmeQueryPlugin]
 6    },
 7    // ...
 8  }
 9}
10
11const queryPlugin = manager => ({
12  name: 'acmeQuery',
13  order: 1,
14  textFn(entry) {
15    return entry.plugin.acme?.value || ''
16  },
17  transformRules: [
18    {
19      transform(ast, queryContext) {
20        return ast
21      }
22    }
23  ],
24  queryHandler(ast, queryContext) {
25    // Create filter on acme keyword in condition to support 'acme = value' or 'acme:value'
26    if (ast.type == 'cmp' && ast.key == 'acme' && ast.op == '=') {
27      ast.filter = (entry) => {
28        return entry.plugin?.acme?.value == ast?.value?.value
29      }
30      // The ast node could be handled. Return true to prevent further chain calls
31      return true
32    }
33
34    // Create custom sort key 'order by acme'
35    if (ast.type == 'orderKey' && ast.value == 'acme') {
36      ast.scoreFn = e => e.plugin.acme.created || '0000-00-00'
37      ast.direction = 'desc'
38      return true
39    }
40
41    // Check ast and return if ast node can be resolved
42    return false
43  }
44}

See the query package for further details with debug.js script to evaluate and inspect the query AST.

 1$ cd packages/query/
 2$ ./debug.js
 3debug.js [ast|traverse|transform|transformStringify|stringify] query
 4$ ./debug.js 'year >= 2024 not tag:trashed'
 5{
 6"type": "query",
 7"value": {
 8  "type": "terms",
 9  "value": [
10  {
11    "type": "cmp",
12    "key": "year",
13    "op": ">=",
14    "value": {
15    "type": "comboundValue",
16    "value": "2024",
17    "col": 9
18    },
19    "col": 1
20  },
21  {
22    "type": "not",
23    "value": {
24    "type": "keyValue",
25    "key": "tag",
26    "value": {
27      "type": "identifier",
28      "value": "trashed",
29      "col": 22
30    },
31    "col": 18
32    },
33    "col": 14
34  }
35  ],
36  "col": 1
37},
38"col": 1
39}