feat: idnorm and playlist support

This commit is contained in:
2026-05-06 11:05:20 +02:00
parent 6c90d765e7
commit 1c7de7174d
4 changed files with 383 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"bufio"
"errors"
"io"
"regexp"
"strings"
)
var (
tvgIDRe = regexp.MustCompile(`tvg-id="([^"]*)"`)
urlTvgRe = regexp.MustCompile(`url-tvg="([^"]*)"`)
)
// processPlaylist streams in→out, rewriting the #EXTM3U header so url-tvg
// points at epgURL. Returns the playlist's tvg-ids (in order, with duplicates)
// and the URL the playlist already declared in url-tvg (empty if absent).
//
// If epgURL is empty the header is passed through unchanged. The playlist
// must start with #EXTM3U after any leading blank lines, otherwise an error
// is returned without writing the header.
func processPlaylist(in io.Reader, out io.Writer, epgURL string) (tvgIDs []string, originalEPG string, err error) {
r := bufio.NewReader(in)
headerDone := false
for {
line, readErr := r.ReadString('\n')
if line != "" {
switch {
case !headerDone && strings.TrimSpace(line) == "":
// leading blank line — write through, keep looking for header
case !headerDone:
if !strings.HasPrefix(strings.TrimSpace(line), "#EXTM3U") {
return nil, "", errors.New("playlist: missing #EXTM3U header")
}
line, originalEPG = rewriteHeader(line, epgURL)
headerDone = true
default:
if strings.HasPrefix(line, "#EXTINF:") {
if m := tvgIDRe.FindStringSubmatch(line); m != nil {
if id := strings.TrimSpace(m[1]); id != "" {
tvgIDs = append(tvgIDs, id)
}
}
}
}
if _, werr := out.Write([]byte(line)); werr != nil {
return nil, "", werr
}
}
if readErr == io.EOF {
if !headerDone {
return nil, "", errors.New("playlist: missing #EXTM3U header")
}
return tvgIDs, originalEPG, nil
}
if readErr != nil {
return nil, "", readErr
}
}
}
func rewriteHeader(line, epgURL string) (rewritten, original string) {
if m := urlTvgRe.FindStringSubmatch(line); m != nil {
original = m[1]
if epgURL != "" {
line = urlTvgRe.ReplaceAllLiteralString(line, `url-tvg="`+epgURL+`"`)
}
return line, original
}
if epgURL == "" {
return line, ""
}
body, eol := splitEOL(line)
return body + ` url-tvg="` + epgURL + `"` + eol, ""
}
func splitEOL(s string) (body, eol string) {
n := len(s)
for n > 0 && (s[n-1] == '\n' || s[n-1] == '\r') {
n--
}
return s[:n], s[n:]
}