diff --git a/api.go b/api.go index 9e56c8c4687c41a0c28c52488750899f129ed7af..3dd6f8a76a27fa9e387d38c3db8a18d4f2870033 100644 --- a/api.go +++ b/api.go @@ -64,6 +64,27 @@ func NewEncodingParams() *EncodingParams { } } +func (p *EncodingParams) String() string { + var out []string + out = append(out, p.Format) + if p.BitRate > 0 { + out = append(out, fmt.Sprintf("%dkBps", p.BitRate)) + } + if p.Quality > -1 { + out = append(out, fmt.Sprintf("q=%g", p.Quality)) + } + switch p.Channels { + case 1: + out = append(out, "mono") + case 2: + out = append(out, "stereo") + } + if p.SampleRate > 0 { + out = append(out, fmt.Sprintf("%gkHz", p.SampleRate/1000)) + } + return strings.Join(out, ", ") +} + func (p *EncodingParams) Valid() error { switch p.Format { case "mp3", "mp3.cbr", "mp3.abr", "vorbis.cbr", "vorbis.abr": diff --git a/fe/http.go b/fe/http.go index 89cd8fceaa58b77551dcc2acf13ecc3ad568770a..11cf3565c7ed3db2e710546cdfb05e9199b5f11a 100644 --- a/fe/http.go +++ b/fe/http.go @@ -12,6 +12,7 @@ import ( "net/url" "path" "path/filepath" + "sort" "strconv" "strings" "time" @@ -281,18 +282,81 @@ func (h *HttpRedirector) serveSource(mount *autoradio.Mount, w http.ResponseWrit proxy.ServeHTTP(w, r) } +type mountStatus struct { + Mount *autoradio.Mount + Listeners int + TransMounts []*mountStatus +} + +func newMountStatus(m *autoradio.Mount, nodes []*autoradio.NodeStatus) *mountStatus { + var listeners int + for _, n := range nodes { + for _, ims := range n.Mounts { + if ims.Name == m.Name { + listeners += ims.Listeners + break + } + } + } + return &mountStatus{ + Mount: m, + Listeners: listeners, + } +} + +type mountStatusList []*mountStatus + +func (l mountStatusList) Len() int { return len(l) } +func (l mountStatusList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l mountStatusList) Less(i, j int) bool { + return l[i].Mount.Name < l[j].Mount.Name +} + // Serve our cluster status page. func (h *HttpRedirector) serveStatusPage(w http.ResponseWriter, r *http.Request) { nodes, _ := h.client.GetNodes() mounts, _ := h.client.ListMounts() + + // Aggregate stats, and create a tree of transcoding mounts. + ms := make(map[string]*mountStatus) + for _, m := range mounts { + if m.HasTranscoder() { + continue + } + ms[m.Name] = newMountStatus(m, nodes) + } + for _, m := range mounts { + if !m.HasTranscoder() { + continue + } + src := ms[m.Transcoding.SourceName] + if src == nil { + continue + } + src.TransMounts = append(src.TransMounts, newMountStatus(m, nodes)) + } + msl := make([]*mountStatus, 0, len(ms)) + for _, m := range ms { + msl = append(msl, m) + } + + // Sort everything. + sort.Sort(mountStatusList(msl)) + for _, s := range msl { + if s.TransMounts != nil { + sort.Sort(mountStatusList(s.TransMounts)) + } + } + ctx := struct { Domain string Nodes []*autoradio.NodeStatus - Mounts []*autoradio.Mount - }{h.domain, nodes, mounts} + Mounts []*mountStatus + }{h.domain, nodes, msl} var buf bytes.Buffer if err := h.template.ExecuteTemplate(&buf, "index.html", ctx); err != nil { + log.Printf("error rendering template: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/fe/http_test.go b/fe/http_test.go index d944540d8c10699404b9dce018963662340df09d..28d6b7379c0305d271606816e599ec2202900617 100644 --- a/fe/http_test.go +++ b/fe/http_test.go @@ -40,6 +40,10 @@ func createTestHttpRedirector(t *testing.T) *HttpRedirector { etcd.Set(autoradio.MountPrefix+"test.ogg", `{"Name": "/test.ogg", "Username": "source1", "Password": "foo"}`, 86400) + etcd.Set(autoradio.MountPrefix+"test.mp3", + `{"Name": "/test.mp3", "Username": "source2", "Password": "foo", + "Transcoding": {"SourceName": "/test.ogg", "Format": "mp3", "BitRate": 32}}`, + 86400) etcd.Set(autoradio.MasterElectionPath, `{"Name": "node1", "IP": ["127.0.0.1"]}`, 86400) @@ -88,8 +92,17 @@ func TestHttpRedirector_StatusPage(t *testing.T) { // Retrieve the status page. data := doHttpRequest(t, "GET", srv.URL, 200) - if !strings.Contains(data, "<div class=\"container\">") { - t.Errorf("Bad response:\n%s", data) + + // Search for some expected content. + needles := []string{ + "<div class=\"container\">", + "/test.ogg", + "/test.mp3", + } + for _, s := range needles { + if !strings.Contains(data, s) { + t.Errorf("Bad response ('%s' not found):\n%s", s, data) + } } } diff --git a/fe/static/style.css b/fe/static/style.css index 681b3c5257bfeb271a2808425190813a66bbf3d0..3626800e3aafe97440506a71011fd462c9117706 100644 --- a/fe/static/style.css +++ b/fe/static/style.css @@ -77,7 +77,3 @@ body { z-index: -100; opacity: 0.3; } - -.error { - color: red -} \ No newline at end of file diff --git a/fe/templates/index.html b/fe/templates/index.html index 6bc3fab4ba67c8ee05edda592fa81a26ce5c5eab..71ae9b2e9af2aaa1a74c4a1baaa973350a7ab22c 100644 --- a/fe/templates/index.html +++ b/fe/templates/index.html @@ -3,7 +3,7 @@ <head> <title>stream.{{.Domain}}</title> <link rel="stylesheet" - href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> + href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/style.css"> <link rel="shortcut icon" href="/static/radio52.png"> </head> @@ -21,22 +21,45 @@ <div class="row mainitems"> <div class="col-lg-6"> <h4>Streams</h4> - {{$domain := .Domain}} - {{range .Mounts}} - <p> - <a href="http://stream.{{$domain}}{{.Name}}">{{.Name}}</a> - <a href="http://stream.{{$domain}}{{.Name}}.m3u">(m3u)</a> - </p> - {{end}} + <ul> + {{$domain := .Domain}} + {{range .Mounts}} + <li> + <a href="http://stream.{{$domain}}{{.Mount.Name}}" + {{if .Mount.RelayUrl}} + data-toggle="tooltip" data-delay="300" title="relay of {{.Mount.RelayUrl}}" + {{end}} + >{{.Mount.Name}}</a> + <a href="http://stream.{{$domain}}{{.Mount.Name}}.m3u">(m3u)</a> + <span class="badge">{{.Listeners}}</span> + {{if .TransMounts}} + <ul> + {{range .TransMounts}} + <li> + <a href="http://stream.{{$domain}}{{.Mount.Name}}" + data-toggle="tooltip" data-delay="300" title="{{.Mount.Transcoding.String}}" + >{{.Mount.Name}}</a> + <a href="http://stream.{{$domain}}{{.Mount.Name}}.m3u">(m3u)</a> + <span class="badge">{{.Listeners}}</span> + </li> + {{end}} + </ul> + {{end}} + </li> + {{end}} + </ul> </div> <div class="col-lg-6"> <h4>Nodes</h4> - {{range .Nodes}} - <p>{{.IP}} ({{.NumListeners}}) - {{if not .IcecastUp}}<span class="error">(IC_DOWN)</span>{{end}} - </p> - {{end}} + <ul> + {{range .Nodes}} + <li> + {{.Name}} <span class="badge">{{.NumListeners}}</span> + {{if not .IcecastUp}}<span class="label label-danger">IC_DOWN</span>{{end}} + </li> + {{end}} + </ul> </div> </div> @@ -48,5 +71,12 @@ </div> </div> + <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> + <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> + <script type="text/javascript"> + $(function() { + $('[data-toggle="tooltip"]').tooltip(); + }); + </script> </body> </html>