Hooks, Events, and Views
Subscription types a plugin can register, with the full catalog of names, payloads, and contracts.
Capabilities are pull: Aviato calls into a plugin when it needs something. Subscriptions are push: Aviato emits a signal and any plugin that has declared interest reacts.
Three subscription types exist, each with a distinct execution contract:
| Type | Purpose | Execution | Timeout | Retry | Plugin returns |
|---|---|---|---|---|---|
| Events | Fire-and-forget notifications | Parallel | 5 s | No | Nothing |
| Hooks | Pipeline / server blocking | Sequential, ordered | 30 s | 1 retry | A delta or null |
| Views | UI extension points | Parallel, accumulate | 6 s | No | UISchema[] |
Pick the right one:
- Need to react to something? Use an event.
- Need to change something Aviato is about to commit? Use a hook.
- Need to add UI to a page? Use a view.
Naming
All names are dot-separated lowercase: resource.action or
resource.sub.action. Wildcards match exactly one segment:
library.item.* matches library.item.added but not
library.item.added.bulk.
The regex enforced by manifest validation:
^[a-z0-9*]+(\.[a-z0-9*]+)*$.
Declaring subscriptions
In plugin.json:
"subscriptions": {
"events": ["library.item.updated", "library.item.removed"],
"hooks": [
{ "name": "pipeline.probe.afterProcess", "order": 40, "requiresLocalFile": true },
{ "name": "pipeline.index.afterProcess", "order": 40 }
],
"views": ["ui.item.detail.actions"]
}A hook entry's full shape is { name, order?, requiresLocalFile? }.
order is an integer in [0, 100] (default 50).
requiresLocalFile is a boolean (default false) covered in
requiresLocalFile below.
In src/index.ts:
import { createPlugin } from '@aviato/plugin-sdk'
const plugin = createPlugin({ /* capability handlers, if any */ })
plugin.events.on('library.item.updated', async (payload) => {
console.log('Item updated:', payload.itemId)
})
plugin.hooks.on('pipeline.probe.afterProcess', async ({ itemId, bundle }) => {
return { ids: { imdb: { id: 'tt1234567', confidence: 0.9 } } }
})
plugin.views.on('ui.item.detail.actions', async ({ itemId, libraryType }) => {
return [{
type: 'action',
id: 'download-subs',
slot: 'detail-actions',
label: 'Download Subtitles',
icon: 'subtitles',
rpcMethod: 'subtitles.download',
}]
})The SDK lazily registers event.dispatch, hook.dispatch, and
view.dispatch RPC methods the first time you call .on() for
that type, and routes incoming dispatches to the right handler by
name (with wildcard matching on the plugin side).
A subscription declared in the manifest but not registered with
.on() is a soft error: Aviato will dispatch and the plugin will
respond "unknown handler".
Events
Fire-and-forget. Aviato emits, the event bridge fans out to every subscribed plugin in parallel with a 5 s timeout, and nothing is awaited. Use events for analytics, notifications, and side-effect mirrors. Never use them for anything the rest of the system needs to wait on.
Catalog
The complete event list is exported as ALL_SERVER_EVENT_TYPES
from @aviato/plugin-sdk. Current set:
| Event | When |
|---|---|
library.scan.started | Library scan begins |
library.scan.progress | Periodic scan progress update |
library.scan.completed | Scan finishes |
library.item.added | New item committed to a library |
library.item.updated | Item fields changed |
library.item.removed | Item deleted |
library.item.enriched | Item received new metadata from an indexer |
library.created, library.updated, library.deleted | Library-level CRUD |
job.started, job.progress, job.completed, job.failed | Pipeline job lifecycle |
transcode.started, transcode.progress, transcode.stopped | Streaming session lifecycle |
plugin.discovered, plugin.started, plugin.stopped, plugin.restarted, plugin.error | Plugin lifecycle |
notification.created | Server notification surface |
playback.progress, playback.completed, playback.removed | Playback session updates |
scheduler.started | Job scheduler boot |
pipeline.item.discovered, pipeline.item.stateChanged, pipeline.item.completed | Per-item pipeline progress |
pipeline.step.started, pipeline.step.completed | Per-step pipeline progress |
admin.pipeline.jobCreated, jobUpdated, jobCompleted, jobFailed, taskUpdated, jobDetailUpdated | Admin dashboard feed |
server.shutdown | Aviato is shutting down |
Payload shape varies by event. Aviato emits each event with a
{ type, timestamp, data } envelope; the data object's exact
fields depend on the event type and are documented inline in the
emit sites.
Hooks
Blocking. Run sequentially in order ascending (0–100, default
50). Each plugin returns a delta Aviato merges into the input
before passing it to the next subscriber.
A null return is the pass-through: equivalent to not modifying
the bundle. Throwing or timing out triggers one retry, then
Aviato gives up on that subscriber and continues.
requiresLocalFile (planned)
Per-item pipeline hooks (pipeline.probe.afterProcess,
pipeline.index.afterProcess, future pipeline.item.beforeActivate)
operate on a Bundle whose media files are referenced by uri.
Those URIs come from the library's filesystem plugin and may not be
local paths — aviato-fs-local returns file:// URIs that already
resolve to disk, but a Dropbox, S3, or SMB filesystem plugin returns
provider-scoped URIs that point at remote bytes.
Some hook handlers can work entirely from bundle metadata (the
indexer's IDs, container-level fields, scan results). Others need to
read the actual bytes from a real on-disk path: shelling out to
ffprobe, parsing a sidecar .nfo, generating thumbnails with
FFmpeg, hashing the file for fingerprinting.
requiresLocalFile is how a plugin declares that requirement
per-hook:
"subscriptions": {
"hooks": [
{ "name": "pipeline.probe.afterProcess", "order": 90, "requiresLocalFile": true }
]
}The intended orchestrator behavior — declared in the manifest schema and persisted by the subscription registry, but not yet wired into the ingestion pipeline — is:
- The orchestrator collects every subscriber's flag for a given item before dispatching its hook chain.
- If any subscriber requires a local file, Aviato materializes
the file once before the chain runs: preferring the source
filesystem plugin's
getLocalPath(whencapabilityConfig.filesystem.supportsLocalFileAccessistrue) and falling back to fetching the file into a scratch directory it cleans up after the pipeline completes. - If no subscriber sets the flag, Aviato skips materialization entirely and the item ingests without ever copying bytes locally — important for items sourced from remote filesystem plugins where transfer is slow or metered.
The materialized path is exposed as localPath on each
BundleMediaFile (see bundle.mdx). Until the
orchestrator wiring lands, plugins that already check localPath
continue to work unchanged for aviato-fs-local-backed libraries
(where localPath is populated during scan); declaring
requiresLocalFile: true now is forward-compatible and lets
authors ship plugin manifests ahead of the runtime.
Set the flag per-hook, not per-plugin: a plugin that subscribes to
both pipeline.probe.afterProcess (needs the file) and
pipeline.index.afterProcess (works from IDs alone) should set
requiresLocalFile: true only on the first.
A subscription that does not need on-disk bytes should leave the
flag unset. The default is false and overusing the flag forces
unnecessary copies for remote filesystems.
Catalog
Three pipeline hooks are dispatched today:
pipeline.scan.prepare
Dispatched once per library at the start of a scan, before file walking. Plugins return scan-shaping hints.
- Input:
{ libraryType: string, fsPlugin: string }. - Output (delta):
{ extensions?: string[], patterns?: string[] }, for extra extensions or glob patterns to include in the scan.
pipeline.probe.afterProcess
Dispatched after the probe phase (typically after ffprobe)
finishes for a library item. This is where most "auxiliary"
plugins do their work: embedded-metadata, external-subtitles,
posters, thumbnails, NFO readers.
- Input:
{ itemId: string, bundle: Bundle }. The in-flight bundle carries media files, ffprobe results, and any deltas applied by earlier subscribers in the chain. - Output (
BundleDelta | null): any partial bundle to merge, typicallyassets,subtitles,auxiliaryfiles,ids, orfields.
Recommended order ranges:
| Range | Use for |
|---|---|
| 10–29 | Container-level extraction (read once, others depend on it) |
| 30–49 | Sidecar / external-file readers (.nfo, posters, .srt) |
| 50–69 | Cross-referencing plugins that need others' output |
| 70–100 | Synthesis / fallback plugins (e.g. thumbnails, only if no artwork) |
Examples from the bundled set: embedded-metadata 20,
external-metadata 30, posters 40, external-subtitles 50,
thumbnails 90.
pipeline.index.afterProcess
Dispatched after an indexer plugin returns identification results, before the item is persisted.
- Input:
{ itemId: string, bundle: Bundle }. The bundle now carries identification IDs, canonical fields, and entity payloads. - Output (
BundleDelta | null): further enrichment.
Use this hook when you want to react after identification is
settled (for example, fetching related artwork once a TMDb ID
exists). Use pipeline.probe.afterProcess when you want to
influence what the indexer sees.
pipeline.item.beforeActivate (planned)
Listed in the design spec but not yet dispatched. Will let
plugins veto activation of an item with { activate: boolean }.
Views
Parallel UI extension points. Web (and future TV / mobile)
clients hit GET /api/plugins/views/:slot?context=.... Aviato
fans out to every subscribed plugin in parallel with a 6 s
timeout and concatenates each plugin's UISchema[] return into
one response.
Views never return JSX. They return declarative schemas that the host renders with platform-native components. That's how the same plugin can extend the web app, an Apple TV app, and a mobile app from one implementation.
View slots (catalog)
| Name | Context | Used for |
|---|---|---|
ui.item.detail.actions | { itemId, libraryType } | Action buttons in the item detail header |
ui.item.detail.panels | { itemId, libraryType } | Below-the-fold panels on the item detail page |
ui.library.header.actions | { libraryId } | Action buttons on the library header |
ui.player.overlays | { itemId, sessionId } | Overlays rendered on top of the player |
ui.settings.sections | {} | Extra sections on the settings page |
Plugins may emit any of three UISchema shapes:
{ type: 'form', slot, id, title, fields: [...] }: a settings form.{ type: 'metadata', slot, id, title, layout, fields: [...] }: a read-only metadata block ('key-value' | 'list' | 'chips' | 'gallery').{ type: 'action', slot, id, label, icon?, confirm?, rpcMethod }: a button that, when clicked, calls back through Aviato to invokerpcMethodon the plugin.
Slot IDs are exported from @aviato/plugin-sdk as SlotIdSchema:
sidebar-discover, sidebar-bottom, header-actions,
hero-banner, home-sections, detail-actions, detail-metadata,
detail-sections, player-overlay, settings-panes,
command-palette, routes.
The Bundle / BundleDelta data shape
Hooks operate on Bundle, the in-flight data structure that
represents an item being ingested. Top-level shape (exported from
@aviato/plugin-sdk):
{
files: {
media: BundleMediaFile[], // primary, trailer, extra, ...
auxiliary: BundleAuxiliaryFile[]
},
fields: Record<string, unknown>, // typed per the library's itemSchema
ids: Record<string, IdValue>, // canonical IDs (imdb, tmdb, mbid, ...)
assets: BundleAsset[], // artwork: poster, fanart, banner, ...
subtitles: BundleSubtitle[], // external + embedded subtitle streams
entities: { ... }, // show, season, person, referenced by items
}BundleDelta is a partial of Bundle. Returning
{ assets: [{ type: 'poster', uri: '...', source: 'aviato-posters' }] }
adds one poster. The dispatcher merges by collection (assets,
subtitles, entities, and chapters append; fields and
ids shallow-merge).
See also
- Plugin system overview
- Library capability defines the
fieldsandentitiesshape that hooks populate. - Bundle covers the full data shape and merge semantics.
- The subscription builders and
matchPatternwildcard matcher used byplugin.events.on/hooks.on/views.onare exported from@aviato/plugin-sdkfor advanced use cases.
Filesystem Capability
How a plugin discovers files for libraries to ingest, and optionally watches for changes and exposes local paths.
UI Schemas
How plugins extend the UI by returning declarative schemas (forms, metadata blocks, and action buttons) that Aviato renders with platform-native components.