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.
SourceAdapter contractAdding 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 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 arelive— 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.
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.
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.
origin:'direct'.localnow://id?slug sentinel → rotating CDN master; dynamic SSRF allow.dulo://channel/id → fresh playbackUrl per play. EPG: Gracenote crosswalk.watch.php?id → 3-hop Referer-gated scrape. Segments disguised as image/pdf. EPG: crosswalk + self-EPG.tubi://channel/id → Tubi API per play; inline self-EPG on catalog rows.broadcast.json → 3-hop API resolve; paginated market guide./playable → 1-hop POST; two-tier per-channel guide.format-stream-url → 1-hop POST; category schedule + batched detail.pluto://region/id → cached region boot + URL construct; per-region timelines. Gracenote crosswalk wired.roku://id → cached session boot + playId resolve; content-proxy fanout guide.plex://compoundId → anonymous JWT (no credentials) → signed library/parts master minted per play. Own per-channel grid self-EPG. Gracenote crosswalk wired.{MACRO} slots → expand per play; inline program self-EPG./epg fetch (Vidaa shape).__MACRO__ VAST slots → fill per play; tvg_id-keyed self-EPG.channelUrls[0] IS the real master; identity resolve + pre-allow host. Separate airings schedule.The resolve / EPG matrix
The same 20 adapters, sorted by how they resolve a stream and where their guide data comes from.
| Adapter | Auth | Resolve strategy | Self-EPG | Gracenote xwalk |
|---|---|---|---|---|
| direct | — | Identity (passthrough) | — | — |
| hdhomerun | — | Catalog import (playback dormant) | — | — |
| local | — | Sentinel → rotating CDN | — | — |
| dulo | session | dulo:// → playbackUrl | — | yes |
| dlhd | — | watch.php → 3-hop scrape | yes | yes |
| dami | — | dlhd resolveStream (shared) | yes | yes |
| tubi | — | tubi:// → Tubi API | yes (inline) | — |
| xumo | — | broadcast.json → 3-hop API | yes | — |
| stirr | — | /playable → 1-hop POST | yes | — |
| tcl | — | format-stream-url → POST | yes | — |
| pluto | — | pluto:// → region boot | yes | yes (wired) |
| roku | — | roku:// → session + playId | yes | — |
| plex | — | plex:// → anon JWT + signed master | yes (grid) | yes (wired) |
| samsung | — | jmp2.uk redirect | yes | yes (wired) |
| lg | — | {MACRO} fill per play | yes | — |
| whale | — | macro fill per play | yes | — |
| distro | — | __MACRO__ fill per play | yes | — |
| freelivesports | — | macro fill per play | yes | — |
| vizio | — | Identity (direct master) | yes (airings) | — |
| vidaa | — | Identity (macros pre-expanded) | yes | yes (wired) |
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.
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.
A remote M3U
Pull in any remote .m3u / .m3u8 URL. Re-syncable via the stored remoteUrl.
A static M3U file
Upload a static .m3u; parsed once from the uploaded file (no sync).
A LAN tuner
Point at a LAN HDHomeRun tuner (deviceUrl); its channel lineup is imported. Playback (TS→HLS remux) is dormant pending the video engine.
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.
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:
| Field | What it is | Example |
|---|---|---|
| marketDma | A numeric Designated Market Area code — the Nielsen/TV-industry ID for a regional TV market. | 501 (New York) |
| marketSlug | A 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 inlineprogram[]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.