masqueradarr
playlist failover · 04 channel backups

Playlist Failover — a channel with a stand-in

The same real-world channel usually exists on several providers. Group them — one parent, an ordered line of backup children — and masqueradarr quietly serves the next one alive when the first won't start.

01 Start from zero

Why a channel needs a backup

masqueradarr never stores a stream URL. It stores a pointer and resolves the real stream the moment you press play (that's the Playlists story). Usually that works. Sometimes it doesn't: the provider's scrape breaks, its token expires, its edge returns 403, or the whole service is down for the evening. The channel is in your playlist, your player asks for it, and nothing arrives.

The thing is, that channel is rarely unique. "ESPN" exists on several of the providers masqueradarr aggregates. If one is dark at 8pm, another is very likely fine. A failover group is you telling masqueradarr about that fact once, so it can act on it every night without you.

● the problem

One pointer, one upstream

An ordinary channel resolves through exactly one provider. If that resolve or that fetch fails, the play fails. Your player shows an error and moves on.

● the fact

The same channel, elsewhere

The same broadcast is carried by other providers in your catalog. They have different names, different logos, different guide ids — but the same picture.

● the fix

An ordered line of stand-ins

Name one channel the parent and rank the others behind it. On an establish failure the engine walks the line and serves the first one that answers.

what your viewer sees

Nothing. A group is invisible downstream: your playlist still carries one line for that channel, with the parent's name, number, logo and guide. When a backup is carrying the stream, it is served under the parent's URL and identity — the player never learns it switched providers.

02 The group

One parent, ordered children

A failover group is not a new collection — it's three fields written onto the channel rows you already have in playlistchannels. Every member of a group is an ordinary channel that has been given a role.

● exactly one

The parent

The channel the world sees. It is the one exported to your .m3u, the one your player requests, and the owner of the group's EPG identity. Everything about the group — its name, number, logo, guide link — is the parent's.

● one or more, ranked

The children

Silent backups, tried in order. Each is hidden from every export, inherits the parent's guide link, and keeps its own provider for routing — so a child can come from a completely different service than its parent.

Field on the channel rowValuesWhat it means
failoverGroupIdopaque id, or nullWhich group this channel belongs to. null = ungrouped. Never derived from the parent, so swapping the parent doesn't churn it.
failoverRoleparent · child · nullThe role within the group. Exactly one parent per group.
failoverOrder0…N-1, or nullA child's position in the line. Lower is tried first. null on the parent.
the four invariants

A group has exactly one parent and at least one child — anything else is degenerate and gets disbanded automatically. A channel belongs to at most one group. A group lives entirely inside one playlist — there is no such thing as a cross-playlist group. And these three fields are operator-owned: no sync ever writes them, so a group survives a re-sync untouched.

03 The contract

What a group changes — and what it doesn't

Grouping is a publishing decision as much as a resilience one. The moment you save a group, its children leave your published catalog.

a group is one exported line select · group · store · export · play
Select
2+ channels
one real-world channel, several providers
Group
pick a parent
drag the rest into priority order
Store
three fields
groupId · role · order — on the channel rows
Export
one .m3u line
the parent only · one guide channel
Play
walk on failure
first live child wins, silently
  • One line out, always. The composed .m3u and its XMLTV guide contain the parent and nothing else. Children are filtered out of every export surface — the Global M3U, per-user exports, and per-playlist custom exports all run through the same filter.
  • Children stay fully visible in the app. They remain in the playlist detail view, nested under their parent and badged child. Hiding them from a file is not the same as deleting them.
  • Children are still health-probed. The scheduled channel probe keeps testing hidden children on their own, so a dead backup shows up as dead before failover ever needs it. Failover can never mask a dead parent as healthy either — the probe always resolves each channel itself.
  • Status is still the governor. A Disabled child is skipped at failover. A Disabled parent hides the whole group from exports — nothing about that channel ships until you re-enable it.
  • The guide identity is the parent's. On save, every child's tvg_id and EPG link are overwritten with the parent's. That is what keeps the exported line coherent no matter which member is actually serving.
04 Walkthrough

Build a cross-provider group, step by step

The payoff of failover is a backup on a different provider — if all your backups sit behind the upstream that just died, you have gained nothing. A group lives inside a single playlist, and a built-in playlist only ever holds one provider's channels. So the first job is to gather the candidates into one custom playlist. That's what Create and Append are for.

gather → group two providers, one channel, one group
1
Playlists → provider A → detail
Pick your primary and clone it out
Select the channel you want to keep (checkbox, or Ctrl/Cmd+click a row). Hit Create in the selection toolbar and name a new custom playlist. this becomes the clone that hosts the group
2
Playlists → provider B → detail
Append the same channel from another provider
Find the equivalent channel, select it, and hit Append — choosing the clone you just made. Repeat for each backup provider you want in the line.
3
Playlists → your clone → detail
Select every member of the group
Tick the checkbox on each one. Ctrl/Cmd+Shift+click range-selects. A “{n} selected” pill appears with the selection toolbar.
4
Selection toolbar → Group
Open the group modal
The Group button (chain icon) opens New failover group. Its banner restates the deal: the parent is exported and served; children are hidden, inherit the parent's EPG link, and are tried in order.
5
The modal
Name the parent, rank the children
The first selected channel is proposed as Parent. To promote a different one, hit Set parent on its row — the old parent demotes into the child list in place. Then drag the rows under Children · failover order by their grip. Position 1 is tried first.
Save group
The group is live
Children inherit the parent's guide link, drop out of the export, and the playlist recomposes. A toast confirms: “Failover group saved · N backup(s) behind …”. nothing else to configure — failover is on by default
two selection gotchas

A plain click on a row when two or more are already selected opens the bulk edit drawer, not the group modal — build your selection with checkboxes or modifier-clicks. And while the Group button lights up at one selected channel, Save group stays disabled until you have a parent and at least one child; the modal will tell you so: “Select at least two channels — one parent plus one or more backups.”

Playlist detail view — the channel table where channels are selected and grouped
Playlist detail · select channels here, then Group
05 Managing a group

Living with it afterwards

Once saved, the group becomes a small tree in the playlist's channel list: the parent, then its children indented beneath it with a connector, in failover order.

● reading the list

Pills and a collapse arrow

A parent carries a parent pill and a “{n} backup(s)” count, plus a chevron that collapses its children out of the way. Each child carries a child pill. The same pills follow the channel into the grid view, the channel drawer, Channel Mapping and Active Streams.

● changing it

The Group actions menu

Every parent row has a waffle menu with Edit group (reopens the modal, back-filled with the whole group) and Disband group. Inside the modal you can re-drag the order, Set parent to promote a child, or add members by re-selecting a wider set before opening it.

The rules the UI enforces for you

  • A child's guide link is read-only. In the channel drawer a child's TVG-ID field is disabled and labelled · inherited, with the hint “Inherited from the group parent — edit the parent's EPG link instead.” The server refuses a direct write with 409 failover_child_epg_locked. Channel Mapping won't let you select a child, and auto-match skips children entirely.
  • Editing the parent's guide link cascades. Relink the parent — in the drawer, on the Mapping screen, or via a bulk edit — and every child is updated in the same request. Open screens update live; you don't need to re-sync or reopen anything.
  • A channel already in another group is handled, not rejected. Pull a foreign child into a new group and it's badged “will be moved” — on save it changes hands and its old group is repaired. Pull in a foreign parent and Save is blocked: “… already heads another failover group — disband that group first.”
  • Everything else about a child stays editable. Rename it, renumber it, change its group-title, disable it. Only its EPG identity is owned by the parent.
  • A group can never go headless. Any prune or delete that removes the parent — or the last child — auto-disbands the group. It never promotes a child in its place, because that would silently change which stream is the exported one.
disband is immediate

Disband group has no confirmation dialog — from the modal footer or the row menu, it fires at once. It is not destructive: members simply lose the grouping, keep the guide link they inherited, and the children re-enter the export as ordinary channels. But the ordering and the parent choice are gone; rebuilding means re-selecting and re-dragging.

06 At play time

What happens when the parent dies

Failover is an establish-time mechanism. It fires when a stream fails to start, not mid-picture. The Rust data plane keeps a per-stream cursor — an attempt number — and asks Node's resolve seam for that attempt's candidate. Attempt 0 is the channel itself; attempt N is its Nth Active child.

the cursor walk — attempt 0 → 1 → 2 rust cursor ⇄ node resolve seam
the walk the per-stream cursor steps down the candidates — one question to the seam per attempt, answered 200 / 502 / 410.
  • 200 · grant → serve
  • 502 · died → next
  • 410 · none left → stop
parent · dlhd
the ordinary path — parent's own adapter, full retry & backoff budget. Establish can fail on a dead scrape or expired token, on transport give-up after the retry budget, or — opt-in only — a definite 4xx / 5xx.
502 · didn't establish · next
child · 1 · pluto
its own provider's adapter — own headers, own SSRF allow-set · one shot, no backoff
502 · died · try next
child · 2 · tubi
one shot, no backoff · resolves alive under its own provider's policy key
200 · grant · serve pinned · session
end of line
had every candidate refused, attempt 3 answers 410 · nothing left · stop — not reached here: attempt 2 answered 200. An ungrouped channel costs exactly this one extra 410 before it fails the way it always has.
served under the parent's URL · name · guide
wraps to the parent once · capped at 8 attempts the cursor pins on the winner for the session — only a 2xx keeps the pin; the next play starts from the parent again.
downstream · player one .m3u line — the parent's name · number · guide, unchanged. The player never learns it switched providers. live · switch unseen
the failover walk rust data plane ⇄ node resolve seam
1
attempt = 0
Try the channel itself
Byte-for-byte the ordinary path — the parent's own adapter resolves the stream, with the full retry-and-backoff budget. identical to an ungrouped channel · no extra database work
2
establish fails
Decide whether to walk
resolve failed — dead scrape, expired token transport failure after the retry budget a definitive 4xx / 5xx — only if you opt in
3
attempt = 1, 2, …
Walk the children in order
Each candidate is resolved through its own provider's adapter — its own headers, its own SSRF allow-set. Backups get one shot, no backoff, so a three-deep group still establishes inside a player's manifest timeout.
4
the seam replies
Three possible answers
200 · a grant — serve it 502 · this one died, try the next 410 · nothing left, stop walking
5
first candidate to answer 2xx
Serve it under the parent's identity
Same URL, same channel name, same guide. Active Streams badges the session failover → <child> so you know, even though the viewer doesn't.
stick on the winner
Pin the cursor for the session
The stream never walks back to the dead parent mid-play. The pin releases only after the stream sits idle — playback stopped — for about five minutes, and the next play starts from the parent again. only a 2xx keeps the pin; every other exit resets it
why a backup from another provider is safe

The data plane caches one shared policy — headers, relabel rules, allowed hosts — per provider. A child from a different service could have clobbered its parent provider's policy for every other stream in flight. It can't: the grant names the serving candidate's provider, and the policy is filed under that key. A dlhd parent backed by a pluto child never disturbs dlhd's other viewers.

bounded on purpose

The walk is capped at 8 attempts and wraps back to the parent once — so a stale pin can never spin. An ungrouped channel costs exactly one extra question to the seam (answered 410 immediately, from a partial index) before it fails the way it always has.

07 The two switches

Settings → Advanced → Video proxy engine

Both live in the proxyconfigs subsystem alongside the rest of the engine's knobs, so both can be set globally or overridden for a single playlist from its drawer. The panel auto-saves — there is no Save button. (Full detail on the two tiers is on the Video proxy engine page.)

● failover groups · default ON

failoverEnabled

“When a channel's stream fails to establish, fall through to its configured failover backups in order.” On by default, because configuring a group is the real opt-in — an ungrouped channel behaves identically either way. Turn it off to make every channel fail like an ungrouped one, without disbanding anything.

● failover on upstream error · default OFF

failoverOnDefiniteError

“Also treat a definitive upstream error response (404 / 403 / 5xx — normally passed through to the player) as a failover trigger.” Off by default because it changes long-standing pass-through semantics. Turn it on when a provider answers politely with a 403 rather than simply going dark.

reading the difference

With the second switch off, a provider that returns a clean 403 is a deliberate answer — masqueradarr forwards it verbatim and your player shows the error. With it on, that 403 becomes just another reason to try the next backup, and only the last definitive answer is forwarded if every candidate refuses.

08 The rules

What you can and cannot do

One habit matters more than all the others, and the UI will happily let you get it wrong.

● do this

Group inside a custom playlist

Clone your primary channel into a new custom playlist, Append its equivalents from other providers, and build the group there. The clone's channels each remember their own origin, so a single group can span dlhd, pluto, tubi — whatever you have.

The playlist is Custom-endpoint, so it publishes at its own path with its own guide, and hiding the children affects nothing but that playlist.

● don't do this

Group inside a built-in playlist

The Group button works on a (Default) source playlist, but you should not use it there. A group built on a built-in is fragile, changes what everyone else receives, and can't give you the one thing failover is for.

It is allowed rather than blocked because the machinery is generic — not because it's a good idea.

Five reasons, concretely

  • A built-in holds one provider's channels. Every “backup” you can pick shares the upstream that just died. Cross-provider redundancy is only reachable by Append-ing into a clone — that is the whole point of the walkthrough above.
  • Built-ins re-sync from the live provider. Your group survives the upsert, but any sync that prunes the parent (or the last child) because the provider renamed or dropped it will auto-disband the group. Groups quietly evaporate over time, and nothing tells you.
  • Built-ins are Global-endpoint. Hiding a child removes it from the consolidated Global M3U for every user of that playlist, not just you.
  • Cloning does not carry the group. Clone copies deliberately start ungrouped — the three fields are stripped. So the group you built on a built-in never transfers into the custom playlist you eventually want.
  • Reset wipes it. A source playlist's Reset deletes and rebuilds its channel rows from scratch. The group goes with them.
and one that isn't a matter of taste

The in-app player requests a channel without naming a playlist. When that happens the resolver prefers the canonical source-playlist row over any clone copy of the same stream. So a group left behind on a built-in will shadow the group you built on your clone, for in-app playback of that channel. Clean up stray built-in groups.

Can you…AnswerWhy
Back a parent with a child from a different providerYesThe child resolves through its own adapter under its own policy key.
Group channels from two different playlistsNoA group lives inside one playlist. Bring them together with Append first.
Put one channel in two groupsNoMembership is a single field on the row.
Pull in a channel that heads another groupNo — 409It would leave that group headless. Disband it first.
Pull in a channel that is a child of another groupYesBadged “will be moved”; it changes hands and the donor group is repaired.
Build the group on a clone / custom playlistYes — do thisStable, isolated, and the only way to mix providers.
Build the group on a built-in (Default) playlistPossible — don'tSame-provider only, disbanded by prunes, and it changes the Global M3U.
Edit a child's TVG-ID / EPG linkNo — 409Inherited from the parent. Edit the parent; it cascades.
Rename, renumber or disable a childYesOnly the guide identity is owned by the parent.
Keep a Disabled child in the groupYesIt is simply skipped at failover, and re-enters the line when re-enabled.
Disable the parentCarefulThe whole group leaves the export until it's re-enabled. The modal warns you.
Reorder the children after the factYesEdit group → drag → Save group. No EPG cascade; order only.
09 Honest scope

Known limits

Failover is deliberately narrow. It makes a channel start; it does not make a channel unbreakable.

● establish-time only

No mid-segment splice

A parent that dies while you're watching is caught on the player's next manifest refetch, not instantly. Seamless mid-stream splicing is a future enhancement.

● raw TS

Coarser trigger

In a continuous raw-TS session, failover triggers on a playlist-refresh failure rather than a per-segment gap. The cursor is kept alive across the swap.

● HDHomeRun

Not a usable backup

HDHomeRun channels still can't be served (TS→HLS remux is pending), so an HDHomeRun child will always be walked past.

● clone-hosted children

Health via propagation

A child living in a clone gets its health from the probe propagating along its origin, not from a direct sweep of the clone.

watching it work

Two places tell you a group is earning its keep: Active Streams badges a failed-over session failover → <child> with the backup's position, and the dedicated failover log category records the whole lineage — Node names the parent and playlist, the Rust engine carries the session id. Both are gated by the single log level on Settings. See Operations.

README · Failover groups (channel backups) services/failover.ts · cascade & reconcile proxy/src/proxy.rs · failover_walk next → Sources & adapters
masqueradarr · playlist failover next → sources & adapters