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
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()
......
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