fix(radio): address ICY parser review feedback

Signed-off-by: hiyukoim <11803292+hiyukoim@users.noreply.github.com>
This commit is contained in:
hiyukoim 2026-07-07 19:25:18 +02:00
parent fd3c3b2be2
commit e25071f87e
4 changed files with 52 additions and 8 deletions

View File

@ -17,17 +17,22 @@ var (
errEndOfStream = errors.New("icy stream ended")
)
const (
maxMetaInt = 1024 * 1024
maxMetadataLen = 255 * 16
)
// ReadStreamTitles reads ICY metadata blocks from r and emits changed StreamTitle values.
func ReadStreamTitles(ctx context.Context, r io.Reader, metaInt int, handleTitle func(string)) error {
if metaInt <= 0 {
if metaInt <= 0 || metaInt > maxMetaInt {
return ErrInvalidMetaInt
}
if handleTitle == nil {
return ErrMissingTitleHandler
}
audio := make([]byte, metaInt)
length := make([]byte, 1)
metadataBuf := make([]byte, maxMetadataLen)
lastTitle := ""
for {
@ -35,8 +40,8 @@ func ReadStreamTitles(ctx context.Context, r io.Reader, metaInt int, handleTitle
return err
}
if err := readFullOrEnd(r, audio); err != nil {
if errors.Is(err, errEndOfStream) {
if _, err := io.CopyN(io.Discard, r, int64(metaInt)); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return nil
}
return err
@ -58,7 +63,7 @@ func ReadStreamTitles(ctx context.Context, r io.Reader, metaInt int, handleTitle
continue
}
metadata := make([]byte, metadataLen)
metadata := metadataBuf[:metadataLen]
if err := readFullOrEnd(r, metadata); err != nil {
if errors.Is(err, errEndOfStream) {
return nil
@ -96,9 +101,12 @@ func streamTitle(metadata []byte) string {
}
start += len(prefix)
end := strings.IndexByte(text[start:], '\'')
end := strings.Index(text[start:], "';")
if end < 0 {
return ""
end = strings.IndexByte(text[start:], '\'')
if end < 0 {
return ""
}
}
return strings.TrimSpace(text[start : start+end])

View File

@ -73,6 +73,22 @@ var _ = Describe("ReadStreamTitles", func() {
Expect(titles).To(BeEmpty())
})
It("returns an error for huge metadata intervals", func() {
titles, err := readTitles(context.Background(), nil, maxMetaInt+1)
Expect(err).To(MatchError(ErrInvalidMetaInt))
Expect(titles).To(BeEmpty())
})
It("keeps single quotes inside StreamTitle values", func() {
stream := icyStream(metaInt, "StreamTitle='L'Oiseau - Track';")
titles, err := readTitles(context.Background(), stream, metaInt)
Expect(err).ToNot(HaveOccurred())
Expect(titles).To(Equal([]string{"L'Oiseau - Track"}))
})
It("stops when context is cancelled", func() {
ctx, cancel := context.WithCancel(context.Background())
cancel()

View File

@ -35,7 +35,7 @@ func NewMetadataManagerService(publisher TitlePublisher) *MetadataManager {
// ICY metadata or a client-error HTTP status — so the reader backs off for
// much longer instead of hammering the station.
func classifyICYError(err error) error {
if errors.Is(err, icy.ErrMissingMetaInt) {
if errors.Is(err, icy.ErrMissingMetaInt) || errors.Is(err, icy.ErrInvalidMetaInt) {
return MarkPermanent(err)
}
var statusErr *icy.ResponseStatusError

View File

@ -0,0 +1,20 @@
package radio
import (
"fmt"
"github.com/navidrome/navidrome/core/radio/icy"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Metadata provider", func() {
Describe("classifyICYError", func() {
It("marks invalid metadata intervals as permanent", func() {
err := classifyICYError(fmt.Errorf("%w: %q", icy.ErrInvalidMetaInt, "999999999"))
Expect(isPermanent(err)).To(BeTrue())
Expect(err).To(MatchError(ContainSubstring(icy.ErrInvalidMetaInt.Error())))
})
})
})