Important
Obscura has been renamed to Prismedia and development continues at:
This repository is archived. Please update your bookmarks, clones, and any references.
A modern, self-hosted private media browser.
Video-first, with first-class comics, images, galleries, and audio. Designed for a single trusted user on a private LAN.
Docs · Quick Start · Highlights · Features · Subreddit · Configuration · Development
Obscura is a private, self-hosted home for your entire media collection. Videos, movies, TV series, comics, manga, books, image galleries, and audio all live together in one refined interface — organized, searchable, and ready to play from any device on your local network.
It runs as a single Docker container: PostgreSQL, ffmpeg, and the web server are all bundled together. Mount your media directories, open a browser, and you're watching. No external databases, no cloud accounts, no data leaves your network.
Discussions and community updates live on the Obscura subreddit.
- Every media type, first-class — videos, movies, TV series, comics, manga, books, image galleries, and audio libraries are all treated equally. No second-class formats.
- Mobile first — built for phones from day one. The desktop view is an expansion of the mobile layout, not the other way around.
- Rich video playback — HLS adaptive streaming with on-demand ffmpeg transcoding, a scrollable frame strip, and one-click marker + thumbnail creation from any frame.
- Subtitles & live transcripts — multi-language sidecar / embedded / uploaded tracks, three caption styles, and a dockable transcript panel that reads alongside the video on desktop.
- Comic and book reader — cbz/zip archives and image folders scan as series, keep natural page order, import ComicInfo metadata, track reading progress, and open in paged spreads or vertical webtoon mode.
- Audio libraries — organize and play your music and audio collection with the same metadata pipeline: albums, tracks, cover art, waveforms, performers, and shuffle support.
- Plugin-powered metadata — TypeScript, Python, and Stash-compatible scraper plugins expose providers for videos, series, performers, galleries, and audio. StashDB is supported natively.
- Bulk identify — select a batch of unmatched items and Obscura iterates every installed provider. No more identifying things one at a time.
- Everything cross-referenced — videos, comics, audio, performers, studios, and tags all link to each other through the same rich metadata surface.
- Automated scanning — point it at a folder, walk away. Obscura scans on a schedule and notices new files.
- Command palette + global search —
⌘Kfrom anywhere, or a dedicated search page spanning all library types. - Content filtering — mark library roots as restricted to keep sensitive content out of shared views; switch with a keyboard shortcut or a gesture on mobile.
- Drag-and-drop uploads — add files from the browser, remove from the library, or delete from disk entirely.
- One image, one port — everything runs in a single Docker container. No external Postgres, no Redis URLs, no env wrangling.
Obscura ships as a single Docker image with PostgreSQL, SvelteKit, ffmpeg, and the background worker bundled. No external databases. No configuration required.
docker run -d \
--name obscura \
-p 8008:8008 \
-v obscura-data:/data \
-v /path/to/your/media:/media \
ghcr.io/pauljoda/obscura:latestservices:
obscura:
image: ghcr.io/pauljoda/obscura:latest
ports:
- "8008:8008"
volumes:
- obscura-data:/data
- /path/to/your/media:/media
restart: unless-stopped
volumes:
obscura-data:docker compose up -dOpen http://localhost:8008 and you're done.
| Mount | Purpose |
|---|---|
/data |
Database, cache, thumbnails, trickplay sprites, HLS transcodes |
/media |
Your media library. Mount one or more directories here |
You can map as many subdirectories under /media as you like and register each one as a library root in Settings.
Obscura publishes two channels to ghcr.io/pauljoda/obscura:
| Tag | What it is | When to use |
|---|---|---|
latest |
The most recent tagged release (vX.Y.Z) |
Default for end users. Stable, versioned, paired with a GitHub Release and changelog entry |
X.Y.Z / X.Y / X |
A specific released version (or minor / major line) | Pin to a known version for reproducible deploys |
dev |
Every commit on main after CI passes |
Bleeding edge. Expect churn. Good for testing fixes before a release |
sha-abc1234 |
A specific commit SHA on main |
Pin to an exact dev build for rollback or bisection |
X.Y.Z-abc1234 |
A specific commit against the in-progress X.Y.Z-dev cycle |
Same as sha-… but self-describing — shows which release this dev build is headed toward |
If you're running Obscura as your media browser and want things to Just Work, use :latest. If you want to try a change that hasn't shipped yet, use :dev. See CHANGELOG.md for everything that's gone into each release, and the GitHub Releases page for the same notes rendered with assets.
An at-a-glance overview of your collection: totals, recent activity, queue state, and live job status.
A responsive grid of scene cards with thumbnails, duration, resolution badges, and quick metadata. Filter, sort, and view as cards or a compact list.
The video library supports two browsing modes. Grid/List shows every scene in your library as a flat feed you can sort, filter, and search. Folders mirrors the directory structure on disk, letting you navigate into subfolders, see scene counts per folder, and drill down to scenes inside a specific directory.
Clicking into a folder opens a detail view inspired by media servers like Jellyfin. Each folder can carry its own metadata: a poster and backdrop image, description, studio, date, star rating, linked performers (shown as a scrollable Cast & Crew strip), and tags. The edit panel uses the same chip pickers and autocomplete as scene editing, so adding performers, tags, and studios is fast. Uploading or dragging files while viewing a folder places them directly into that folder's directory on disk — no library root picker needed.
Folders are also searchable from the command palette and the full search page, and appear on studio and tag detail pages when associated.
Direct playback of common formats plus on-demand HLS transcoding for anything the browser won't play natively. The scrollable frame strip under the player lets you scrub by thumbnail — and turn any frame into a marker or a custom preview image with a single click.
Obscura treats subtitles as a first-class feature. Three ingestion paths are wired up end-to-end:
- Sidecar discovery — drop a
.srt/.vtt/.assnext to a video (optionally tagged with a language, e.g.movie.en.srt) and it gets picked up on the next library scan. - Embedded extraction — a background worker job runs
ffmpegagainst videos with soft-subtitle streams and converts each track to WebVTT automatically. Image-based subtitle codecs (PGS, VobSub) are skipped gracefully. - Manual upload — drop a file in from the scene page with an inline language picker.
All tracks land in a shared Transcript tab where you can rename them inline, delete them, or trigger a re-extract. The transcript itself shows the full cue list with the current line highlighted, past lines grayed but still clickable, and a single click on any cue seeks the player.
On desktop, a Dock next to video button pins the transcript directly beside the player with a draggable resize handle between them — watch and read at the same time, no context switching. The dock preference and last-used width persist in localStorage, follow you across scenes, and auto-collapse on scenes without subtitles so you never see empty space.
The player renders captions through a custom overlay with three visual styles you can switch between on the fly:
- Stylized — Dark Room brass-edged plate with a subtle glow, matches the rest of the UI.
- Classic — flat translucent-black box with plain white text, the look of most media players.
- Outline — white text with a black stroke and no box, maximum transparency over the picture.
Text size and vertical position are continuously adjustable, and an in-player settings side panel lets you tweak everything live on top of whatever is currently playing. Per-user overrides persist locally; a reset button snaps you back to the library defaults.
Library-wide defaults — auto-enable on load, preferred-language priority list (first match wins, with ISO 639-1 ↔ 639-2 equivalence so en also matches eng), default caption style, text size, and position — are all configurable from the global settings page, with a live dummy-frame preview that updates as you tweak the controls.
Every entity — videos, comics, books, audio, performers, studios — carries the same metadata surface: title, studio, performers, tags, ratings, custom notes, and provenance. Plugin-powered providers cover everything, and StashDB / community Stash scrapers are supported natively for existing collections.
Select a batch of unmatched scenes, galleries, or performers and Obscura will iterate every installed scraper to find a match. No more identifying things one at a time.
Browse, install, enable, and disable community scraper plugins directly from the UI. Stash-compatible scrapers and StashDB endpoints are supported alongside native TypeScript and Python plugins — the full public scraper index is built in, no manual file copying required.
Folder-based and archive-based galleries are first-class. Browse, tag, rate, link authors/performers and studios, and view them in grid or lightbox modes.
Comics and manga are part of the same gallery system — not a separate silo. Drop in cbz/zip archives or image folders, and Obscura keeps page filenames in natural reading order, imports ComicInfo metadata where available, groups chapter archives into series-style galleries, tracks read/unread progress, and opens them in a dedicated reader with paged spreads or vertical webtoon mode.
The book reader brings the same organized, metadata-rich experience to your reading collection. Books live alongside comics, galleries, and videos in one unified library — browsable, searchable, and readable from any device on your network.
Organize and play your audio collection with the same metadata pipeline — libraries, tracks, cover art, tags, and performer/studio linking. Includes a built-in player with shuffle and playlist support.
Press ⌘K (or Ctrl+K) from anywhere to jump to anything. There's also a dedicated search page that spans scenes, performers, studios, galleries, tags, and audio libraries.
Register multiple library roots, choose whether generated assets (trickplays, sprites, previews, HLS cache) live next to your media files or in a dedicated cache volume, and enable automatic periodic scanning so new files show up on their own.
A live view of every queue and job — library scan, probe, fingerprint, thumbnail, sprite, HLS, import, scrape. Retry, cancel, and watch progress in real time.
Mark any library root as restricted to keep specific content out of shared or public views. Flip the filter globally with a keyboard shortcut on desktop or a hidden gesture on mobile. The flag propagates to all videos, images, galleries, audio libraries, and tracks under that root.
Every view is designed for a phone first. Navigation, playback, scanning, scraping — everything works on mobile, not just "sort of."
Drag files directly into the browser, or use the upload picker. Remove items from the library, or remove them from disk entirely — your choice, per action.
Obscura works out of the box with zero configuration. These environment variables are available for advanced use:
| Variable | Default | Description |
|---|---|---|
OBSCURA_CACHE_DIR |
/data/cache |
Directory for HLS cache, thumbnails, sprites, previews |
OBSCURA_MAX_VIDEO_UPLOAD |
21474836480 |
Max bytes for a single upload (default ~21 GiB) |
OBSCURA_SECRET |
auto-generated at /data/.obscura-secret |
Secret key used to encrypt plugin credentials (e.g. TMDB API keys) at rest. Auto-generated on first boot and persisted in the data volume. Override only if you want to manage it externally; changing it invalidates all previously saved plugin credentials. |
Mount as many media directories as you need:
docker run -d \
--name obscura \
-p 8008:8008 \
-v obscura-data:/data \
-v /mnt/nas/videos:/media/videos \
-v /mnt/nas/photos:/media/photos \
-v /mnt/nas/music:/media/music \
ghcr.io/pauljoda/obscura:latestThen add each one as a library root in Settings → Library. Per-root, you can choose whether generated assets live alongside the media files or in Obscura's dedicated cache directory.
Obscura uses a Dark Control Room visual system inspired by Blackmagic DaVinci Resolve, high-end audio rack gear, and film color-grading suites.
- Surface hierarchy — Five levels from near-black graphite to elevated panel gray
- Accent — Burnished brass (
#c49a5a), used sparingly for active and selected states, always with glow - Typography — Geist (headings), Inter (body), JetBrains Mono (metadata and utility)
- Motion — Weighted and deliberate, precision machinery, no bounce
- Shape — Sharp corners everywhere (
border-radius: 0)
Full specification in docs/design-language.md.
The single image bundles:
| Component | Role |
|---|---|
| SvelteKit | Web frontend and same-origin HTTP API ingress |
| pg-boss | Background job queue (Postgres-backed, no Redis) |
| PostgreSQL 16 | Database |
| ffmpeg | Video and audio transcoding |
All services run inside the container, coordinated by a single entrypoint. Port 8008 is the only exposed port.
- Node.js 22+
- pnpm 10+
- Docker and Docker Compose (for PostgreSQL)
git clone https://github.com/pauljoda/obscura.git
cd obscura
pnpm install
# Start PostgreSQL (pg-boss manages the queue inside the DB)
docker compose -f infra/docker/docker-compose.yml up postgres -d
# Apply migrations
pnpm --filter @obscura/db db:migrate
# Start all services in dev mode
pnpm devThe app and API both run at http://localhost:8008, with JSON served under /api/*.
| Command | Description |
|---|---|
pnpm dev |
Start all services in development |
pnpm build |
Build all apps and packages |
pnpm check |
Lint and typecheck across the monorepo |
pnpm release:check |
Validate version and changelog alignment |
pnpm --filter @obscura/db db:generate |
Generate a new SQL migration from schema changes |
pnpm --filter @obscura/db db:migrate |
Apply versioned migrations to PostgreSQL |
pnpm --filter @obscura/db db:studio |
Open Drizzle Studio |
# Dev-style build (does not require a matching CHANGELOG release heading)
docker build -f infra/docker/unified.Dockerfile -t obscura .
# Release-style build (enforces package.json version matches a released
# CHANGELOG heading — fails on -dev versions). Used by the Release workflow.
docker build -f infra/docker/unified.Dockerfile --build-arg RELEASE_STRICT=1 -t obscura .
docker run -p 8008:8008 -v obscura-data:/data -v /your/media:/media obscuraObscura follows Semantic Versioning and Keep a Changelog.
- Every commit to
mainrebuilds the:devimage and appends entries to## [Unreleased]inCHANGELOG.md. Each unreleased section leads with a What's New TL;DR of the most impactful user-facing changes, followed by detailed entries grouped by Added / Changed / Fixed. The rootpackage.jsoncarries aX.Y.Z-devmarker between releases. - Releases are cut server-side by the Release GitHub Action. Maintainers trigger it from the Actions tab with a
patch/minor/majorbump (or an explicitX.Y.Z). The workflow:- Runs
scripts/release/cut.mjs --phase release: bumps everypackage.json, promotes## [Unreleased]to## [X.Y.Z] - YYYY-MM-DD, and writesRELEASE_NOTES.mdfrom the new section. - Creates commit
chore(release): vX.Y.Zand tagvX.Y.Z. - Runs
scripts/release/cut.mjs --phase post: bumps toX.Y.(Z+1)-devand commitschore(release): begin vX.Y.(Z+1)-dev cycle. - Pushes main + tag.
- Builds the unified Docker image with
RELEASE_STRICT=1from the release tag and pusheslatest,X.Y.Z,X.Y, andXto GHCR. - Creates a GitHub Release for
vX.Y.Zwith the extracted changelog as the body.
- Runs
- Users consuming
:latestalways get the most recent released build — never a half-finishedmain. Users who want to test unreleased changes can pin:devor a specific:sha-…tag.
See the release runbook and full policy in CLAUDE.md.
Licensed under CC BY-NC-SA 4.0.
You are free to share and adapt this work for non-commercial purposes, with attribution, under the same license terms. See LICENSE for details.





















