Files
cameradar/internal/output/m3u.go
T
Brendan Le Glaunec e81eeb0c4d feat: v6 rewrite
2026-01-27 22:11:17 +01:00

127 lines
3.0 KiB
Go

package output
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/Ullaakut/cameradar/v6"
"github.com/Ullaakut/cameradar/v6/internal/ui"
)
type m3uReporter struct {
delegate ui.Reporter
outputPath string
}
// NewM3UReporter wraps the provided reporter and writes an M3U playlist on summary.
func NewM3UReporter(delegate ui.Reporter, outputPath string) ui.Reporter {
return &m3uReporter{
delegate: delegate,
outputPath: strings.TrimSpace(outputPath),
}
}
func (r *m3uReporter) Start(step cameradar.Step, message string) {
r.delegate.Start(step, message)
}
func (r *m3uReporter) Done(step cameradar.Step, message string) {
r.delegate.Done(step, message)
}
func (r *m3uReporter) Progress(step cameradar.Step, message string) {
r.delegate.Progress(step, message)
}
func (r *m3uReporter) Debug(step cameradar.Step, message string) {
r.delegate.Debug(step, message)
}
func (r *m3uReporter) Error(step cameradar.Step, err error) {
r.delegate.Error(step, err)
}
func (r *m3uReporter) Summary(streams []cameradar.Stream, err error) {
r.delegate.Summary(streams, err)
if r.outputPath == "" {
return
}
writeErr := writeM3UFile(r.outputPath, streams)
if writeErr != nil {
r.delegate.Error(cameradar.StepSummary, writeErr)
}
}
func (r *m3uReporter) UpdateSummary(streams []cameradar.Stream) {
updater, ok := r.delegate.(interface{ UpdateSummary([]cameradar.Stream) })
if !ok {
return
}
updater.UpdateSummary(streams)
}
func (r *m3uReporter) Close() {
r.delegate.Close()
}
func writeM3UFile(path string, streams []cameradar.Stream) error {
content := BuildM3U(streams)
dir := filepath.Dir(path)
if dir != "." {
err := os.MkdirAll(dir, 0o750)
if err != nil {
return fmt.Errorf("creating output directory %q: %w", dir, err)
}
}
err := os.WriteFile(path, []byte(content), 0o600)
if err != nil {
return fmt.Errorf("writing m3u output: %w", err)
}
return nil
}
// BuildM3U creates an M3U playlist with discovered streams.
func BuildM3U(streams []cameradar.Stream) string {
var builder strings.Builder
builder.WriteString("#EXTM3U\n")
for _, stream := range streams {
url := formatRTSPURL(stream)
if url == "" {
continue
}
builder.WriteString("#EXTINF:-1,")
builder.WriteString(formatStreamLabel(stream))
builder.WriteString("\n")
builder.WriteString(url)
builder.WriteString("\n")
}
return builder.String()
}
func formatStreamLabel(stream cameradar.Stream) string {
label := stream.Address.String() + ":" + strconv.FormatUint(uint64(stream.Port), 10)
if stream.Device == "" {
return label
}
return label + " (" + stream.Device + ")"
}
func formatRTSPURL(stream cameradar.Stream) string {
path := strings.TrimSpace(stream.Route())
if path != "" && !strings.HasPrefix(path, "/") {
path = "/" + path
}
credentials := ""
if stream.CredentialsFound && (stream.Username != "" || stream.Password != "") {
credentials = stream.Username + ":" + stream.Password + "@"
}
return "rtsp://" + credentials + stream.Address.String() + ":" + strconv.FormatUint(uint64(stream.Port), 10) + path
}