masqueradarr
playlists · 03 the core data model

Playlists — the heart of masqueradarr

A playlist is an envelope; its channels live separately. Understand that split — and the two channel stores behind it — and everything else about the catalog falls into place.

01 A playlist is an envelope

The row, and the channels inside it

A Playlist is a row in the playlists collection — the envelope: name, hosted URL, endpoint mode, schedule, and state. Its channels live separately in playlistchannels, queried by the playlist's source (or clone) id. A playlist row with no channels is a valid, paused shell.

● the envelope

playlists row

Name · HOSTED AT url · endpoint mode (global/custom) · schedule (interval + auto) · state. It carries no channels itself.

● the contents

playlistchannels

The channel rows — name, number, logo, guide link, live status — keyed to the playlist's source id. This is what the detail view and the published M3U are built from.

why the split matters

Because the envelope and the channels are separate documents, a playlist can exist with zero channels (a paused shell provisioned but not yet synced), and the same channel store can be re-projected on every sync without touching the envelope's settings, schedule, or per-user grants.

02 Two channel stores

A pristine reference, projected into an editable copy

Every playlist is backed by two channel collections — both with a deterministic _id = "<source>:<id>", both empty until the first Sync now.

● sync-only

sourcechannels

The normalized, pristine reference written only by a sync — the source of truth. Not surfaced to the UI.

● editable · UI-facing

playlistchannels

The editable store, 1:1 with the runtime channel, built from sourcechannels at sync time via toPlaylistChannel. Channel/playlist routes return these docs verbatim.

your edits survive re-syncs — how

When you rename a channel, change its number, disable it, or set its guide link, you're editing playlistchannels. A re-sync merges: it $sets source-derived fields (so upstream changes flow through), $setOnInserts the user-editable fields (so your edits are never overwritten), and prunes channels that vanished upstream. Reset is the escape hatch — it drops the channels and re-syncs from scratch, discarding local edits.

never fabricated

