AviatoAviato
DeveloperPlugins

Plugin System

How Aviato plugins extend libraries, the ingestion pipeline, and the UI.

Aviato is a thin core wrapped around a plugin runtime. Almost everything that makes a media library useful (discovering files on disk, identifying them against TMDb or MusicBrainz, generating thumbnails, parsing .nfo sidecars, painting an item-detail page) ships as a plugin. Aviato orchestrates work; plugins do it.

This page is the entry point. For deeper topics see:

Anatomy of a plugin

Every plugin is a directory with at minimum:

my-plugin/
├── plugin.json     # manifest (the only file Aviato reads to discover the plugin)
├── package.json    # standard Bun/Node package metadata
└── src/index.ts    # entry point that calls createPlugin({...})

Plugins are loaded from the data directory at <data-dir>/plugins/. Each subdirectory with a valid plugin.json becomes a plugin.

Runtime model

Plugins run as separate processes, not in-process modules. This is deliberate: it isolates crashes, lets plugins use any runtime, and prevents a misbehaving plugin from blocking Aviato's event loop.

AspectDetail
TransportJSON-RPC 2.0 over stdio
Enginesbun (default), node, python, binary, declared in the manifest's engine field
Process modelOne process per plugin, regardless of how many capabilities it provides
DiscoveryAviato walks the plugin directory at startup, reads each plugin.json, and validates the manifest
Lifecyclediscovered → starting → running → stopping → stopped, with error as a sticky terminal state

Plugin authors interact with Aviato exclusively through the @aviato/plugin-sdk npm package.

What a plugin can do

A plugin exposes its functionality through two layers of the manifest.

Capabilities (capabilities: string[]) are long-lived, RPC-driven contracts Aviato calls into when it needs something done. Each capability is a named bundle of methods. Current set:

CapabilityWhat it doesMethod namespace
filesystemDiscovers, reads, watches, and (optionally) writes filesfilesystem.*
indexerIdentifies files against an external metadata source like TMDb or MusicBrainzindexer.*
libraryDefines a library type: its schema, entities, browse rules, and rendererlibrary.*

A single plugin can declare any combination of capabilities. library-tv is purely a library plugin; aviato-tmdb is purely an indexer; fs-local is a filesystem plugin. Nothing prevents a plugin from declaring ["filesystem", "indexer", "library"] and serving all three roles from one process.

Transcoding lives in Aviato's streaming pipeline, not in a plugin capability. There is no transcoder capability.

Subscriptions (subscriptions: { events, hooks, views }) are short-lived reactions to things happening in Aviato. A plugin with no capabilities at all is still useful if it subscribes to hooks. Most "auxiliary" plugins work this way: embedded-metadata, external-subtitles, posters, and thumbnails declare capabilities: [] and exist solely to mutate bundles during the probe phase.

See hooks.mdx for the full catalog.

Manifest essentials

{
  "id": "aviato-my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "What it does",
  "author": "Aviato",
  "license": "MIT",
  "engine": "bun",
  "entry": "src/index.ts",
  "aviato": { "minVersion": "0.1.0" },

  "capabilities": ["library"],
  "mediaTypes": ["movies"],

  "configuration": [
    { "key": "apiKey", "label": "API Key", "input": "text", "required": false }
  ],

  "capabilityConfig": {
    "library": { "bundling": { "strategy": "per-folder" }, "paths": [...] }
  },

  "subscriptions": {
    "events": ["library.item.updated"],
    "hooks": [{ "name": "pipeline.probe.afterProcess", "order": 40 }],
    "views":  ["ui.item.detail.actions"]
  }
}

Required fields: id (lowercase-kebab), name, version (semver), description, author, license, engine, entry, aviato.minVersion, capabilities (may be empty), and capabilityConfig for any capability that declares one.

mediaTypes accepts two forms:

  • Array form, like ["movies", "tv"]: a simple list.
  • Object form, like { "tv": { "episodic": true } }: lets a plugin attach per-mediaType config flags (episodic, seekable, idleProtection) consumed by the player and the host UI.

Aviato refuses to start any plugin whose manifest fails schema validation and surfaces the Zod error path in the admin UI.

Author surface (@aviato/plugin-sdk)

A single entry point, createPlugin, wires up everything.

import { createPlugin } from '@aviato/plugin-sdk'

const plugin = createPlugin({
  // capability handlers (only the ones declared in the manifest)
  library: { getSchema, getSortOptions, getFilterOptions, getGroupingOptions, getItemSummary, getItemDetail },
  filesystem: { validate, scan, watch?, unwatch? },
  indexer:    { supports, index, search, getMatchDetail },
})

// subscriptions
plugin.events.on('library.item.updated', payload => { /* fire-and-forget */ })
plugin.hooks.on('pipeline.probe.afterProcess', async ({ itemId, bundle }) => {
  return { /* BundleDelta */ }
})
plugin.views.on('ui.item.detail.actions', async ({ itemId, libraryType }) => {
  return [{ type: 'action', id: '...', slot: 'detail-actions', label: '...', rpcMethod: '...' }]
})

Capability handlers are request/response: Aviato calls them through JSON-RPC when it needs work done. Subscription handlers are reactive: Aviato pushes notifications and the plugin reacts.

The SDK is standalone, has no closed-source dependencies, and ships all of its types as Zod schemas. It can be vendored, audited, or extended without coupling to Aviato itself.

Working with plugins locally

# Plugin logs surface in /admin/plugins/<id>/logs

Plugin processes are crash-recovered with exponential backoff. After a configurable number of consecutive crashes Aviato trips a circuit breaker on that plugin and surfaces an error state in the admin UI.

See also

On this page