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 } func parseICYMetadata(block []byte) icyMetadata { raw := strings.TrimRight(string(bytes.Trim(block, "\x00")), "\x00") meta := icyMetadata{} for _, field := range strings.Split(raw, ";") { 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] } meta.StreamTitle = v break } return meta } 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 }