Skip to content

Commit 489e960

Browse files
authored
HTTP Server: Statuses can be collected in the background (#203)
* Fixed collection of statuses. * AutoConnect also supports periodic update. * Refresh. * Convurrent. * More logging.
1 parent 2ac6d1b commit 489e960

File tree

3 files changed

+119
-25
lines changed

3 files changed

+119
-25
lines changed

application/application.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ func (a *Application) Update() error {
393393
if err == nil {
394394
break
395395
}
396-
a.log("error getting receiever status: %v", err)
396+
a.log("error getting receiver status: %v", err)
397397
a.log("unable to get status from device; attempt %d/5, retrying...", i+1)
398398
time.Sleep(time.Second * 2)
399399
}

cast/payload.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,5 @@ type DeviceInfo struct {
165165
Ssid string `json:"ssid"`
166166
Timezone string `json:"timezone"`
167167
UptimeSec float64 `json:"uptime"`
168+
SsdpUdn string `json:"ssdp_udn"`
168169
}

http/handlers.go

Lines changed: 117 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"golang.org/x/sync/errgroup"
78
"net"
89
"net/http"
910
"strconv"
11+
"strings"
1012
"sync"
1113
"time"
1214

13-
"golang.org/x/sync/errgroup"
14-
1515
log "github.com/sirupsen/logrus"
1616
"github.com/vishen/go-chromecast/application"
1717
"github.com/vishen/go-chromecast/dns"
@@ -22,28 +22,73 @@ type Handler struct {
2222
apps map[string]application.App
2323
mux *http.ServeMux
2424

25-
verbose bool
26-
autoconnect bool
25+
verbose bool
26+
27+
autoconnectPeriod time.Duration
28+
autoconnectTicker *time.Ticker
29+
30+
// autoupdatePeriodSec defines how frequently app.Update method is called in the background.
31+
autoupdatePeriod time.Duration
32+
autoupdateTicker *time.Ticker
2733
}
2834

2935
func NewHandler(verbose bool) *Handler {
3036
handler := &Handler{
31-
verbose: verbose,
32-
apps: map[string]application.App{},
33-
mux: http.NewServeMux(),
34-
mu: sync.Mutex{},
35-
autoconnect: false,
37+
verbose: verbose,
38+
apps: map[string]application.App{},
39+
mux: http.NewServeMux(),
40+
mu: sync.Mutex{},
41+
42+
autoconnectPeriod: time.Duration(-1),
43+
autoconnectTicker: nil,
44+
45+
autoupdatePeriod: time.Duration(-1),
46+
autoupdateTicker: nil,
3647
}
3748
handler.registerHandlers()
3849
return handler
3950
}
4051

41-
// Autoconnect configures the handler to perform auto-discovery of all the cast devices & groups.
52+
// AutoConnect configures the handler to perform periodic auto-discovery of all the cast devices & groups.
53+
// It's intended to be called just after `NewHandler()`, before the handler is registered in the server.
54+
func (h *Handler) AutoConnect(period time.Duration) error {
55+
// Setting the autoconnect property - to allow (in future) periodic refresh of the connections.
56+
h.autoconnectPeriod = period
57+
if err := h.connectAllInternal("", "3"); err != nil {
58+
return err
59+
}
60+
if h.autoconnectPeriod > 0 {
61+
h.autoconnectTicker = time.NewTicker(period)
62+
go func() {
63+
for {
64+
<-h.autoconnectTicker.C
65+
if err := h.connectAllInternal("", "3"); err != nil {
66+
log.Printf("AutoConnect issued connectAllInternal failed: %v", err)
67+
}
68+
}
69+
}()
70+
}
71+
return nil
72+
}
73+
74+
// AutoUpdate configures the handler to perform auto-update of all the cast devices & groups.
4275
// It's intended to be called just after `NewHandler()`, before the handler is registered in the server.
43-
func (h *Handler) Autoconnect() error {
76+
// Thanks to AutoUpdate, /status and /statuses returns relatively recent status 'instantly'.
77+
func (h *Handler) AutoUpdate(period time.Duration) error {
4478
// Setting the autoconnect property - to allow (in future) periodic refresh of the connections.
45-
h.autoconnect = true
46-
return h.connectAllInternal("", "3")
79+
h.autoupdatePeriod = period
80+
if h.autoupdatePeriod > 0 {
81+
h.autoupdateTicker = time.NewTicker(period)
82+
go func() {
83+
for {
84+
<-h.autoupdateTicker.C
85+
if err := h.UpdateAll(); err != nil {
86+
log.Printf("AutoUpdate issued UpdateAll failed: %v", err)
87+
}
88+
}
89+
}()
90+
}
91+
return nil
4792
}
4893

4994
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -291,20 +336,42 @@ func (h *Handler) connectAll(w http.ResponseWriter, r *http.Request) {
291336
}
292337

293338
func (h *Handler) connectAllInternal(iface string, waitSec string) error {
294-
devices := h.discoverDnsEntries(context.Background(), iface, waitSec)
295-
uuidMap := map[string]application.App{}
339+
ctx := context.Background()
340+
devices := h.discoverDnsEntries(ctx, iface, waitSec)
341+
apps := make(chan *application.App, len(devices)+1)
342+
g, ctx := errgroup.WithContext(ctx)
296343
for _, device := range devices {
297-
app, err := h.connectInternal(device.Addr, device.Port, device.DeviceName)
344+
g.Go(func() error {
345+
log.Printf("Connecting to %s:%d (%s)", device.Addr, device.Port, device.DeviceName)
346+
app, err := h.connectInternal(device.Addr, device.Port, device.DeviceName)
347+
if err != nil {
348+
log.Printf("Connection to %s:%d (%s) failed: %v", device.Addr, device.Port, device.DeviceName, err)
349+
return err
350+
}
351+
log.Printf("Connected to %s:%d (%s)", device.Addr, device.Port, device.DeviceName)
352+
apps <- &app
353+
return nil
354+
})
355+
}
356+
err := g.Wait()
357+
log.Printf("Post wait status: %v", err)
358+
close(apps)
359+
360+
// Even if we cannot connect to some of the devices, we still update the map for remaining devices.
361+
uuidMap := map[string]application.App{}
362+
for app := range apps {
363+
info, err := (*app).Info()
298364
if err != nil {
299-
return err
365+
log.Printf("Skipping device %v", app)
366+
} else {
367+
uuidMap[strings.ReplaceAll(info.SsdpUdn, "-", "")] = *app
300368
}
301-
uuidMap[device.UUID] = app
302369
}
303370

304371
h.mu.Lock()
305372
h.apps = uuidMap
306373
h.mu.Unlock()
307-
return nil
374+
return err
308375
}
309376

310377
func (h *Handler) disconnect(w http.ResponseWriter, r *http.Request) {
@@ -353,7 +420,14 @@ func (h *Handler) status(w http.ResponseWriter, r *http.Request) {
353420
return
354421
}
355422
h.log("status for device")
356-
423+
syncUpdate := r.URL.Query().Get("syncUpdate") == "true"
424+
if syncUpdate {
425+
if err := app.Update(); err != nil {
426+
h.log("error updating status: %v", err)
427+
httpError(w, fmt.Errorf("error updating status: %w", err))
428+
return
429+
}
430+
}
357431
castApplication, castMedia, castVolume := app.Status()
358432
info, err := app.Info()
359433
if err != nil {
@@ -377,8 +451,21 @@ func (h *Handler) status(w http.ResponseWriter, r *http.Request) {
377451
}
378452
}
379453

454+
func (h *Handler) UpdateAll() error {
455+
uuids := h.ConnectedDeviceUUIDs()
456+
g := new(errgroup.Group)
457+
for _, deviceUUID := range uuids {
458+
app, ok := h.app(deviceUUID)
459+
if ok {
460+
g.Go(func() error { return app.Update() })
461+
}
462+
}
463+
return g.Wait()
464+
}
465+
380466
func (h *Handler) statuses(w http.ResponseWriter, r *http.Request) {
381467
h.log("statuses for devices")
468+
syncUpdate := r.URL.Query().Get("syncUpdate") == "true"
382469
uuids := h.ConnectedDeviceUUIDs()
383470
mapUUID2Ch := map[string]chan statusResponse{}
384471
g := new(errgroup.Group)
@@ -388,6 +475,11 @@ func (h *Handler) statuses(w http.ResponseWriter, r *http.Request) {
388475
ch := make(chan statusResponse, 1)
389476
mapUUID2Ch[deviceUUID] = ch
390477
g.Go(func() error {
478+
if syncUpdate {
479+
if err := app.Update(); err != nil {
480+
return err
481+
}
482+
}
391483
castApplication, castMedia, castVolume := app.Status()
392484
info, err := app.Info()
393485
if err != nil {
@@ -404,8 +496,9 @@ func (h *Handler) statuses(w http.ResponseWriter, r *http.Request) {
404496
}
405497
}
406498
if err := g.Wait(); err != nil {
407-
h.log("%v", err)
408-
httpError(w, err)
499+
h.log("collecting statuses failed: %v", err)
500+
httpError(w, fmt.Errorf("collecting statuses failed: %w", err))
501+
return
409502
}
410503

411504
statusResponses := map[string]statusResponse{}
@@ -560,7 +653,7 @@ func (h *Handler) rewind(w http.ResponseWriter, r *http.Request) {
560653
q := r.URL.Query()
561654
seconds := q.Get("seconds")
562655
if seconds == "" {
563-
httpValidationError(w, "missing 'seconds' in query paramater")
656+
httpValidationError(w, "missing 'seconds' in query parameter")
564657
return
565658
}
566659

@@ -589,7 +682,7 @@ func (h *Handler) seek(w http.ResponseWriter, r *http.Request) {
589682
q := r.URL.Query()
590683
seconds := q.Get("seconds")
591684
if seconds == "" {
592-
httpValidationError(w, "missing 'seconds' in query paramater")
685+
httpValidationError(w, "missing 'seconds' in query parameter")
593686
return
594687
}
595688

0 commit comments

Comments
 (0)