diff --git a/internal/ingest/adapters/icecast/icy.go b/internal/ingest/adapters/icecast/icy.go index 5a69d43..138db16 100644 --- a/internal/ingest/adapters/icecast/icy.go +++ b/internal/ingest/adapters/icecast/icy.go @@ -77,18 +77,31 @@ func (r *icyReader) readMetadataBlock() error { return nil } +// parseICYMetadata parses the ICY inline metadata block. +// +// ICY metadata is a semicolon-delimited key=value format where values are +// single-quoted strings. A naive strings.Split(raw, ";") breaks when the +// StreamTitle itself contains semicolons (e.g. "Artist - Title; Live Edit"). +// This parser is quote-aware: it only splits on semicolons that appear +// outside of single-quoted value strings. func parseICYMetadata(block []byte) icyMetadata { raw := strings.TrimRight(string(bytes.Trim(block, "\x00")), "\x00") meta := icyMetadata{} - for _, field := range strings.Split(raw, ";") { + + fields := splitICYFields(raw) + for _, field := range fields { field = strings.TrimSpace(field) if !strings.HasPrefix(field, "StreamTitle=") { continue } v := strings.TrimPrefix(field, "StreamTitle=") v = strings.TrimSpace(v) - if len(v) >= 2 && ((v[0] == '\'' && v[len(v)-1] == '\'') || (v[0] == '"' && v[len(v)-1] == '"')) { - v = v[1 : len(v)-1] + // Strip enclosing single or double quotes. + if len(v) >= 2 { + if (v[0] == '\'' && v[len(v)-1] == '\'') || + (v[0] == '"' && v[len(v)-1] == '"') { + v = v[1 : len(v)-1] + } } meta.StreamTitle = v break @@ -96,6 +109,29 @@ func parseICYMetadata(block []byte) icyMetadata { return meta } +// splitICYFields splits an ICY metadata string on semicolons that appear +// outside of single-quoted value strings. Semicolons inside quotes (e.g. +// StreamTitle='Artist - Song; Live';) are preserved as part of the value. +func splitICYFields(s string) []string { + var fields []string + inQuote := false + start := 0 + for i := 0; i < len(s); i++ { + c := s[i] + if c == '\'' { + inQuote = !inQuote + } + if c == ';' && !inQuote { + fields = append(fields, s[start:i]) + start = i + 1 + } + } + if start < len(s) { + fields = append(fields, s[start:]) + } + return fields +} + func parseICYMetaInt(raw string) (int, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/ingest/adapters/stdinpcm/source.go b/internal/ingest/adapters/stdinpcm/source.go index 5785928..104b66b 100644 --- a/internal/ingest/adapters/stdinpcm/source.go +++ b/internal/ingest/adapters/stdinpcm/source.go @@ -119,6 +119,7 @@ func (s *Source) Stats() ingest.SourceStats { func (s *Source) readLoop(ctx context.Context) { defer s.wg.Done() + defer close(s.errs) defer close(s.chunks) frameBytes := s.channels * 2