Skip to content

Repository files navigation

VOD & DVR Manager

Curates movies and TV shows from multiple real sources — Xtream-Codes (XC) IPTV providers, Plex, Emby/Jellyfin, and your own media files (a local folder, SMB, SFTP, S3-compatible storage, Google Drive, Dropbox, or Box) — into one deduplicated pool, then re-exposes that pool as its own XC-compatible catalog server so one or more Dispatcharr instances can pull it like any other provider.

Same real content is often available from several sources at once (a movie on both an XC reseller and your own Plex library, or the same title from two different resellers). VOD & DVR Manager treats those as multiple sources for one pool entry rather than duplicate entries, and automatically fails over between them if one goes down or hits its connection limit — series included, pulling episodes from every provider that matches, not just whichever matched first. A source's own trailer, when it has one, passes through to the re-exposed XC feed as-is.

New here? See USERGUIDE.md for a full walkthrough with screenshots — installation, connecting Dispatcharr (single or multiple instances), security hardening, and every curation tool. This README is a concise technical reference for people already up and running.

Requirements

  • Docker + Docker Compose
  • ffmpeg (already included in the Docker image — nothing to install separately)
  • At least one real VOD source: an XC-type IPTV provider, a Plex server, or an Emby/Jellyfin server
  • One or more Dispatcharr instances to pull the resulting catalog into
  • A free TMDB API key (v3 auth) — used for enrichment (once a movie/series has a known TMDB id, its detail refreshes straight from TMDB instead of your provider, easing load on any provider that rate-limits aggressively), the year-review/missing-artwork disambiguation flows, and category List Sync; not required for basic operation
  • Optional: a free MDBList API key — a second public list source for category List Sync, alongside TMDB Lists
  • Optional: an API key from Anthropic, OpenAI, and/or Google (Gemini) for the AI-assisted features (any one is enough; more than one lets you switch providers without re-entering a key)

Quick start

docker compose up -d

This pulls the published ghcr.io/jstevenscl/vod-manager:latest image (see docker-compose.yml). Building from source instead — e.g. for local development against this repo — works too:

docker build -t vod-manager:local .

The app listens on port 8282. First run asks you to set an admin username/password (VOD & DVR Manager's own login, separate from Dispatcharr's) — see USERGUIDE.md for why you should set one rather than skip it.

From there: add your real providers (Curation & Maintenance → Providers) and import their catalogs, then connect Dispatcharr (below). Full walkthrough with screenshots in USERGUIDE.md.

Library sources — your own media files

