Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rivenmedia/riven-ts/llms.txt

Use this file to discover all available pages before exploring further.

Riven’s internal communication is entirely event-driven. Every stage of the media pipeline — from bootstrapping the server to delivering a stream link — emits a typed RivenEvent that plugins can subscribe to via the hooks field of their RivenPlugin definition. Each event is defined as a Zod discriminated union member, providing full runtime validation on both emission and handling.
Hook handlers are matched by the event’s type string. Each handler receives { event, dataSources, settings, logger } — where event contains the payload fields (without the type discriminant). Riven validates the return value against the corresponding response schema before continuing.

ProgramEvent Type Helper

All internal events are built with the ProgramEvent<Type, Payload> generic. The type discriminant is always prefixed with riven.; the Payload object is merged directly into the top-level event shape.
import type { ProgramEvent } from "@repo/util-plugin-sdk";

// Construct a typed event manually
type MyEvent = ProgramEvent<"my-plugin.thing.happened", { id: string }>;
// => { type: "riven.my-plugin.thing.happened"; id: string }
Use the companion ParamsFor<T> helper to extract just the payload fields from a known event type, stripping the type discriminant:
import type { ParamsFor, RivenEvent } from "@repo/util-plugin-sdk";

type ScrapePayload = ParamsFor<
  Extract<RivenEvent, { type: "riven.media-item.scrape.requested" }>
>;
// => { item: MediaItemInstance }

Lifecycle Events

These events fire at the boundaries of the Riven process lifetime.
Emitted once after Riven has fully bootstrapped — all plugins registered, data sources initialised, and the GraphQL schema compiled.Payload: (none)When it fires: Immediately after the server is ready to accept connections.
hooks: {
  "riven.core.started": async ({ logger }) => {
    logger.info("Riven has started");
  },
}
Emitted when Riven begins its graceful shutdown sequence. Use this hook to flush caches, close connections, or perform any other cleanup.Payload: (none)When it fires: On SIGTERM / SIGINT before the process exits.
hooks: {
  "riven.core.shutdown": async ({ logger }) => {
    logger.info("Shutting down");
    await myClient.disconnect();
  },
}

Content Service Event

Emitted on a schedule to poll content-source plugins (e.g. watchlist providers) for new items to add to the library.Payload: (none — the plugin is expected to return data)Response schema:
{
  movies: Array<{
    imdbId?: string | null;
    tmdbId?: string | null;
    externalRequestId?: string;
    requestedBy: string | null;
  }>;
  shows: Array<{
    imdbId?: string | null;
    tmdbId?: string | null;
    tvdbId?: string | null;
    seasons: number[] | null;
    externalRequestId?: string;
    requestedBy: string | null;
  }>;
  updateIntervalSeconds: number | null;
}
Return updateIntervalSeconds: null to use Riven’s default polling interval, or a non-negative integer to override it dynamically.
hooks: {
  "riven.content-service.requested": async ({ dataSources, settings }) => ({
    movies: [{ imdbId: "tt0468569" }],
    shows: [{ tvdbId: "81189", seasons: [1, 2] }],
    updateIntervalSeconds: 3600,
  }),
}

Item Request Events

Events in this group track the lifecycle of an ItemRequest — the record created when a user or content service asks Riven to acquire a piece of media.
Fired after a new ItemRequest row has been successfully persisted.Payload:
{ item: ItemRequestInstance }
Fired when persisting a new ItemRequest fails for a general reason.Payload:
{
  item: Omit<ItemRequest, "id">;
  error: unknown;
}
Fired when the requested media item already exists in the library (duplicate imdbId / tmdbId / tvdbId).Payload:
{ item: ItemRequest }
Fired after an ItemRequest and all its associated MediaItem records have been deleted.Payload:
{ item: ItemRequest }
Fired after an ItemRequest has been updated — for example when additional seasons are requested for an existing show.Payload:
{ item: ItemRequestInstance }

Indexing Events

Indexing is the first processing stage: Riven emits a request event carrying external IDs and expects an indexer plugin to return rich metadata (title, genres, seasons, episodes, etc.).
Requests indexing metadata for a newly created movie item.Payload:
{ item: ItemRequestInstance }
Response schema (return from hook):
{
  item: {
    id: string;           // UUID
    type: "movie";
    title: string;
    imdbId: string | null;
    releaseDate: string | null;  // ISO 8601 datetime
    contentRating: MovieContentRating;
    runtime: number | null;
    genres: string[];
    rating?: number | null;
    posterUrl?: string | null;
    country?: string | null;
    language?: string | null;
    aliases?: Record<string, string[]> | null;
  };
} | null
Return null when no indexing was performed (e.g. item already indexed).
Requests indexing metadata for a newly created show item.Payload:
{ item: ItemRequestInstance }
Response schema (return from hook):
{
  item: {
    id: string;           // UUID
    type: "show";
    title: string;
    imdbId: string | null;
    contentRating: ShowContentRating;
    network: string | null;
    status: ShowStatus;
    genres: string[];
    rating?: number | null;
    posterUrl?: string | null;
    country?: string | null;
    language?: string | null;
    aliases?: Record<string, string[]> | null;
    seasons: Record<number, {
      number: number;
      title: string | null;
      episodes: Array<{
        contentRating: ShowContentRating;
        absoluteNumber: number;
        number: number;
        title: string;
        posterPath?: string | null;
        airedAt: string | null;  // ISO 8601
        runtime: number | null;
      }>;
    }>;
  };
} | null
Fired after the index response has been validated and persisted to the database.Payload:
{ item: MovieInstance | ShowInstance }
Fired when persisting the indexer response fails.Payload:
{
  item: ItemRequestInstance;
  error: unknown;
}
Fired when an index request is attempted for an ItemRequest that has already been indexed (state machine guard).Payload:
{ item: ItemRequest }

