Skip to content
Snippets Groups Projects
Commit d031eaf1 authored by ale's avatar ale
Browse files

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.
parent be398b6f
No related branches found
No related tags found
1 merge request!1v2.0
...@@ -2,12 +2,13 @@ package icecast ...@@ -2,12 +2,13 @@ package icecast
import ( import (
"context" "context"
"encoding/xml" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"strconv" "net/url"
"time" "time"
"git.autistici.org/ale/autoradio" "git.autistici.org/ale/autoradio"
...@@ -15,40 +16,62 @@ import ( ...@@ -15,40 +16,62 @@ import (
) )
var ( var (
icecastStatusPage = "/status-autoradio.xsl" icecastStatusPage = "/status-json.xsl"
icecastStatusUpdateInterval = 2 * time.Second icecastStatusUpdateInterval = 2 * time.Second
) )
// Icecast returns empty fields in our status handler, which we'll // TODO: deserialize properly the time format used by Icecast.
// 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...
type icecastMountStatus struct { type icecastMountStatus struct {
Name string `xml:"name,attr"` Artist string `json:"artist"`
Listeners string `xml:"listeners"` BitRate int32 `json:"audio_bitrate"`
BitRate string `xml:"bitrate"` Channels int32 `json:"audio_channels"`
Quality string `xml:"quality"` AudioInfo string `json:"audio_info"`
VideoQuality string `xml:"video-quality"` SampleRate int32 `json:"audio_samplerate"`
FrameSize string `xml:"frame-size"` Genre string `json:"genre"`
FrameRate string `xml:"frame-rate"` 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 { // Icecast status-json.xsl returns different structures if there is a
XMLName xml.Name `xml:"status"` // single source or more than one.
Mounts []icecastMountStatus `xml:"mount"` type icecastStatusManySources struct {
Icestats struct {
Source []icecastMountStatus `json:"source"`
} `json:"icestats"`
} }
func parseIcecastStatus(r io.Reader) (*icecastStatus, error) { type icecastStatusSingleSource struct {
var doc icecastStatus Icestats struct {
if err := xml.NewDecoder(r).Decode(&doc); err != nil { 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 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) req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d%s", port, icecastStatusPage), nil)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -64,37 +87,30 @@ func fetchIcecastStatus(ctx context.Context, port int) (*icecastStatus, error) { ...@@ -64,37 +87,30 @@ func fetchIcecastStatus(ctx context.Context, port int) (*icecastStatus, error) {
return parseIcecastStatus(resp.Body) return parseIcecastStatus(resp.Body)
} }
func convertIcecastStatus(status *icecastStatus) []*pb.IcecastMount { func convertIcecastStatus(status []icecastMountStatus) []*pb.IcecastMount {
out := make([]*pb.IcecastMount, 0, len(status.Mounts)) out := make([]*pb.IcecastMount, 0, len(status))
for _, m := range status.Mounts { for _, m := range status {
listenURL, err := url.Parse(m.ListenURL)
if err != nil {
continue
}
outm := pb.IcecastMount{ outm := pb.IcecastMount{
Path: autoradio.IcecastPathToMountPath(m.Name), Path: autoradio.IcecastPathToMountPath(listenURL.Path),
Listeners: toi(m.Listeners), Listeners: m.Listeners,
BitRate: toi(m.BitRate), BitRate: m.BitRate,
Quality: tof(m.Quality), SampleRate: m.SampleRate,
VideoQuality: tof(m.VideoQuality), Quality: m.Quality,
FrameSize: m.FrameSize, Channels: m.Channels,
FrameRate: tof(m.FrameRate), Name: m.Name,
Description: m.Description,
Title: m.Title,
Artist: m.Artist,
} }
out = append(out, &outm) out = append(out, &outm)
} }
return out 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) { func (c *Controller) statusUpdater(ctx context.Context) {
tick := time.NewTicker(icecastStatusUpdateInterval) tick := time.NewTicker(icecastStatusUpdateInterval)
defer tick.Stop() defer tick.Stop()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment