Add Enigma2/OpenWebif M3U support: program mapping and zap-before-streaming

- Parse #EXTVLCOPT:program=N from M3U playlists (both full and streaming
  parsers, per channel, strict mode) and store it on playlist items
- Transcode multi-program MPEG-TS streams by mapping only the selected
  program's first video and audio stream (DVB teletext/subtitles in the
  program otherwise make FFmpeg fail to produce HLS)
- Optional per-source "zap before streaming" with configurable delay:
  POST /api/openwebif/zap takes sourceId + streamId, requires an enabled,
  zap-enabled M3U source containing that channel, derives the receiver
  from the configured source URL (never from the client), validates the
  Enigma2 service reference, uses a timeout without redirects and does
  not echo the receiver response
- Live TV: Stop button, and stop playback when leaving the page
- Tests for ordinary and program-tagged playlists and the zap endpoint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vitor Simoes
2026-10-13 23:01:38 +01:00
parent 035667cfba
commit 700f63da90
17 changed files with 415 additions and 19 deletions

View File

@ -1,3 +1,5 @@
'use strict';
/**
* M3U Playlist Parser (Streaming)
* Parses EXTM3U format playlists and extracts channel information line-by-line
@ -88,6 +90,7 @@ async function parse(input) {
const groupsSet = new Set();
let currentInfo = null;
let currentGroup = null;
let currentVlcProgram = null;
let lines;
@ -121,6 +124,11 @@ async function parse(input) {
if (currentInfo) {
currentInfo.groupTitle = currentGroup;
}
} else if (trimmed.startsWith('#EXTVLCOPT:program=')) {
currentVlcProgram = trimmed.split('=')[1]?.trim() || null;
if (currentInfo) {
currentInfo.program = currentVlcProgram;
}
} else if (!trimmed.startsWith('#')) {
// This is a stream URL
if (currentInfo) {
@ -132,9 +140,11 @@ async function parse(input) {
...currentInfo,
id: stableId,
url: trimmed,
program: currentVlcProgram,
groupTitle: groupTitle
});
currentInfo = null;
currentVlcProgram = null;
}
}
}
@ -187,6 +197,7 @@ async function* parseStreaming(input, batchSize = 500) {
const groupsSet = new Set();
let currentInfo = null;
let currentGroup = null;
let currentVlcProgram = null;
let batch = [];
let lines;
@ -216,6 +227,11 @@ async function* parseStreaming(input, batchSize = 500) {
if (currentInfo) {
currentInfo.groupTitle = currentGroup;
}
} else if (trimmed.startsWith('#EXTVLCOPT:program=')) {
currentVlcProgram = trimmed.split('=')[1]?.trim() || null;
if (currentInfo) {
currentInfo.program = currentVlcProgram;
}
} else if (!trimmed.startsWith('#')) {
if (currentInfo) {
const groupTitle = currentInfo.groupTitle || currentGroup || 'Uncategorized';
@ -225,9 +241,11 @@ async function* parseStreaming(input, batchSize = 500) {
...currentInfo,
id: stableId,
url: trimmed,
program: currentVlcProgram,
groupTitle: groupTitle
});
currentInfo = null;
currentVlcProgram = null;
// Yield batch when full
if (batch.length >= batchSize) {