|
- package icecast
-
- import (
- "bytes"
- "fmt"
- "io"
- "strconv"
- "strings"
- )
-
- type icyMetadata struct {
- StreamTitle string
- }
-
- type icyReader struct {
- r io.Reader
- metaInt int
- audioLeft int
- onMetadata func(icyMetadata)
- }
-
- func newICYReader(r io.Reader, metaInt int, onMetadata func(icyMetadata)) io.Reader {
- if r == nil || metaInt <= 0 {
- return r
- }
- return &icyReader{
- r: r,
- metaInt: metaInt,
- audioLeft: metaInt,
- onMetadata: onMetadata,
- }
- }
-
- func (r *icyReader) Read(p []byte) (int, error) {
- if len(p) == 0 {
- return 0, nil
- }
- for {
- if r.audioLeft == 0 {
- if err := r.readMetadataBlock(); err != nil {
- return 0, err
- }
- r.audioLeft = r.metaInt
- continue
- }
- want := len(p)
- if want > r.audioLeft {
- want = r.audioLeft
- }
- n, err := r.r.Read(p[:want])
- if n > 0 {
- r.audioLeft -= n
- return n, nil
- }
- if err != nil {
- return 0, err
- }
- }
- }
-
- func (r *icyReader) readMetadataBlock() error {
- var lenBuf [1]byte
- if _, err := io.ReadFull(r.r, lenBuf[:]); err != nil {
- return err
- }
- blockLen := int(lenBuf[0]) * 16
- if blockLen == 0 {
- return nil
- }
- block := make([]byte, blockLen)
- if _, err := io.ReadFull(r.r, block); err != nil {
- return err
- }
- if r.onMetadata != nil {
- r.onMetadata(parseICYMetadata(block))
- }
- 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{}
-
- 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)
- // 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
- }
- 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 == "" {
- return 0, nil
- }
- n, err := strconv.Atoi(raw)
- if err != nil || n < 0 {
- return 0, fmt.Errorf("invalid icy-metaint: %q", raw)
- }
- return n, nil
- }
|