Fields the source has no equivalent for (channel #, tvg_id, epg, stream.res) are stored as explicit null — never invented. Top-level status is the Active/Disabled governor; a Disabled channel is dropped from the published playlist without being deleted.

Three further fields on a playlistchannels row — failoverGroupId, failoverRole, failoverOrder — are operator-owned: no sync ever writes them, so a channel's failover grouping survives a re-sync alongside your other edits. That's the Playlist Failover story.

03 How a source reaches the UI

Provision → sync → normalize → project → play

The full lifecycle of a built-in source, from an empty shell to a channel you can stream. Nothing is written at boot — the pipeline only runs when you provision and sync.

source lifecycle the generic core · never branches per source
1
POST /api/sources/:id/provision
Provision a shell
ensureShellRow creates a zero-channel Playlist doc. Nothing is fetched yet.
2
Sync now
Fetch the live catalog
syncLiveadapter.listChannels() pulls the provider's current channel list.
3
adapter.normalize(raw)
Write the pristine reference
Raw rows become SourceChannel docs in sourcechannels — the normalized source of truth. afterSync() also writes epgsources / epgchannels / programs — self-EPG sources only
4
toPlaylistChannel
Project into the editable store
Each SourceChannel becomes a PlaylistChannel in playlistchannels. user edits preserved via $setOnInsert
5
SPA · GET /api/playlists/:id/channels
Surface in the UI
The SPA reads playlistchannels verbatim; the channels appear in the detail view, Dashboard counts, and Channel Mapping.
stream request → /api/v1/:source/:encUrl
Resolve & play
streamGate → proxyRelay → masq-proxy (Rust); the resolve seam calls resolveStream to fetch · rewrite · pipe. LIVE — HLS + raw-TS
04 Kinds of playlists

Built-in defaults, and bring-your-own

A playlist with its source set is a (Default) source playlist (id === source). Everything else is a custom playlist you compose yourself. All the type tags and modes are stored lowercase.

Kindsource tagCreated viaChannels
(Default) source<dynamic>Add Playlist → Built-InSynced from the adapter; id === source
ClonecloneAdd Playlist → CloneIndependent copies in playlistchannels; origin = the provider source for routing
URL importurlAdd Playlist → URLParsed from the upstream; re-syncable via the stored remoteUrl
File uploadfileAdd Playlist → FileParsed once from the uploaded .m3u
HDHomeRunhdhomerunAdd Playlist → HDHomeRunDiscovered from a LAN tuner; playback (TS→HLS remux) dormant, pending the video engine
a clone routes through the real adapter

A clone's channels are independent copies keyed to the clone, so editing them never disturbs the originals — but each remembers its origin provider, so a play still resolves through the right adapter's resolver. Streaming is always resolve-on-demand: the stream URL /api/v1/<source>/<enc-entry> is derived, never stored.

05 Grouped channels

A channel can have backups

Any playlist's channels can be gathered into a failover group: one parent plus an ordered line of children — silent backups for the same real-world channel, often from entirely different providers. Select the channels on the playlist detail screen, hit Group, pick the parent, drag the rest into priority order.

Only the parent is exported: the composed .m3u and its guide carry one line for the group, and the children inherit the parent's guide link. When the parent's stream won't start, the engine walks the children in order and serves the first one that answers — under the parent's URL and identity, so the viewer never notices.

group on a clone, not on a built-in

A group lives inside a single playlist, and a built-in (Default) playlist only ever holds one provider's channels — so every backup you could pick there shares the upstream that just died. Clone the channels into a custom playlist, Append the equivalents from other providers, and build the group there. The full reasoning, the walkthrough, and the play-time mechanics are on the Playlist Failover page.

06 On screen

List view and detail view

The list view shows every playlist as a row — name, source/type, status, channel count, schedule chips — built-in and custom side by side. The detail view is the channels inside one playlist, where you edit individual channels and run syncs.

Playlists list view
List view · every playlist as a row
Playlist detail view
Detail view · the editable channel store
07 Endpoint modes

Global vs. Custom — where a playlist is served

The endpoint mode decides how a playlist is exposed. Built-in defaults are Global by default; the custom kinds are Custom.

● global endpoint

The consolidated Global M3U

Served through the one Global M3U endpoint — a roll-up of every Active channel across all Global playlists, accessed as a single playlist.m3u.

● custom endpoint

Its own path + guide sibling

Served at its own path, riding the per-playlist export machinery — a dedicated .m3u with a matching XMLTV guide sibling.

  • The URL always prepends settings.domain. Change the Domain in Settings and it cascades — every playlist's url ("HOSTED AT") is rewritten automatically.
  • State pauses the endpoint. state:false makes downstream clients get a 404, without deleting anything.
  • Schedule drives refresh. interval + auto hand the playlist to the scheduler; a manual Sync now is always available.
08 How guide data binds

Two mechanisms — keep them separate

Guide data reaches a playlist two distinct ways. (The full EPG story — providers, the shared sync path, the compose pipeline — is the EPG & guide data page.)

● the everyday case

Channel-level linking

EPG attaches through a playlist's channels, not the row. Each channel carries a 2-factor (tvg_id, epg) link — set on the Channel Mapping screen (or self-linked by sources that ship their own guide). At compose time the guide is built from exactly the channels that carry a link, so many sources can feed one playlist's guide.

● built-in self-EPG

Playlist-bound (playlistBinding)

Some built-ins carry their own inline guide. Syncing such a playlist runs its afterSync hook, which writes the guide and upserts an EPG source flagged playlistBinding:true, then self-links the channels. The playlist drives their refresh, so the EPG Sources screen hides their sync/schedule controls — you never add or schedule them by hand.

09 Per-user access

Who can use which playlist

Access is a per-user allow-list, split to mirror the endpoint modes: allowedPlaylists (Global-endpoint) and allowedCustomPlaylists (Custom-endpoint). You assign membership on the Playlists screen, per playlist — not by editing the user. (Full auth & token detail is on the Users & access page.)

  • Admins ⇒ every playlist. An admin's allow-lists are materialized to hold every playlist id, and a new playlist auto-grants to all admins — so an admin always sees the full catalog.
  • Each user gets a tokenized URL. A personal .m3u + XMLTV guide link whose download is token-free but whose stream is token-gated to that user's allowed playlists.
  • Granting is what makes an account work. A standard user with nothing granted sees an empty Dashboard, gets a channel-less M3U, and can't stream.
README · Playlists schemas.md · §3.2 · sourcechannels → playlistchannels m3u skill · composition & export next → Playlist Failover
masqueradarr · playlists next → playlist failover