Scraping Events

Scraping is the second pipeline stage: Riven emits a request for torrent/magnet links for an indexed MediaItem and expects a scraper plugin to return a map of infoHash → magnet pairs.
Requests stream candidates for an indexed media item.Payload:
{ item: MediaItemInstance }
Response schema (return from hook):
{
  id: string;                              // UUID of the media item
  results: Record<string, string>;         // infoHash → magnet URI (non-empty values)
}
hooks: {
  "riven.media-item.scrape.requested": async ({ event }) => ({
    id: event.item.id,
    results: {
      "da39a3ee5e6b4b0d3255bfef95601890afd80709": "magnet:?xt=urn:btih:...",
    },
  }),
}
Fired after scraped stream candidates have been persisted.Payload:
{ item: MediaItemInstance }
Fired when persisting scrape results fails.Payload:
{
  item: MediaItemInstance;
  error: unknown;
}
Fired when a scrape is attempted for an item in an incompatible state.Payload:
{ item: MediaItemInstance }
Fired when the scraper returns zero stream candidates.Payload:
{
  item: MediaItemInstance;
  error: unknown;
}

Downloading Events

After a stream is selected, Riven hands off to a downloader plugin (typically a debrid service) via these events.
Requests the download of a specific torrent from a debrid provider.Payload:
{
  infoHash: string;         // SHA-1 hash
  provider: string | null;
}
Response schema:
// Success
{ success: true; data: { torrentId: string; files: DebridFile[] } }
// Failure
{ success: false; statusCode: number }
Requests a bulk cache check for a list of info hashes before attempting any downloads. Allows downloaders to quickly filter out uncached torrents.Payload:
{
  infoHashes: string[];     // array of SHA-1 hashes (min 1)
  provider: string | null;
}
Response schema:
// Map of infoHash → cached files (empty array = not cached)
Record<string, DebridFile[]>
Fired when all required files for a media item have been successfully downloaded.Payload:
{
  item: MediaItemInstance;
  downloader: string;
  durationMs: number;
  provider: string | null;
}
Fired when a show or season has been partially downloaded (some episodes complete, others pending).Payload:
{
  item: MediaItemInstance;
  downloader: string;
}
Fired when a download attempt fails.Payload:
{
  item: MediaItemInstance;
  error: unknown;
}
Fired when a download is attempted for an item in an incompatible state (e.g. already downloaded).Payload:
{ item: MediaItemInstance }
Requests the list of provider names supported by a downloader plugin, along with any currently rate-limited providers.Payload: (none)Response schema:
{
  providers: string[];
  rateLimitedProviders: Record<string, number>;  // provider → retry-after seconds (default {})
}

Streaming Events

Once a MediaEntry has a downloadUrl, Riven can request a permanent or expiring stream link from the plugin that created it.

Subtitle Event

Requests subtitle files for a downloaded media item.Payload:
{ item: MediaItemInstance }
Response schema:
{
  subtitles: Array<{
    language: string;
    content: string;
    fileHash: string;
    fileSize: number;
    sourceProvider: string;
    sourceId?: string;
  }>;
}

External Event

External events are emitted into Riven from outside — typically by a GraphQL mutation or a content-service callback — rather than being emitted by the core pipeline. They use the riven-external. prefix instead of riven..
Triggers a new item request from outside Riven. Send this via ctx.sendEvent() inside a resolver.Payload:
{
  item:
    | { type: "movie"; imdbId?: string | null; tmdbId?: string | null; externalRequestId?: string; requestedBy: string | null }
    | { type: "show";  imdbId?: string | null; tmdbId?: string | null; tvdbId?: string | null; seasons: number[] | null; externalRequestId?: string; requestedBy: string | null }
}
Usage from a resolver:
@Mutation(() => Boolean)
async requestItem(
  @Arg("imdbId") imdbId: string,
  @Ctx() ctx: GraphQLContext,
): Promise<boolean> {
  ctx.sendEvent({
    type: "riven-external.item-requested",
    item: { type: "movie", imdbId },
  });
  return true;
}
Use RivenEventSchemaMap (exported from @repo/util-plugin-sdk/events) to look up the Zod schema for any internal event type at runtime:
import { RivenEventSchemaMap } from "@repo/util-plugin-sdk/events";

const schema = RivenEventSchemaMap.get("riven.media-item.scrape.requested");
const parsed = schema?.parse(rawEvent);

Build docs developers (and LLMs) love