From d031eaf106419d287eae2366b83a0411658e2498 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 13 Apr 2019 12:03:41 +0100
Subject: [PATCH] Use the JSON Icecast2 built-in status API

Drop our own XSLT template (yay), we can get the same information
using the built-in /status-json.xsl page with Icecast >= 2.4.
---
 node/icecast/status.go | 112 +++++++++++++++++++++++------------------
 1 file changed, 64 insertions(+), 48 deletions(-)

diff --git a/node/icecast/status.go b/node/icecast/status.go
index a58e3c2d..07493409 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()
-- 
GitLab