Besides XC/Plex/Emby/Jellyfin, a provider can also be your own files, read directly instead of pulled from an IPTV panel: a local folder or mounted path (including NFS, via a Docker volume or host mount — there's no separate "NFS" backend since the underlying tool this feature is built on, rclone, has no NFS client), an SMB/CIFS share, an SFTP server, S3-compatible storage (AWS, MinIO, Wasabi, B2, and similar), or Google Drive / Dropbox / Box. Each file's name is parsed for a title/year/season/episode and matched against TMDB conservatively — anything uncertain lands in the existing Missing Artwork/Needs Review queues instead of guessing, and your files are never deleted or modified by anything in VOD & DVR Manager. The three cloud providers use an OAuth token you generate yourself with rclone authorize on your own machine — VOD & DVR Manager never sees your real login for any of them. MediaFire isn't supported (not an rclone backend); Box uses the same mechanism as Drive/Dropbox but is less thoroughly tested. Full setup for each backend in USERGUIDE.md.

Connecting Dispatcharr instances

VOD & DVR Manager distinguishes two separate relationships with Dispatcharr:

  • Connected Instances — who's allowed to pull from VOD & DVR Manager. Each Dispatcharr instance gets its own auto-generated, high-entropy username/password pair. Use VOD & DVR Manager's own URL as the server_url on an XC-type M3U account in that Dispatcharr instance, with the generated credentials.
  • Dispatcharr Connections — who VOD & DVR Manager itself reaches out to. Used to push each provider's connection limit into Dispatcharr's own admission control, and to check real-time live-TV viewer counts for shared-connection-limit coordination (see below).

A single Dispatcharr instance is usually both at once (it pulls from VOD Manager and VOD & DVR Manager pushes profile data back to it), but they don't have to match — you can have Dispatcharr instances that only pull, and connections VOD & DVR Manager only reaches out to for coordination.

Adding a new instance (Configuration → Dispatcharr Connections → "Connect a new instance"): give it that instance's own admin API token and VOD Manager's own URL as reachable from that instance — this isn't always the same URL you're viewing VOD & DVR Manager at yourself (a co-located instance might use a Docker-internal hostname; a remote one needs your real public URL). VOD & DVR Manager then automatically creates the client credentials and the Dispatcharr-side M3U account for you (with a 50-concurrent-stream account- level cap — generous on purpose, since the real per-provider limits are enforced separately; see below). The only thing left is on Dispatcharr's own side: enable VOD on the new account and pick which groups/categories to turn on — normal setup for any source, regardless of how the account got created.

On Dispatcharr v0.29.0+, every movie's 18+ status also syncs automatically as part of the normal VOD refresh, so Dispatcharr's own per-profile "Hide Mature Content" setting works against VOD & DVR Manager's catalog with no extra setup. Movies only for now — Dispatcharr has no equivalent flag for series yet.

Per-instance category access control

Dispatcharr has no per-user/per-profile VOD split of its own — once content is pulled in through a Connected Instance's M3U account, every Dispatcharr user on that instance sees the identical catalog. For real per-audience control (e.g. a kids-only client, or handing a limited catalog directly to an end-user IPTV app like TiviMate or IPTV Smarters instead of routing it through Dispatcharr at all), restrict a specific Connected Instance's credential to a set of categories under Configuration → Connected Instances → Category access. Left as "— all —" (the default), a client sees the whole pool, matching every existing credential's behavior today. Restricting it is enforced everywhere that credential is used — catalog listing, info lookups, and the actual stream — not just hidden from the browse UI, so a restricted client can't reach disallowed content even with a direct/copied stream URL.

In-app test player

Movies, series episodes, and Needs Review items all have a Play button that opens a lightweight in-app player — meant for verifying imports (matched the right title, source actually plays, etc.), not real end-user viewing (real viewers watch through Dispatcharr with an external player, which never goes through this player at all). Direct playback works for anything a stock <video> element can decode natively; two fallbacks cover the rest, both re-encoding the source with ffmpeg on the fly rather than just relaying it:

  • Transcoded — fast to start, but forward-only (no mid-stream scrubbing). "Jump to" buttons restart the stream partway in instead.
  • HLS (seekable) — real seek support via a proper HLS playlist + segments, at the cost of a slower start (ffmpeg has to produce a first segment before anything plays) and using somewhat more CPU/disk while active. Backward seek works across everything encoded so far; seeking past the live edge is naturally blocked until ffmpeg catches up, the same limitation any in-progress live/event HLS stream has.

Both fallbacks (and every other player-facing route) are torn down when a session ends — closing the player, an idle timeout with no further requests, or a Kill from Activity below all release the encoder process and any on-disk segments.

AI-assisted categories, Needs Review, and Missing Artwork

An API key from any of Anthropic, OpenAI, or Google Gemini (Configuration → API Keys — configure as many as you have access to, then pick which one is active) unlocks three assists, none of which ever apply anything automatically — every one is a suggestion you still review and confirm:

  • Suggest a category with AI (Categories) — describe a category in plain English and the AI proposes a structured filter rule using the same fields/ops the manual rule builder uses (name, genre, year, country/language, director, is_adult). Good for anything expressible as field conditions; review the proposed rule before creating it.
  • AI Evaluate (✨ button on any category) — for criteria the rule fields genuinely can't express (mood, plot, audience fit), the AI judges actual titles against a plain-English description instead of matching fields. Real per-request API cost, so this always runs over a bounded candidate set (optionally narrowed first by a rule pre-filter) rather than the whole pool — the result always reports how many candidates were actually considered vs. left out by the cap, never a silent truncation.
  • Ask AI (Needs Review, Missing Artwork) — when an item has no year, or no confident poster match, and multiple TMDB candidates are ambiguous, the AI picks the most likely correct match with its reasoning and a confidence level, as an extra hint alongside the normal TMDB suggestion list. You still click a candidate yourself to actually resolve it.
  • Bulk resolve with AI (Needs Review, Missing Artwork, Duplicate Finder) — the same three judgments as above, run as a background job over a batch you select instead of one item at a time. Only ever applies a fix when the AI reports high confidence — anything medium/low/no-match is left untouched with its reasoning shown, never guessed into the pool. Duplicate Finder's version only calls the AI when a group has no shared TMDB id at all; a group that already agrees on one merges immediately, no AI call needed, and a group with a genuine conflicting TMDB id is never merged regardless of what the AI says.

See USERGUIDE.md for the full set of curation tools (Missing Artwork, Language Filter, Duplicate Finder, Needs Review, Orphan Checker) with screenshots.

DVR recordings

DVR isn't a separate provider you add — it's a capability you turn on for a Dispatcharr connection you already have, so a connection's finished recordings flow into the same pool as everything else. Beyond ingestion, it covers EPG-driven Recording Rules (VOD & DVR Manager's own replacement for Dispatcharr's own Series Rules, which have a channel-matching bug), backfill (reuse existing pooled content instead of re-recording), per-person disk quotas/stream limits/retention, a Missing Episodes view, Metrics, and a separate self-service Portal so end users can schedule and manage their own recordings without touching the admin UI at all. Each person's Portal account records into their own explicitly-assigned DVR category — there's no silent shared default, by design (see USERGUIDE.md for why). An opt-in delete from Dispatcharr once safely copied setting keeps Dispatcharr's own storage from filling up forever with content VOD & DVR Manager has already absorbed — off by default, and only ever deletes after a byte-verified independent copy exists. Full setup (including the local-path-vs-download-mode decision, which is easy to get wrong) and screenshots in USERGUIDE.md.

Shared connection-limit coordination

If a real provider also has its own native live-TV account somewhere in Dispatcharr (common — the same IPTV subscription usually serves both live channels and VOD), live TV and VOD & DVR Manager's own usage draw from the same real connection pool without either side knowing about the other by default. Configure it under Providers → Shared Limit / Live Accounts:

  • Shared Limit — the provider's real total connection cap (from your subscription).
  • Live accounts — link the provider to its native live-TV account on each Dispatcharr connection that has one, optionally scoped to one specific M3U profile on that account rather than the whole thing. A provider can have a different live-TV account on more than one Dispatcharr instance; all of them count toward the same real limit.

VOD & DVR Manager checks the current combined usage (its own active streams + every linked live account's viewer count) before opening a new stream against that provider, and fails over to the next available source instead of exceeding the real limit.

Multiple providers on the same real login are also automatically pooled together, the same way Dispatcharr's own connection tracking does it: credentials are compared (username + password, decrypted for the comparison) across your active providers, and any that share the exact same real login count against one shared limit rather than each getting their own — since they are the same underlying subscription regardless of how many separate provider rows you've set up for it.

Multi-account providers (sub-accounts)

Some subscriptions aren't one login — they're several separate logins sold as a bundle (e.g. a "5x1" package: five independent single-connection accounts). Dispatcharr represents this with M3U profiles under one source; VOD & DVR Manager now has the same concept natively: a provider can hold multiple sub-accounts, each with its own real username, password, and connection limit.

Configure it under Providers → Sub-accounts (expand a provider row):

  • Add each real login as a sub-account, with its own max_streams (0 = unlimited).
  • VOD & DVR Manager tries active sub-accounts in order and picks the first one with a free slot when opening a stream — matching Dispatcharr's own default-then-next-profile failover exactly, not a summed/aggregate limit.
  • Sub-accounts can be individually deactivated (e.g. a login temporarily suspended) without touching the rest of the provider.
  • Providers without any sub-accounts configured behave exactly as before — this is purely additive.

Already split into separate provider rows? If you previously worked around this by manually creating one VOD & DVR Manager provider per login (the only option before sub-accounts existed), use Merge Providers (Providers page → Merge Providers) to consolidate them: pick a primary provider and one or more others to fold in as its sub-accounts. Content is never deleted — every source re-points to the primary provider, and any Dispatcharr live-account links move over too. If a merge finds the same piece of content already present on both providers (a genuine collision, not just a duplicate), that item is left on the old row and reported back so you can resolve it by hand; the old row is only removed once it's fully empty.

Security and deployment

  • Set an admin login and don't skip it. Until a login is configured, every API route is unauthenticated — a startup log warning and an in-UI confirmation are both there specifically to make this hard to overlook.
  • Don't expose this to the public internet without TLS in front of it. The app itself doesn't terminate TLS — put a reverse proxy or VPN/tunnel (Cloudflare Tunnel, WireGuard, etc.) in front if it needs to be reachable from outside your own network. This matters more than usual here: the XC protocol itself has no concept of session auth beyond a username/password in the URL, checked on every request — that's a real, if unavoidable, weak point once anything is internet-facing.
  • Login passwords are hashed with PBKDF2-HMAC-SHA256 (260,000 iterations), not a fast general-purpose hash — resistant to offline brute-forcing if the config file ever leaked.
  • Provider passwords, Dispatcharr tokens, and XC client secrets are encrypted at rest in the database, not stored in plaintext. The encryption key lives in config.json so it travels with that file's own backup/restore/reset flow; existing plaintext values from before this was added upgrade automatically on next startup.
  • Both the admin login and the XC (streaming) login have brute-force lockout — repeated failed attempts from one address get temporarily locked out (XC lockout is configurable under Configuration → Security; changes apply within ~30s). This slows down automated brute-forcing but doesn't replace putting this behind a VPN/tunnel if it's ever going to be reachable beyond a trusted network. Lockout state is in-memory and resets on every container restart — a restart-and-retry attacker is a much smaller threat than an internet-facing app with no lockout at all.
  • Streaming credentials are never written to logs. The XC protocol embeds them in the URL itself (its own convention, not ours) — both the app's own logging and the container's access log redact them before anything is written to stdout.
  • Every connected instance gets its own credential, not a shared one — Configuration → Connected Instances. Revoke or regenerate one without affecting the others if a specific instance's credential is ever compromised.
  • Optional per-instance IP allowlist — if a specific connected instance's source IP is known and stable, you can lock its credential to that IP as an extra layer. Leave it blank for instances behind CGNAT/rotating IPs (locking those would just break them, not add real security, since the address isn't a reliable identity signal for them anyway).

Refresh schedule

Configuration → Refresh Schedule controls how often background work runs:

  • Catalog refresh — how often each provider type (XC, Plex, Emby, Jellyfin) gets automatically re-imported, each on its own interval. Plex/Emby libraries can take much longer to scan than a cheap XC catalog pull, so they're not forced onto the same cadence. Defaults to 6 hours for every type.
  • Enrichment TTL — how long detail-level metadata (posters, cast, genre) is cached before a movie/series is eligible to be refetched. Defaults to 24 hours.
  • List Sync — how often categories with a linked public list source (TMDB List or MDBList) auto-resync. Off (manual "Sync now" only) by default — enabling it adds new recurring API traffic to whichever source(s) each category uses.

Backup and restore

Configuration → Backup & Restore lets you download, restore, or reset each piece of VOD & DVR Manager's state independently (config, sessions, the catalog database) — e.g. reset a corrupted database without touching saved credentials, or roll back just the config. Database downloads use SQLite's VACUUM INTO for a consistent snapshot even while the app is actively writing to it.

Configuration → Diagnostics downloads the app's own log history with credentials, hostnames, and IP addresses scrubbed — safe to share when reporting a bug or asking for help.

Curation tools

Curation & Maintenance and the Movies/TV Shows toolbars host a set of catalog-quality tools — Missing Artwork (bulk poster fixing, with a language-aware filter and sibling-safe bulk archiving), Language Filter (the same language filtering over your whole library, not just poster-missing items), Enabled Playback Languages (a live, instantly-reversible playback/export filter by source language — separate from the import-time Language Exclusion above; nothing archived or deleted, just hidden from playback while unchecked), Duplicate Finder (matches on punctuation variants, adjacent-year mislabeling, and TMDB id, with one-click bulk merges for both fully-corroborated TMDB-confirmed matches and a second, separate tier where only one candidate carries a self-consistent TMDB id; an opt-in, off-by-default checkbox also groups a quality-tagged title like "4K: Movie" with its plain "Movie" as a candidate, for consolidating with Stream Priority's quality mode below — matches sharing a confirmed TMDB id merge automatically the moment enrichment confirms it, before ever reaching this queue — and an archived item stays archived even when a different provider's own import later matches it by name; only re-importing from the exact same source it was archived from can bring it back), Needs Review and the broader sidebar Metadata Review (resolve year-ambiguous imports and titles a provider left without any TMDB identity at all) alongside its sibling Incorrect TMDB IDs (a stored id TMDB itself now confirms is gone), and Orphan Checker (finds dead rows a provider deletion can leave behind — a series whose only source provider no longer exists, or movies/episodes with zero sources at all). Every movie/series can also be manually renamed or have its year corrected from its own detail view, for whatever a provider's own catalog data got wrong with no other way to fix it — including setting its TMDB id directly when you already know the correct match, and (for a series) editing an individual episode's name/number. Apply TMDB Titles (Movies/TV Shows toolbar) does this in bulk for every already-confirmed TMDB match at once, instead of one item at a time. Client Title Format (Curation & Maintenance) is a separate, ongoing setting to have VOD clients see a "(Year)" suffix on every title, independent of the pool's own stored data.

Flag as wrong content (the flag icon on any movie, series, episode, or individual source) reports "this isn't actually what its label says" — Curation & Maintenance's Flagged Content queue lists everything flagged across the whole catalog in one place. Resolving there just clears the flag; fix the actual mismatch (rename, re-match TMDB, move/remove the wrong source) from the item itself first.

Category List Sync (Manage Categories → the list icon on any category) auto-populates a category from one or more public lists — a TMDB List and/or an MDBList list, mixed freely on the same category — matching each list entry against your pool by TMDB id first, falling back to a forgiving title+year match. Two sync modes: Add only (default) never removes anything already placed, for a curated category you're still hand-tuning; Mirror also removes anything no longer on any linked list, keeping the category an exact reflection of its source(s) (e.g. a "Top 100" list that cycles over time) — skipped for that pass if any linked source's fetch fails, so a transient error never looks like "everything's gone."

Auto-create categories (per provider, Providers → Auto-create categories) creates a Smart Category for every distinct category name a provider reports on import, matched via that item's own provider-category tag — so your VOD & DVR Manager categories mirror the provider's own grouping without hand-building a rule for each one. Two providers that both have a category literally named "Comedy" share one VOD & DVR Manager "Comedy" category rather than creating a duplicate. Never overwrites a category you've already built by hand with the same name — it only ever fills in what's missing.

Auto-create categories checkbox on a provider

Archive new categories (per provider, next to Auto-create categories) auto-archives any category a provider reports for the first time, catching it before it lands in your library instead of after — off by default, and never retroactively archives a category the provider was already reporting before you turned it on.

Stream priority (Curation & Maintenance → Stream Priority) controls which of a pool item's multiple real sources gets used first when more than one provider has the same content: by provider priority (today's default), by detected quality (4K/1080p/HD, sniffed from the source's own name/ category), or either one as primary with the other as tiebreaker.

Stream Priority selector

Full details and screenshots for each in USERGUIDE.md.

About

Self-hosted VOD catalog manager for Xtream Codes/Plex/Emby/Jellyfin providers -- Dispatcharr integration, AI-assisted metadata enrichment, and curation tools (duplicate/orphan detection, language filtering, missing-artwork cleanup).

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages