BGPVR Operator guide
Operator guide

Public Playlist Pro

Needs a Pro licence or higher.

An aggregator for public IPTV lists (iptv-org and friends): fetch third-party M3Us, filter them by country, health-check the streams, and serve the merged result as one unauthenticated M3U at /api/public/playlist.m3u. Completely separate from Channels and from Streams — nothing here is ever offered to subscribers as part of a package.

On this page
  1. Turn the module on and find the view
  2. Add sources
  3. Health-check the streams
  4. Publish and filter the playlist
  5. Keep the list tidy
  6. Watch out

Turn the module on and find the view

Modules → Utilities → tick "Public Playlist Aggregator" → Save. The sub-panel shows the Public M3U URL with a Copy button and a "Manage Sources →" link. The Public Playlist item in the left nav stays hidden until this setting is 1, and only for the admin role.

Add sources

Header → "Add Source" → Label, Source URL, optional Country Filter (comma-separated codes, e.g. BG,UK; blank imports all). Paste a GitHub folder URL and a "Browse" button appears that enumerates the .m3u files in that tree so you can bulk-add them. Header → "Upload M3U" registers a local file as a source instead (refresh is a no-op for uploaded sources). Fetching runs in the background — the row's Channels and Last Fetched columns fill in a few seconds later.

Health-check the streams

Per source: the heartbeat button in the row ("Test streams") shows a live done/total progress bar. All sources: header → "Check All" runs them one at a time on the server. Results appear as alive / dead / geo-blocked counts per source and as extra stat cards at the top. HTTP 451, and 403 with a geo keyword in the body, are classified geo-blocked rather than dead.

Publish and filter the playlist

Copy the Public URL from the card at the top of the view. Query filters: ?country=BG,UK, ?group=…, ?logo=1, ?epg=1, ?alive=1, ?source=<id>, ?q=…, ?limit=… (default 2000, capped at 5000). Once a check has run, "Copy (alive only)" gives you the URL with alive=1 appended. Output is grouped by country name with flag emoji and gzipped when the client asks for it.

Keep the list tidy

The pencil opens an inline row editor for Label / URL / Country Filter. The Enabled toggle immediately removes a source's channels from the public M3U and from the stats without deleting anything. The circular-arrow icon re-fetches one source; header → "Refresh All" re-fetches everything; "Fetch Logos" fills in logos only for channels that have none. All sources are also re-fetched automatically once a day at 03:00 server local time.

Watch out

  • /api/public/playlist.m3u is unauthenticated by design — anyone with the URL gets the list. It returns 404 while public_playlist_enabled is 0, so switching the module off is the kill switch.
  • ?alive=1 filters on (stream_status='ok' OR 'unknown'), and every channel starts as 'unknown'. Before you have run a check it therefore serves everything; it really means "not known dead".
  • The delete confirmation says "Delete this source and all its channels?", but SQLite foreign keys are not enabled on these connections, so the ON DELETE CASCADE does not fire: the channel rows stay in the database. They stop being served and stop being counted (every query joins the sources table), but they are not actually removed.
  • Stream checks run from the server. Geo-restricted streams (e.g. UK-only) show as dead or geo-blocked even though they work for viewers in the right region — the view says so in a banner. Unlike the relay Health Check, this never deletes anything: it only marks status.
  • Every /api/admin/public-playlist/* endpoint is admin_required, so staff accounts with delegated permissions cannot use this view at all.
  • Refreshes are conditional (If-Modified-Since / If-None-Match); a source that answers 304 is left untouched and only its Last Fetched timestamp moves. Uploaded sources have no URL and are skipped entirely by Refresh All.
  • The Public URL shown in the card comes from the public_site_url setting; if that is unset it falls back to a relative path (and to the browser's origin in the view), which is not what you want to hand out. Set it in Settings first.
  • Re-fetching a source replaces that source's channels wholesale, so any per-channel state — including logos added by "Fetch Logos" and the alive/dead check results — is lost on the next refresh.