diff --git a/node/icecast/status.go b/node/icecast/status.go index a58e3c2d9fa614f7acada4c4363acb547bde0394..0749340948e6aa22aedce3731f0cdf11086f7e3e 100644 --- a/node/icecast/status.go +++ b/node/icecast/status.go @@ -2,12 +2,13 @@ package icecast import ( "context" - "encoding/xml" + "encoding/json" "fmt" "io" + "io/ioutil" "log" "net/http" - "strconv" + "net/url" "time" "git.autistici.org/ale/autoradio" @@ -15,40 +16,62 @@ import ( ) var ( - icecastStatusPage = "/status-autoradio.xsl" + icecastStatusPage = "/status-json.xsl" icecastStatusUpdateInterval = 2 * time.Second ) -// Icecast returns empty fields in our status handler, which we'll -// need to turn into integers (the xml unmarshaler will return an -// error in this specific case), so we use a separate type for -// decoding the status page output. This would be much simpler if I -// knew how to get the XSLT to put a default value in the output -// instead of an empty field... +// TODO: deserialize properly the time format used by Icecast. type icecastMountStatus struct { - Name string `xml:"name,attr"` - Listeners string `xml:"listeners"` - BitRate string `xml:"bitrate"` - Quality string `xml:"quality"` - VideoQuality string `xml:"video-quality"` - FrameSize string `xml:"frame-size"` - FrameRate string `xml:"frame-rate"` + Artist string `json:"artist"` + BitRate int32 `json:"audio_bitrate"` + Channels int32 `json:"audio_channels"` + AudioInfo string `json:"audio_info"` + SampleRate int32 `json:"audio_samplerate"` + Genre string `json:"genre"` + Listeners int32 `json:"listeners"` + ListenURL string `json:"listenurl"` + Quality float32 `json:"quality"` + Description string `json:"server_description"` + Name string `json:"server_name"` + Type string `json:"server_type"` + Subtype string `json:"subtype"` + Title string `json:"title"` + //StreamStart time.Time `json:"stream_start_iso8601"` } -type icecastStatus struct { - XMLName xml.Name `xml:"status"` - Mounts []icecastMountStatus `xml:"mount"` +// Icecast status-json.xsl returns different structures if there is a +// single source or more than one. +type icecastStatusManySources struct { + Icestats struct { + Source []icecastMountStatus `json:"source"` + } `json:"icestats"` } -func parseIcecastStatus(r io.Reader) (*icecastStatus, error) { - var doc icecastStatus - if err := xml.NewDecoder(r).Decode(&doc); err != nil { +type icecastStatusSingleSource struct { + Icestats struct { + Source icecastMountStatus `json:"source"` + } `json:"icestats"` +} + +func parseIcecastStatus(r io.Reader) ([]icecastMountStatus, error) { + data, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + // Try the single source document schema first. + var single icecastStatusSingleSource + if err := json.Unmarshal(data, &single); err == nil { + return []icecastMountStatus{single.Icestats.Source}, nil + } + var many icecastStatusManySources + if err := json.Unmarshal(data, &many); err != nil { return nil, err } - return &doc, nil + return many.Icestats.Source, nil } -func fetchIcecastStatus(ctx context.Context, port int) (*icecastStatus, error) { +func fetchIcecastStatus(ctx context.Context, port int) ([]icecastMountStatus, error) { req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d%s", port, icecastStatusPage), nil) if err != nil { return nil, err @@ -64,37 +87,30 @@ func fetchIcecastStatus(ctx context.Context, port int) (*icecastStatus, error) { return parseIcecastStatus(resp.Body) } -func convertIcecastStatus(status *icecastStatus) []*pb.IcecastMount { - out := make([]*pb.IcecastMount, 0, len(status.Mounts)) - for _, m := range status.Mounts { +func convertIcecastStatus(status []icecastMountStatus) []*pb.IcecastMount { + out := make([]*pb.IcecastMount, 0, len(status)) + for _, m := range status { + listenURL, err := url.Parse(m.ListenURL) + if err != nil { + continue + } outm := pb.IcecastMount{ - Path: autoradio.IcecastPathToMountPath(m.Name), - Listeners: toi(m.Listeners), - BitRate: toi(m.BitRate), - Quality: tof(m.Quality), - VideoQuality: tof(m.VideoQuality), - FrameSize: m.FrameSize, - FrameRate: tof(m.FrameRate), + Path: autoradio.IcecastPathToMountPath(listenURL.Path), + Listeners: m.Listeners, + BitRate: m.BitRate, + SampleRate: m.SampleRate, + Quality: m.Quality, + Channels: m.Channels, + Name: m.Name, + Description: m.Description, + Title: m.Title, + Artist: m.Artist, } out = append(out, &outm) } return out } -func toi(s string) int32 { - if i, err := strconv.Atoi(s); err == nil { - return int32(i) - } - return 0 -} - -func tof(s string) float32 { - if f, err := strconv.ParseFloat(s, 32); err == nil { - return float32(f) - } - return 0 -} - func (c *Controller) statusUpdater(ctx context.Context) { tick := time.NewTicker(icecastStatusUpdateInterval) defer tick.Stop()