masqueradarr
sources & adapters · 05 the pluggable core

One adapter file, zero core branches

Every provider — ~17 of them, plus your own imports — plugs into the same source-agnostic core through one contract. The generic pipeline never branches per source; every difference lives in the adapter object.

01 The SourceAdapter contract

Adding a provider is one file plus one line

Every adapter implements the SourceAdapter contract (server/src/sources/types.ts) and is registered in server/src/sources/registry.ts. The generic core (buildSource) runs the same pipeline for all of them — the whole point is that it never branches per source.

the generic sync pipeline listChannels → normalize → dedupe → docs
listChannels
raw catalog
the provider's current channels
normalize
one schema
into sourcechannels
dedupe
by id
deterministic _id
project
playlistchannels
the editable store

The adapter supplies the per-source hooks the core calls at the right moments:

  • listChannels / normalize — populate the catalog on sync (the two above).
  • resolveStream / proxy — turn a stored sentinel into a real, playable upstream at play time. These are live — the Rust data-plane calls them per stream through the resolve seam.
  • afterSync — optional; write self-EPG (epgsources/epgchannels/programs) off the same listing the sync already fetched.
the rule

Boot init, the manifest, and the SPA all iterate the registry. To add a source: write adapters/<id>.ts, add one line to registry.ts, and commit an offline seed-data/sources/<id>.snapshot.json. Never add a per-source if in the core.

02 The adapter taxonomy

Six families, one registry

Adapters differ mainly in how a stored entry becomes a playable URL. That resolution strategy sorts them into families — from proxy-only synthetics, to a single authenticated source, to the anonymous scrape / API / macro / identity groups.

registry.ts → families → adapters color = resolution strategy
registry.ts · SOURCES: SourceAdapter[]
Synthetic · proxy-onlysynthetic: trueno catalog · no shell row · manifest omits
direct
Imported
Passthrough for user-imported M3U URLs; identity resolve, any https allowed. Channels carry origin:'direct'.
hdhomerun
HDHomeRun
Local OTA/cable tuner; catalog import is live. Playback needs no remux any more — its bare TS socket is segmented in-engine under local origin.
local
Local Now
localnow://id?slug sentinel → rotating CDN master; dynamic SSRF allow.
AuthenticatedrequiresAuth: truethe only per-source auth surface
dulo
dulo.tv
Supabase session + device fingerprint, captured via a headful Chromium. dulo://channel/id → fresh playbackUrl per play. EPG: Gracenote crosswalk.
Scrape-basedhtml / mirrorcatalog from a rotating mirror · multi-hop resolve
dlhd
DaddyLive
Scraped rotating-mirror HTML directory; watch.php?id → 3-hop Referer-gated scrape. Segments disguised as image/pdf. Six independent embed providers per channel — see Player 1..6. EPG: crosswalk + self-EPG.
API sentinelopaque sentinelcatalog from JSON API · resolve = API call per play
tubi
Tubi.TV
tubi://channel/id → Tubi API per play; inline self-EPG on catalog rows.
xumo
Xumo Play
broadcast.json → 3-hop API resolve; paginated market guide.
stirr
STIRR
/playable → 1-hop POST; two-tier per-channel guide.
tcl
TCL TV+
format-stream-url → 1-hop POST; category schedule + batched detail.
pluto
Pluto TV
pluto://region/id → cached region boot + URL construct; per-region timelines. Gracenote crosswalk wired.
roku
The Roku Channel
roku://id → cached session boot + playId resolve; content-proxy fanout guide.
plex
Plex Live TV
plex://compoundId → anonymous JWT (no credentials) → signed library/parts master minted per play. Own per-channel grid self-EPG. Gracenote crosswalk wired.
Macro-fill__MACRO__ slotsstored HLS URL with slots · fill per play
samsung
Samsung TV Plus
jmp2.uk redirect → CDN master; self-EPG. Gracenote crosswalk wired.
lg
LG Channels
HLS URL with {MACRO} slots → expand per play; inline program self-EPG.
whale
Whale TV+
macro fill per play; separate /epg fetch (Vidaa shape).
distro
Distro TV
__MACRO__ VAST slots → fill per play; tvg_id-keyed self-EPG.
freelivesports
FreeLiveSports
device/cb/ref macro slots → fill per play; inline program self-EPG.
Identity / directreal HLS mastercatalog carries the master · pre-allow host only
vizio
Vizio WatchFree+
channelUrls[0] IS the real master; identity resolve + pre-allow host. Separate airings schedule.
vidaa
Vidaa Free TV
macros already expanded at catalog time; identity resolve. Self-EPG via uid. Gracenote crosswalk wired.
03 Key properties, at a glance

The resolve / EPG matrix

The same 20 adapters, sorted by how they resolve a stream and where their guide data comes from.

AdapterAuthResolve strategySelf-EPGGracenote xwalk
directIdentity (passthrough)
hdhomerunCatalog import (playback via local origin)
localSentinel → rotating CDN
dulosessiondulo:// → playbackUrlyes
dlhdwatch.php → 3-hop scrape, 6 providersyesyes
tubitubi:// → Tubi APIyes (inline)
xumobroadcast.json → 3-hop APIyes
stirr/playable → 1-hop POSTyes
tclformat-stream-url → POSTyes
plutopluto:// → region bootyesyes (wired)
rokuroku:// → session + playIdyes
plexplex:// → anon JWT + signed masteryes (grid)yes (wired)
samsungjmp2.uk redirectyesyes (wired)
lg{MACRO} fill per playyes
whalemacro fill per playyes
distro__MACRO__ fill per playyes
freelivesportsmacro fill per playyes
vizioIdentity (direct master)yes (airings)
vidaaIdentity (macros pre-expanded)yesyes (wired)
04 Custom playlists

