package icecast import ( "strings" "sync" ) type RadioTextOptions struct { Enabled bool Prefix string MaxLen int OnlyOnChange bool } func mapStreamTitleToRadioText(streamTitle string, opts RadioTextOptions) string { if !opts.Enabled { return "" } maxLen := opts.MaxLen if maxLen <= 0 || maxLen > 64 { maxLen = 64 } title := sanitizeASCII(streamTitle) if title == "" { return "" } prefixRaw := opts.Prefix prefixHadTrailingSpace := strings.TrimRight(prefixRaw, " \t\r\n") != prefixRaw prefix := sanitizeASCII(opts.Prefix) if prefix != "" && prefixHadTrailingSpace { prefix += " " } rt := title if prefix != "" { rt = prefix + title } if len(rt) > maxLen { rt = strings.TrimSpace(rt[:maxLen]) } return rt } func sanitizeASCII(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } var b strings.Builder b.Grow(len(raw)) prevSpace := true for _, r := range raw { switch r { case '\n', '\r', '\t': r = ' ' } if r < 0x20 || r == 0x7f || r > 0x7e { continue } if r == ' ' { if prevSpace { continue } prevSpace = true b.WriteByte(' ') continue } prevSpace = false b.WriteByte(byte(r)) } return strings.TrimSpace(b.String()) } type RadioTextRelay struct { opts RadioTextOptions apply func(string) error mu sync.Mutex lastRT string } func NewRadioTextRelay(opts RadioTextOptions, initialRT string, apply func(string) error) *RadioTextRelay { return &RadioTextRelay{ opts: opts, apply: apply, lastRT: sanitizeASCII(initialRT), } } func (r *RadioTextRelay) HandleStreamTitle(streamTitle string) error { if r == nil || r.apply == nil { return nil } next := mapStreamTitleToRadioText(streamTitle, r.opts) if next == "" { return nil } r.mu.Lock() skip := r.opts.OnlyOnChange && next == r.lastRT if !skip { r.lastRT = next } r.mu.Unlock() if skip { return nil } return r.apply(next) }