AviatoAviato
DeveloperPlugins

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:

TypePurposeExecutionTimeoutRetryPlugin returns
EventsFire-and-forget notificationsParallel5 sNoNothing
HooksPipeline / server blockingSequential, ordered30 s1 retryA delta or null
ViewsUI extension pointsParallel, accumulate6 sNoUISchema[]

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:

EventWhen
library.scan.startedLibrary scan begins
library.scan.progressPeriodic scan progress update
library.scan.completedScan finishes
library.item.addedNew item committed to a library
library.item.updatedItem fields changed
library.item.removedItem deleted
library.item.enrichedItem received new metadata from an indexer
library.created, library.updated, library.deletedLibrary-level CRUD
job.started, job.progress, job.completed, job.failedPipeline job lifecycle
transcode.started, transcode.progress, transcode.stoppedStreaming session lifecycle
plugin.discovered, plugin.started, plugin.stopped, plugin.restarted, plugin.errorPlugin lifecycle
notification.createdServer notification surface
playback.progress, playback.completed, playback.removedPlayback session updates
scheduler.startedJob scheduler boot
pipeline.item.discovered, pipeline.item.stateChanged, pipeline.item.completedPer-item pipeline progress
pipeline.step.started, pipeline.step.completedPer-step pipeline progress
admin.pipeline.jobCreated, jobUpdated, jobCompleted, jobFailed, taskUpdated, jobDetailUpdatedAdmin dashboard feed
server.shutdownAviato 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 (when capabilityConfig.filesystem.supportsLocalFileAccess is true) 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, typically assets, subtitles, auxiliary files, ids, or fields.

Recommended order ranges:

RangeUse for
10–29Container-level extraction (read once, others depend on it)
30–49Sidecar / external-file readers (.nfo, posters, .srt)
50–69Cross-referencing plugins that need others' output
70–100Synthesis / 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)

NameContextUsed 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 invoke rpcMethod on 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 fields and entities shape that hooks populate.
  • Bundle covers the full data shape and merge semantics.
  • The subscription builders and matchPattern wildcard matcher used by plugin.events.on / hooks.on / views.on are exported from @aviato/plugin-sdk for advanced use cases.

On this page