Bring your own channels

Beyond the built-in adapters, you can compose your own playlists. Every custom playlist rides the same per-user, token-gated .m3u + XMLTV export machinery as the built-ins.

● clone

Curate from any source

Hand-pick channels from any synced source into a curated playlist. The channels are independent copies (edits don't disturb the originals) but streams still route through the real adapter.

● url import

A remote M3U

Pull in any remote .m3u / .m3u8 URL. Re-syncable via the stored remoteUrl.

● file upload

A static M3U file

Upload a static .m3u; parsed once from the uploaded file (no sync).

● hdhomerun

A LAN tuner

Point at a LAN HDHomeRun tuner (deviceUrl); its channel lineup is imported. Playback no longer waits on a remux: with originEnabled the tuner's bare MPEG-TS socket is cut into segments in-engine and republished as ordinary HLS.

05 DaddyLive players

Six buttons, six different companies

A DaddyLive channel page offers PLAYER 1..6. It is tempting to read those as redundant embeds of one feed. They are not: each button loads a different third-party provider, and they do not all carry the same channels.

one channel, verified live

Ch 648 (Boomerang USA): Player 1's own CDN 404s it. Players 2, 3, 5 and 6 are Cloudflare-gated, dead or NXDOMAIN. Only Player 4 — an entirely separate operator — actually carries it. Picking a player picks a provider, and which provider works varies per channel and drifts over time.

The resolver is built around that fact rather than around a fixed player number:

● provider-agnostic hop 2

Any iframe is a candidate

The /premiumtv/ embed is simply tried first. The signed playlist URL is then read by an ordered chain of extractors — base64/atob, plaintext, an XOR-array eval blob, a packed payload, hex escapes — each recovering whatever constants the page carries instead of hardcoding them, so a key rotation self-heals.

● both shapes are valid

Master or media playlist

Providers return either a master (#EXT-X-STREAM-INF) or a media playlist (#EXTINF); both are accepted. An #EXTM3U with neither is now rejected outright rather than served as an empty stream.

● learned + sticky

The winner is remembered

A winning player is cached per channel for about 30 minutes and a failing one is burnt for about 5, so the common case stays a single hop-1 fetch even when the winner isn't Player 1. Your pick always leads; the rest are the fallback order.

● bounded

A hang can't outlast the player

Every hop has an 8-second timeout and the whole walk a 20-second deadline, so a wedged provider cannot outlast a media player's own manifest timeout.

Where you set it: Settings → Advanced → DaddyLive Player Source picks the source-wide default (Auto leads with Player 1), and the channel drawer carries a per-channel override. At play time the data plane's first failover attempt re-resolves the same channel through a different provider — burning the one that was serving — and only then starts walking any configured failover-group children. Active Streams badges the result failover → Player 4.

expect this layer to churn

These providers are third parties that rotate, so this is the most churn-prone part of the adapter by design — the extractor chain exists so a new obfuscation is a small addition rather than a rewrite. When DaddyLive stops carrying a channel on every player, no amount of player-walking helps: failover groups are the durable answer.

06 Local Now — a deep dive

A fully-managed, market-scoped custom playlist

Local Now is a US FAST service that delivers a market-specific lineup — local broadcast stations, regional news, and national FAST networks — curated by geographic TV market. masqueradarr integrates it as a re-syncable custom playlist with a bundled EPG guide.

US-only

Local Now is geo-gated to US IP addresses. Adding one from a non-US IP returns a clear error — there's no built-in VPN workaround; route the server through a US network to use it.

Picking a market

Each playlist is scoped to a city/market, chosen two ways: a city search typeahead (GET /api/import/local/cities?q=), or auto-detect ("use my detected market", read from Local Now's homepage geo signal, falling back to New York). Every choice resolves to two stored identifiers:

FieldWhat it isExample
marketDmaA numeric Designated Market Area code — the Nielsen/TV-industry ID for a regional TV market.501 (New York)
marketSlugA comma-joined list of Local Now market slugs — a primary city slug plus any associated PBS station slugs.nyNewYorkCity,pbs-wnet,…

What you get

  • Immediate sync — creates a custom playlist row (source:'local', endpoint:'custom') and syncs the market's lineup at once; subscription-locked channels are pruned, the rest deduped by id.
  • Self-linked EPG — each playlist owns a playlist-bound EPG source (playlistBinding:true, id = playlist id). Channels are self-linked at sync — no manual Channel Mapping needed — and the guide renders immediately. Guide data is inline program[] from the same catalog fetch.
  • Hourly refresh — the playlist's own hourly cronjob drives all refreshes; the EPG Sources screen hides its manual controls.
  • Many markets, fully independent — add one playlist per market; each gets its own channels, EPG source, and hourly schedule. Cascade-delete removes a market's playlist, EPG, guide, and cronjob together.
README · Pluggable sources · Channel Adapter Architecture · Local Now restapi-sources skill · the adapter contract .claude/docs/*-datasource.md · per-source detail next → EPG & guide data
masqueradarr · sources & adapters next → epg & guide data