Skip to content

Plugin events & hooks ​

Envelope ​

Every handler receives a frozen envelope merged with the event-specific payload:

ts
interface PluginEventEnvelope {
  eventId: string;
  payloadVersion: 1;
  occurredAt: string;   // ISO 8601
  correlationId: string;
}

host.events.on requires the eventHandler capability:

ts
host.events.on("media.file.ready", ({ libraryId, mediaFileId, metadataId }) => { /* … */ });

Event map ​

PluginEventDataMap — 21 events across eight groups.

Server note: media.file.identified is emitted when an ingest matches a new media file to metadata (status: "matched"), and media.file.unavailable when a scan removes files that disappeared from disk. The remaining contract events are all live.

Plugin ​

EventPayload
plugin.enabledpluginId
plugin.disabledpluginId

Library ​

EventPayload
library.scan.startedlibraryId, scanId
library.scan.completedlibraryId, scanId, errors

Media files ​

EventPayload
media.file.discoveredlibraryId, mediaFileId
media.file.readylibraryId, mediaFileId, metadataId
media.file.unavailablelibraryId, mediaFileId
media.file.identifiedmediaFileId, metadataId, status: "matched" | "unmatched" | "ignored"
media.file.technical-data-updatedmediaFileId, size, sourceMtimeMs, audioChanged

Metadata ​

EventPayload
metadata.search.requestedtype: "movie" | "tv_show", title, year?
metadata.savedmetadataId
metadata.refreshedmetadataId

Artifacts ​

EventPayload
artifact.createdmediaFileId, artifactId, artifactType

Markers ​

EventPayload
media.markers.updatedmediaFileId, markerCount

Playback ​

EventPayload
playback.session.startedsessionId, mediaFileId
playback.session.endedsessionId, mediaFileId, reason?
playback.lifecycle.startedsessionId, userId?, profileId, mediaFileId, mode: "direct-stream" | "transcode", videoCodec, audioCodec, videoBitrateKbps, audioStreamIndex, startedAt
playback.lifecycle.progressprofileId, mediaFileId, position, duration, progressPercent, audioStreamIndex?, subtitleId?, updatedAt
playback.lifecycle.stoppedsessionId, mediaFileId, reason, stoppedAt
playback.progress.updatedprofileId, mediaFileId, position, duration, completed, audioStreamIndex?, subtitleId?

Notifications ​

EventPayload
notification.creatednotificationId, userId, profileId?, type, sourcePluginId?

Helper exports ​

ts
import {
  createPluginEventPayload,
  type PluginEventDataMap,
  type PluginEventEnvelope,
  type PluginEventName,
  type PluginEventMap,
  type PluginEventHandler,
} from "@reelvault/sdk/plugin";

createPluginEventPayload builds the same frozen envelope production uses; the testing host calls it when you host.emit(...).

Hooks ​

Hooks intercept core pipelines before they complete. A handler may return a transformed candidate, return undefined to skip the operation, or throw a rejection.

ts
import { rejectPluginHook } from "@reelvault/sdk/plugin";

host.hooks.beforeMetadataSave(({ candidate }) => {
  if (isJunk(candidate)) return undefined;                    // skip this save
  return { ...candidate, title: cleanup(candidate.title) };    // transform
});
HookContextPipeline
beforeMediaRecognition{ candidate: { type, title, year?, season?, episode? } }filename → title recognition
beforeMetadataSave{ candidate: MetadataCandidate }metadata import/save
beforeArtifactCreate{ candidate: { mediaFileId, kind, contentType, size } }artifact writes
ts
class PluginHookRejection extends Error {}
function rejectPluginHook(message: string): never;

rejectPluginHook stops the pipeline without counting it as a plugin failure — use it for deliberate vetoes.

See also ​

Released under the GNU GPL v3.