aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2024-09-02 09:33:04 -0400
committerXe Iaso <me@xeiaso.net>2024-09-02 09:33:04 -0400
commit4628a6e4ba4920925bef6b5dbb6dfd15c7b08a73 (patch)
tree34aefa02776bec947c3cb6f38b7c1fd69f7a9d2f /internal
parenta2cf1050866bc902ad014ab21767531ff64a337a (diff)
downloadx-4628a6e4ba4920925bef6b5dbb6dfd15c7b08a73.tar.xz
x-4628a6e4ba4920925bef6b5dbb6dfd15c7b08a73.zip
import cmd/aerial
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'internal')
-rw-r--r--internal/pvfm/bot/bot.go181
-rw-r--r--internal/pvfm/bot/doc.go10
-rw-r--r--internal/pvfm/bot/help.go38
-rw-r--r--internal/pvfm/commands/source/source.go8
-rw-r--r--internal/pvfm/fname.go28
-rw-r--r--internal/pvfm/info.go105
-rw-r--r--internal/pvfm/pvl/pvl.go77
-rw-r--r--internal/pvfm/recording/doc.go4
-rw-r--r--internal/pvfm/recording/recording.go120
-rw-r--r--internal/pvfm/recording/recording_demo.go59
-rw-r--r--internal/pvfm/schedule/schedule.go102
-rw-r--r--internal/pvfm/station/station.go96
12 files changed, 828 insertions, 0 deletions
diff --git a/internal/pvfm/bot/bot.go b/internal/pvfm/bot/bot.go
new file mode 100644
index 0000000..5ed093d
--- /dev/null
+++ b/internal/pvfm/bot/bot.go
@@ -0,0 +1,181 @@
+package bot
+
+import (
+ "errors"
+ "log"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/time/rate"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+var (
+ ErrRateLimitExceeded = errors.New("bot: per-command rate limit exceeded")
+)
+
+type command struct {
+ aliases []string
+ verb string
+ helptext string
+}
+
+func (c *command) Verb() string {
+ return c.verb
+}
+
+func (c *command) Helptext() string {
+ return c.helptext
+}
+
+// Handler is the type that bot command functions need to implement. Errors
+// should be returned.
+type Handler func(*discordgo.Session, *discordgo.Message, []string) error
+
+// CommandHandler is a generic interface for types that implement a bot
+// command. It is akin to http.Handler, but more comprehensive.
+type CommandHandler interface {
+ Verb() string
+ Helptext() string
+
+ Handler(*discordgo.Session, *discordgo.Message, []string) error
+ Permissions(*discordgo.Session, *discordgo.Message, []string) error
+}
+
+type basicCommand struct {
+ *command
+ handler Handler
+ permissions Handler
+ limiter *rate.Limiter
+}
+
+func (bc *basicCommand) Handler(s *discordgo.Session, m *discordgo.Message, parv []string) error {
+ return bc.handler(s, m, parv)
+}
+
+func (bc *basicCommand) Permissions(s *discordgo.Session, m *discordgo.Message, parv []string) error {
+ if !bc.limiter.Allow() {
+ return ErrRateLimitExceeded
+ }
+
+ return bc.permissions(s, m, parv)
+}
+
+// The "default" command set, useful for simple bot projects.
+var (
+ DefaultCommandSet = NewCommandSet()
+)
+
+// Command handling errors.
+var (
+ ErrAlreadyExists = errors.New("bot: command already exists")
+ ErrNoSuchCommand = errors.New("bot: no such command exists")
+ ErrNoPermissions = errors.New("bot: you do not have permissions for this command")
+ ErrParvCountMismatch = errors.New("bot: parameter count mismatch")
+)
+
+// The default command prefix. Command `foo` becomes `.foo` in chat, etc.
+const (
+ DefaultPrefix = "."
+)
+
+// NewCommand creates an anonymous command and adds it to the default CommandSet.
+func NewCommand(verb, helptext string, handler, permissions Handler) error {
+ return DefaultCommandSet.Add(NewBasicCommand(verb, helptext, handler, permissions))
+}
+
+// NewBasicCommand creates a CommandHandler instance using the implementation
+// functions supplied as arguments.
+func NewBasicCommand(verb, helptext string, permissions, handler Handler) CommandHandler {
+ return &basicCommand{
+ command: &command{
+ verb: verb,
+ helptext: helptext,
+ },
+ handler: handler,
+ permissions: permissions,
+ limiter: rate.NewLimiter(rate.Every(5*time.Second), 1),
+ }
+}
+
+// CommandSet is a group of bot commands similar to an http.ServeMux.
+type CommandSet struct {
+ sync.Mutex
+ cmds map[string]CommandHandler
+
+ Prefix string
+}
+
+// NewCommandSet creates a new command set with the `help` command pre-loaded.
+func NewCommandSet() *CommandSet {
+ cs := &CommandSet{
+ cmds: map[string]CommandHandler{},
+ Prefix: DefaultPrefix,
+ }
+
+ cs.AddCmd("help", "Shows help for the bot", NoPermissions, cs.help)
+
+ return cs
+}
+
+// NoPermissions is a simple middelware function that allows all command invocations
+// to pass the permissions check.
+func NoPermissions(s *discordgo.Session, m *discordgo.Message, parv []string) error {
+ return nil
+}
+
+// AddCmd is syntactic sugar for cs.Add(NewBasicCommand(args...))
+func (cs *CommandSet) AddCmd(verb, helptext string, permissions, handler Handler) error {
+ return cs.Add(NewBasicCommand(verb, helptext, permissions, handler))
+}
+
+// Add adds a single command handler to the CommandSet. This can be done at runtime
+// but it is suggested to only add commands on application boot.
+func (cs *CommandSet) Add(h CommandHandler) error {
+ cs.Lock()
+ defer cs.Unlock()
+
+ v := strings.ToLower(h.Verb())
+
+ if _, ok := cs.cmds[v]; ok {
+ return ErrAlreadyExists
+ }
+
+ cs.cmds[v] = h
+
+ return nil
+}
+
+// Run makes a CommandSet compatible with discordgo event dispatching.
+func (cs *CommandSet) Run(s *discordgo.Session, msg *discordgo.Message) error {
+ cs.Lock()
+ defer cs.Unlock()
+
+ if strings.HasPrefix(msg.Content, cs.Prefix) {
+ params := strings.Fields(msg.Content)
+ verb := strings.ToLower(params[0][1:])
+
+ cmd, ok := cs.cmds[verb]
+ if !ok {
+ return ErrNoSuchCommand
+ }
+
+ err := cmd.Permissions(s, msg, params)
+ if err != nil {
+ log.Printf("Permissions error: %s: %v", msg.Author.Username, err)
+ s.ChannelMessageSend(msg.ChannelID, "You don't have permissions for that, sorry.")
+ return ErrNoPermissions
+ }
+
+ err = cmd.Handler(s, msg, params)
+ if err != nil {
+ log.Printf("command handler error: %v", err)
+ s.ChannelMessageSend(msg.ChannelID, "error when running that command: "+err.Error())
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/internal/pvfm/bot/doc.go b/internal/pvfm/bot/doc.go
new file mode 100644
index 0000000..d5c5af5
--- /dev/null
+++ b/internal/pvfm/bot/doc.go
@@ -0,0 +1,10 @@
+/*
+Package bot contains some generically useful bot rigging for Discord chatbots.
+
+This package works by defining command handlers in a CommandSet, and then dispatching
+based on message contents. If the bot's command prefix is `;`, then `;foo` activates
+the command handler for command `foo`.
+
+A CommandSet has a mutex baked into it for convenience of command implementation.
+*/
+package bot
diff --git a/internal/pvfm/bot/help.go b/internal/pvfm/bot/help.go
new file mode 100644
index 0000000..7e62ff2
--- /dev/null
+++ b/internal/pvfm/bot/help.go
@@ -0,0 +1,38 @@
+package bot
+
+import (
+ "fmt"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+func (cs *CommandSet) help(s *discordgo.Session, m *discordgo.Message, parv []string) error {
+ switch len(parv) {
+ case 1:
+ result := cs.formHelp()
+
+ authorChannel, err := s.UserChannelCreate(m.Author.ID)
+ if err != nil {
+ return err
+ }
+
+ s.ChannelMessageSend(authorChannel.ID, result)
+
+ s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("<@%s> check direct messages, help is there!", m.Author.ID))
+
+ default:
+ return ErrParvCountMismatch
+ }
+
+ return nil
+}
+
+func (cs *CommandSet) formHelp() string {
+ result := "Bot commands: \n"
+
+ for verb, cmd := range cs.cmds {
+ result += fmt.Sprintf("%s%s: %s\n", cs.Prefix, verb, cmd.Helptext())
+ }
+
+ return (result + "If there's any problems please don't hesitate to ask a server admin for help.")
+}
diff --git a/internal/pvfm/commands/source/source.go b/internal/pvfm/commands/source/source.go
new file mode 100644
index 0000000..d72f70c
--- /dev/null
+++ b/internal/pvfm/commands/source/source.go
@@ -0,0 +1,8 @@
+package source
+
+import "github.com/bwmarrin/discordgo"
+
+func Source(s *discordgo.Session, m *discordgo.Message, parv []string) error {
+ s.ChannelMessageSend(m.ChannelID, "Source code: https://github.com/PonyvilleFM/aura")
+ return nil
+}
diff --git a/internal/pvfm/fname.go b/internal/pvfm/fname.go
new file mode 100644
index 0000000..fa84695
--- /dev/null
+++ b/internal/pvfm/fname.go
@@ -0,0 +1,28 @@
+package pvfm
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "within.website/x/internal/pvfm/pvl"
+)
+
+func GenFilename() (string, error) {
+ cal, err := pvl.Get()
+ if err != nil {
+ return "", nil
+ }
+
+ now := cal.Result[0]
+
+ localTime := time.Now()
+ thentime := time.Unix(now.StartTime, 0)
+ if thentime.Unix() < localTime.Unix() {
+ // return fmt.Sprintf("%s - %s.mp3", now.Title, localTime.Format(time.RFC822)), nil
+ }
+
+ now.Title = strings.Replace(now.Title, "/", "-slash-", 0)
+
+ return fmt.Sprintf("%s - %s.mp3", now.Title, localTime.Format(time.RFC822)), nil
+}
diff --git a/internal/pvfm/info.go b/internal/pvfm/info.go
new file mode 100644
index 0000000..4c3456e
--- /dev/null
+++ b/internal/pvfm/info.go
@@ -0,0 +1,105 @@
+/*
+Package pvfm grabs information about PonyvilleFM from the station servers.
+*/
+package pvfm
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+ "time"
+)
+
+var (
+ latestInfo Wrapper
+
+ bugTime = flag.Int("pvfm-poke-delay", 15, "how stale the info can get")
+)
+
+// The regex to check for Aerial's name.
+var (
+ AerialRegex = regexp.MustCompile(`Aerial`)
+)
+
+// Wrapper is a time, info pair. This is used to invalidate the cache of
+// data from ponyvillefm.com.
+type Wrapper struct {
+ Age time.Time
+ Info Info
+}
+
+// Info is the actual information we care about. It contains information about the
+// available streams.
+type Info struct {
+ Listeners Listeners `json:"all"`
+ Main RadioStream `json:"one"`
+ Secondary RadioStream `json:"two"`
+ MusicOnly RadioStream `json:"free"`
+}
+
+// RadioStream contains data about an individual stream.
+type RadioStream struct {
+ Listeners int `json:"listeners"`
+ Nowplaying string `json:"nowplaying"`
+ Artist string `json:"artist"`
+ Album string `json:"album"`
+ Title string `json:"title"`
+ Onair string `json:"onair"`
+ Artwork string `json:"artwork"`
+}
+
+// Listeners contains a single variable Listeners.
+type Listeners struct {
+ Listeners int `json:"listeners"`
+}
+
+// GetStats returns an Info, error pair representing the latest (or cached)
+// version of the statistics from the ponyvillefm servers. If there is an error
+// anywhere
+func GetStats() (Info, error) {
+ now := time.Now()
+
+ // If right now is before the age of the latestInfo plus the pacing time,
+ // return the latestInfo Info.
+ if now.Before(latestInfo.Age.Add(time.Second * time.Duration(*bugTime))) {
+ return latestInfo.Info, nil
+ }
+
+ i := Info{}
+
+ // Grab stuff from the internet
+ c := &http.Client{
+ Timeout: time.Second * 15,
+ }
+
+ resp, err := c.Get("http://ponyvillefm.com/data/nowplaying")
+ if err != nil {
+ return Info{}, fmt.Errorf("http fetch: %s %d: %v", resp.Status, resp.StatusCode, err)
+ }
+ defer resp.Body.Close()
+
+ content, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return Info{}, err
+ }
+
+ err = json.Unmarshal(content, &i)
+ if err != nil {
+ return Info{}, fmt.Errorf("json unmarshal: %v", err)
+ }
+
+ // Update the age/contents of the latestInfo
+ latestInfo.Info = i
+ latestInfo.Age = now
+
+ return latestInfo.Info, nil
+}
+
+// IsDJLive returns true if a human DJ is live or false if the auto DJ (and any
+// of its playlists) is playing music.
+func (i Info) IsDJLive() bool {
+ return !AerialRegex.Match([]byte(i.Main.Onair))
+}
diff --git a/internal/pvfm/pvl/pvl.go b/internal/pvfm/pvl/pvl.go
new file mode 100644
index 0000000..cb8e80b
--- /dev/null
+++ b/internal/pvfm/pvl/pvl.go
@@ -0,0 +1,77 @@
+/*
+Package pvl grabs Ponyville Live data.
+*/
+package pvl
+
+import (
+ "encoding/json"
+ "errors"
+ "flag"
+ "io/ioutil"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+var (
+ latestInfo Wrapper
+
+ bugTime = flag.Int64("pvl-poke-delay", 666, "how stale pvl info can get")
+)
+
+type Wrapper struct {
+ Age time.Time
+ Info Calendar
+}
+
+type Calendar struct {
+ Result []struct {
+ Body interface{} `json:"body"`
+ EndTime int64 `json:"end_time"`
+ Guid string `json:"guid"`
+ ID float64 `json:"id"`
+ ImageURL string `json:"image_url"`
+ IsAllDay bool `json:"is_all_day"`
+ IsPromoted bool `json:"is_promoted"`
+ Location interface{} `json:"location"`
+ Range string `json:"range"`
+ StartTime int64 `json:"start_time"`
+ StationID int64 `json:"station_id"`
+ Title string `json:"title"`
+ WebURL string `json:"web_url"`
+ } `json:"result"`
+ Status string `json:"status"`
+}
+
+// Get grabs the station schedule from Ponyville Live.
+func Get() (Calendar, error) {
+ now := time.Now()
+ if now.Before(latestInfo.Age.Add(time.Second * time.Duration(*bugTime))) {
+ return latestInfo.Info, nil
+ }
+
+ c := Calendar{}
+ resp, err := http.Get("http://ponyvillelive.com/api/schedule/index/station/ponyvillefm")
+ if err != nil {
+ return Calendar{}, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode/100 == 5 {
+ return Calendar{}, errors.New("pvl: API returned " + strconv.Itoa(resp.StatusCode) + " " + resp.Status)
+ }
+
+ content, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return Calendar{}, err
+ }
+
+ err = json.Unmarshal(content, &c)
+ if err != nil {
+ return Calendar{}, err
+ }
+
+ latestInfo.Info = c
+ latestInfo.Age = now
+ return latestInfo.Info, nil
+}
diff --git a/internal/pvfm/recording/doc.go b/internal/pvfm/recording/doc.go
new file mode 100644
index 0000000..95bd800
--- /dev/null
+++ b/internal/pvfm/recording/doc.go
@@ -0,0 +1,4 @@
+/*
+Package recording manages recording radio streams to files.
+*/
+package recording
diff --git a/internal/pvfm/recording/recording.go b/internal/pvfm/recording/recording.go
new file mode 100644
index 0000000..d3378ce
--- /dev/null
+++ b/internal/pvfm/recording/recording.go
@@ -0,0 +1,120 @@
+package recording
+
+import (
+ "context"
+ "errors"
+ "log"
+ "os"
+ "os/exec"
+ "time"
+)
+
+var (
+ ErrMismatchWrite = errors.New("recording: did not write the same number of bytes that were read")
+)
+
+// Recording ...
+type Recording struct {
+ ctx context.Context
+ url string
+ fname string
+ cancel context.CancelFunc
+ started time.Time
+ restarts int
+
+ Debug bool
+ Err error
+}
+
+// New creates a new Recording of the given URL to the given filename for output.
+func New(url, fname string) (*Recording, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 8*time.Hour)
+
+ r := &Recording{
+ ctx: ctx,
+ url: url,
+ fname: fname,
+ cancel: cancel,
+ started: time.Now(),
+ }
+
+ return r, nil
+}
+
+// Cancel stops the recording.
+func (r *Recording) Cancel() {
+ r.cancel()
+}
+
+// Done returns the done channel of the recording.
+func (r *Recording) Done() <-chan struct{} {
+ return r.ctx.Done()
+}
+
+// OutputFilename gets the output filename originally passed into New.
+func (r *Recording) OutputFilename() string {
+ return r.fname
+}
+
+// StartTime gets start time
+func (r *Recording) StartTime() time.Time {
+ return r.started
+}
+
+// Start blockingly starts the recording and returns the error if one is encountered while streaming.
+// This should be stopped in another goroutine.
+func (r *Recording) Start() error {
+ sr, err := exec.LookPath("streamripper")
+ if err != nil {
+ return err
+ }
+
+ cmd := exec.CommandContext(r.ctx, sr, r.url, "-A", "-a", r.fname)
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+
+ log.Printf("%s: %v", cmd.Path, cmd.Args)
+
+ err = cmd.Start()
+ if err != nil {
+ return err
+ }
+
+ // Automatically kill recordings after four hours
+ go func() {
+ t := time.NewTicker(4 * time.Hour)
+ defer t.Stop()
+
+ log.Println("got here")
+
+ for {
+ select {
+ case <-r.ctx.Done():
+ return
+ case <-t.C:
+ log.Printf("Automatically killing recording after 4 hours...")
+ r.Cancel()
+ }
+ }
+ }()
+
+ go func() {
+ defer r.Cancel()
+ err := cmd.Wait()
+ if err != nil {
+ log.Println(err)
+ }
+ }()
+
+ defer r.cancel()
+
+ for {
+ time.Sleep(250 * time.Millisecond)
+
+ select {
+ case <-r.ctx.Done():
+ return nil
+ default:
+ }
+ }
+}
diff --git a/internal/pvfm/recording/recording_demo.go b/internal/pvfm/recording/recording_demo.go
new file mode 100644
index 0000000..3264841
--- /dev/null
+++ b/internal/pvfm/recording/recording_demo.go
@@ -0,0 +1,59 @@
+// +build ignore
+
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+
+ "github.com/PonyvilleFM/aura/recording"
+)
+
+var (
+ url = flag.String("url", "", "url to record")
+ fname = flag.String("fname", "", "filename to record to")
+ debug = flag.Bool("debug", false, "debug mode")
+
+ askedToDie bool
+)
+
+func main() {
+ flag.Parse()
+
+ r, err := recording.New(*url, *fname)
+ if err != nil {
+ log.Printf("%s -> %s: %v", *url, *fname, err)
+ log.Fatal(err)
+ }
+
+ r.Debug = *debug
+
+ go func() {
+ log.Printf("Starting download of stream %s to %s", *url, *fname)
+ err := r.Start()
+ if err != nil {
+ log.Fatal(err)
+ }
+ }()
+
+ go func() {
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt)
+
+ for _ = range c {
+ if askedToDie {
+ os.Exit(2)
+ }
+
+ log.Println("Stopping recording... (^C again to kill now)")
+ r.Cancel()
+
+ askedToDie = true
+ }
+ }()
+
+ <-r.Done()
+ log.Printf("stream %s recorded to %s", *url, *fname)
+}
diff --git a/internal/pvfm/schedule/schedule.go b/internal/pvfm/schedule/schedule.go
new file mode 100644
index 0000000..982460c
--- /dev/null
+++ b/internal/pvfm/schedule/schedule.go
@@ -0,0 +1,102 @@
+/*
+Package schedule grabs DJ schedule data from Ponyville FM's servers.
+*/
+package schedule
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "time"
+)
+
+// ScheduleResult is a wrapper for a list of ScheduleEntry records.
+type ScheduleResult struct {
+ Result []ScheduleEntry `json:"result"`
+}
+
+// ScheduleEntry is an individual schedule datum.
+type ScheduleEntry struct {
+ StartTime string `json:"start_time"`
+ StartUnix int `json:"start_unix"`
+ Duration int `json:"duration"` // minutes
+ EndTime string `json:"end_time"`
+ EndUnix int `json:"end_unix"`
+ Name string `json:"name"`
+ Host string `json:"host"`
+ Description string `json:"description"`
+ Showcard string `json:"showcard"`
+ Background string `json:"background"`
+ Timezone string `json:"timezone"`
+ Status string `json:"status"`
+}
+
+func (s ScheduleEntry) String() string {
+ startTimeUnix := time.Unix(int64(s.StartUnix), 0)
+ nowWithoutNanoseconds := time.Unix(time.Now().Unix(), 0)
+ dur := startTimeUnix.Sub(nowWithoutNanoseconds)
+
+ if dur > 0 {
+ return fmt.Sprintf(
+ "In %s (%v %v): %s - %s",
+ dur, s.StartTime, s.Timezone, s.Host, s.Name,
+ )
+ } else {
+ return fmt.Sprintf(
+ "Now: %s - %s",
+ s.Host, s.Name,
+ )
+ }
+}
+
+var (
+ latestInfo Wrapper
+
+ bugTime = flag.Int("pvfm-schedule-poke-delay", 15, "how stale the info can get")
+)
+
+// Wrapper is a time, info pair. This is used to invalidate the cache of
+// data from ponyvillefm.com.
+type Wrapper struct {
+ Age time.Time
+ Info *ScheduleResult
+}
+
+// Get returns schedule entries, only fetching new data at most every n
+// seconds, where n is defined above.
+func Get() ([]ScheduleEntry, error) {
+ now := time.Now()
+
+ if now.Before(latestInfo.Age.Add(time.Second * time.Duration(*bugTime))) {
+ return latestInfo.Info.Result, nil
+ }
+
+ s := &ScheduleResult{}
+ c := http.Client{
+ Timeout: time.Duration(time.Second * 15),
+ }
+
+ resp, err := c.Get("http://ponyvillefm.com/data/schedule")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ content, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(content, s)
+ if err != nil {
+ return nil, err
+ }
+
+ // Update the age/contents of the latestInfo
+ latestInfo.Info = s
+ latestInfo.Age = now
+
+ return s.Result, nil
+}
diff --git a/internal/pvfm/station/station.go b/internal/pvfm/station/station.go
new file mode 100644
index 0000000..f6e9cd6
--- /dev/null
+++ b/internal/pvfm/station/station.go
@@ -0,0 +1,96 @@
+/*
+Package station grabs fallback data from the radio station.
+*/
+package station
+
+import (
+ "encoding/json"
+ "flag"
+ "io/ioutil"
+ "net/http"
+ "time"
+)
+
+var (
+ latestInfo Wrapper
+
+ bugTime = flag.Int("station-poke-delay", 15, "how stale the info can get")
+)
+
+type Wrapper struct {
+ Age time.Time
+ Info Info
+}
+
+type Info struct {
+ Icestats struct {
+ Admin string `json:"admin"`
+ BannedIPs int `json:"banned_IPs"`
+ Build string `json:"build"`
+ Host string `json:"host"`
+ Location string `json:"location"`
+ OutgoingKbitrate int `json:"outgoing_kbitrate"`
+ ServerID string `json:"server_id"`
+ ServerStart string `json:"server_start"`
+ StreamKbytesRead int `json:"stream_kbytes_read"`
+ StreamKbytesSent int `json:"stream_kbytes_sent"`
+ Source []struct {
+ Artist string `json:"artist"`
+ AudioBitrate int `json:"audio_bitrate,omitempty"`
+ AudioChannels int `json:"audio_channels,omitempty"`
+ AudioInfo string `json:"audio_info"`
+ AudioSamplerate int `json:"audio_samplerate,omitempty"`
+ Bitrate int `json:"bitrate"`
+ Connected int `json:"connected"`
+ Genre string `json:"genre"`
+ IceBitrate int `json:"ice-bitrate,omitempty"`
+ IncomingBitrate int `json:"incoming_bitrate"`
+ ListenerPeak int `json:"listener_peak"`
+ Listeners int `json:"listeners"`
+ Listenurl string `json:"listenurl"`
+ MetadataUpdated string `json:"metadata_updated"`
+ OutgoingKbitrate int `json:"outgoing_kbitrate"`
+ QueueSize int `json:"queue_size"`
+ ServerDescription string `json:"server_description"`
+ ServerName string `json:"server_name"`
+ ServerType string `json:"server_type"`
+ ServerURL string `json:"server_url"`
+ StreamStart string `json:"stream_start"`
+ Subtype string `json:"subtype,omitempty"`
+ Title string `json:"title"`
+ TotalMbytesSent int `json:"total_mbytes_sent"`
+ YpCurrentlyPlaying string `json:"yp_currently_playing"`
+ } `json:"source"`
+ } `json:"icestats"`
+}
+
+func GetStats() (Info, error) {
+ now := time.Now()
+ if now.Before(latestInfo.Age.Add(time.Second * time.Duration(*bugTime))) {
+ return latestInfo.Info, nil
+ }
+
+ i := Info{}
+
+ resp, err := http.Get("http://dj.bronyradio.com:8000/status-json.xsl")
+ if err != nil {
+ return Info{}, err
+ }
+
+ defer resp.Body.Close()
+
+ content, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return Info{}, err
+ }
+
+ err = json.Unmarshal(content, &i)
+ if err != nil {
+ return Info{}, err
+ }
+
+ latestInfo.Info = i
+ latestInfo.Age = now
+
+ return latestInfo.Info, nil
+}