Skip to main content

Plugin System Overview

Obscura's metadata is plugin-driven. Posters, descriptions, performers, studios, tags, episode breakdowns — none of it is hard-coded. Every provider is a plugin with a manifest, a declared capability set, and an execution envelope.

This page is the bird's-eye view: what kinds of plugins exist, how they relate, and what to read next.

Three runtimes

RuntimeWhat it isWhen to use
TypeScriptA compiled JS module loaded into the worker process via dynamic import().New providers when you want type safety, npm ecosystem, in-process speed.
PythonA standalone script invoked as a subprocess; talks JSON over stdin/stdout.New providers where Python libraries make life easier (some scrapers, some data formats).
Stash-compatA wrapper around a Stash YAML scraper. The community has hundreds of these; the adapter maps Stash actions onto Obscura's envelope.Pulling in existing Stash scrapers without rewriting them.

All three speak the same protocol at the executePlugin() boundary:

┌──────────────────────┐
identify request │ PluginExecutionInput│
─────────────────►│ { action, input, │
│ auth, batch } │
└──────────┬───────────┘

┌───────────────┼───────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ TypeScript │ │ Python │ │ Stash │
│ loader │ │ subprocess │ │ adapter │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
└────────┬──────┴───────┬──────┘
▼ ▼
┌──────────────────────────────┐
│ PluginExecutionOutput<T> │
│ { ok, result | results, │
│ error } │
└──────────────┬───────────────┘


Normalizers → scrape_results row

Whether you write TypeScript or Python, your code answers a single question: "given this input and these credentials, what metadata do you have?"

A separate concept: StashBox

StashBox endpoints are not plugins. They use a fingerprint-based GraphQL protocol (PHash, OSHash, MD5) to look up content. Configuration lives in stashbox_endpoints rows; queries go through the client in packages/stash-import/src/stashbox/.

StashBox results land in the same UI as plugin results — scrape_results rows reviewed and accepted in the same drawer — but the path that produced them is different. The dedicated docs are at Advanced · StashBox.

What a plugin produces

A plugin returns a normalized result matched to the action it ran:

Action categoryResult type
videoByURL, videoByName, videoByFragmentNormalizedVideoResult
folderByName, folderByFragment, folderCascadeNormalizedFolderResult (with optional episodeMap)
galleryByURL, galleryByFragmentNormalizedGalleryResult
imageByURLNormalizedImageResult
audioByURL, audioByFragment, audioLibraryByNameNormalizedAudioTrackResult / NormalizedAudioLibraryResult
performer*Performer result (via Stash adapter for legacy parity)
movieByName, movieByURL, movieByFragmentNormalizedMovieResult
seriesByName, seriesByURL, seriesByFragmentNormalizedSeriesResult (with optional disambig candidates[])
seriesCascadeNormalizedSeriesResult with full seasons[].episodes[] tree
episodeByName, episodeByFragmentNormalizedEpisodeResult

These shapes are documented in detail in Capabilities.

The application normalizer trims strings, validates URLs, deduplicates names case-insensitively, coerces numbers, and accepts both singular and plural field names. Returning slightly-the-wrong shape isn't fatal — the normalizer is forgiving. Returning nothing useful is fine too: just null.

What happens after a result lands

plugin → normalized result
→ scrape_results row written (status = pending,
proposed_* fields populated)
→ user reviews in the cascade drawer
→ on Accept: missing performers/tags/studios created,
images downloaded to /data/cache/metadata/,
entity row updated, status = accepted

The cascade drawer is what turns one TMDB series result into a fully-populated series + N seasons + M episodes in your library. The plugin returns the tree; Obscura walks it.

First-party plugins

The CHANGELOG mentions The Movie Database (TMDB), TVDB, YouTube, and MusicBrainz. They live in the obscura-community-plugins sister repo, not in the main repo. They're the easiest reference reads for "what does a real plugin look like."

You install them from Plugins → Obscura Index in the web app. One click downloads, verifies, and registers them.

Where plugin code lives

PathWhat it is
packages/plugins/src/types.tsThe wire-protocol types — OscuraPlugin, PluginExecutionInput, PluginExecutionOutput, PluginCapabilities, every Normalized*Result. The contract.
packages/plugins/src/manifest-parser.tsYAML manifest reader and validator.
packages/plugins/src/ts-loader.tsTypeScript runtime loader.
packages/plugins/src/executor.tsPython subprocess executor.
packages/plugins/src/normalizer.tsOutput normalizers.
packages/stash-import/src/stash-adapter.tsStash-compat YAML adapter.
packages/app-core/src/plugin-execution.tsWeb-side glue: resolve manifest, decrypt auth, dispatch into the runtime, normalize result.
packages/app-core/src/plugin-registry.tsDiscover installed plugins from the plugin_packages table.
packages/app-core/src/plugin-proposed-result.tsConvert a plugin output into a scrape_results row.

If you're going to read source, start with packages/plugins/src/types.ts. Everything else makes sense once you know the wire format.