Skip to content

Commit 801326d

Browse files
committed
cmd: watch command output json and add retries for dropped connections
Updates: #85
1 parent 3f1f017 commit 801326d

File tree

3 files changed

+127
-55
lines changed

3 files changed

+127
-55
lines changed

application/application.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,8 @@ func (a *Application) Update() error {
289289
var recvStatus *cast.ReceiverStatusResponse
290290
var err error
291291
// Simple retry. We need this for when the device isn't currently
292-
// available, but it is likely that it will come up soon.
293-
// TODO: This seems to happen when changing media on the cast device,
294-
// not sure how to fix but there might be some way of knowing from the
295-
// payload?
292+
// available, but it is likely that it will come up soon. If the device
293+
// has switch network addresses the caller is expected to handle that situation.
296294
for i := 0; i < a.connectionRetries; i++ {
297295
recvStatus, err = a.getReceiverStatus()
298296
if err == nil {

cmd/utils.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ func init() {
2828

2929
var (
3030
cache = storage.NewStorage()
31+
32+
// Set up a global dns entry so we can attempt reconnects
33+
entry castdns.CastDNSEntry
3134
)
3235

3336
type CachedDNSEntry struct {
@@ -65,6 +68,12 @@ func castApplication(cmd *cobra.Command, args []string) (*application.Applicatio
6568
dnsTimeoutSeconds, _ := cmd.Flags().GetInt("dns-timeout")
6669
useFirstDevice, _ := cmd.Flags().GetBool("first")
6770

71+
// Used to try and reconnect
72+
if deviceUuid == "" && entry != nil {
73+
deviceUuid = entry.GetUUID()
74+
entry = nil
75+
}
76+
6877
applicationOptions := []application.ApplicationOption{
6978
application.WithDebug(debug),
7079
application.WithCacheDisabled(disableCache),
@@ -82,7 +91,6 @@ func castApplication(cmd *cobra.Command, args []string) (*application.Applicatio
8291
applicationOptions = append(applicationOptions, application.WithIface(iface))
8392
}
8493

85-
var entry castdns.CastDNSEntry
8694
// If no address was specified, attempt to determine the address of any
8795
// local chromecast devices.
8896
if addr == "" {
@@ -134,6 +142,16 @@ func castApplication(cmd *cobra.Command, args []string) (*application.Applicatio
134142
return app, nil
135143
}
136144

145+
// reconnect will attempt to reconnect to the cast device
146+
// TODO: This is all very hacky, currently a global dns entry is set which
147+
// contains the device UUID, and this is then used to reconnect. This should
148+
// be handled much nicer and we shouldn't need to pass around the cmd and args everywhere
149+
// just to reconnect. This might require adding something that wraps the application and
150+
// dns?
151+
func reconnect(cmd *cobra.Command, args []string) (*application.Application, error) {
152+
return castApplication(cmd, args)
153+
}
154+
137155
func getCacheKey(suffix string) string {
138156
return fmt.Sprintf("cmd/utils/dns/%s", suffix)
139157
}

cmd/watch.go

Lines changed: 106 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,81 +15,137 @@
1515
package cmd
1616

1717
import (
18+
"encoding/json"
1819
"fmt"
20+
"os"
21+
"strings"
1922
"time"
2023

2124
"github.com/buger/jsonparser"
2225
"github.com/spf13/cobra"
2326

27+
"github.com/vishen/go-chromecast/application"
2428
pb "github.com/vishen/go-chromecast/cast/proto"
2529
)
2630

27-
var interval float32
28-
2931
// watchCmd represents the watch command
3032
var watchCmd = &cobra.Command{
3133
Use: "watch",
3234
Short: "Watch all events sent from a chromecast device",
3335
Run: func(cmd *cobra.Command, args []string) {
34-
app, err := castApplication(cmd, args)
35-
if err != nil {
36-
fmt.Printf("unable to get cast application: %v\n", err)
37-
return
36+
interval, _ := cmd.Flags().GetInt("interval")
37+
retries, _ := cmd.Flags().GetInt("retries")
38+
output, _ := cmd.Flags().GetString("output")
39+
40+
o := outputNormal
41+
if strings.ToLower(output) == "json" {
42+
o = outputJSON
3843
}
39-
go func() {
40-
for {
41-
if err := app.Update(); err != nil {
42-
fmt.Printf("unable to update cast application: %v\n", err)
44+
45+
for i := 0; i < retries; i++ {
46+
retry := false
47+
app, err := castApplication(cmd, args)
48+
if err != nil {
49+
fmt.Printf("unable to get cast application: %v\n", err)
50+
time.Sleep(time.Second * 10)
51+
continue
52+
}
53+
done := make(chan struct{}, 1)
54+
go func() {
55+
for {
56+
if err := app.Update(); err != nil {
57+
fmt.Printf("unable to update cast application: %v\n", err)
58+
retry = true
59+
close(done)
60+
return
61+
}
62+
outputStatus(app, o)
63+
time.Sleep(time.Second * time.Duration(interval))
64+
}
65+
}()
66+
67+
app.AddMessageFunc(func(msg *pb.CastMessage) {
68+
protocolVersion := msg.GetProtocolVersion()
69+
sourceID := msg.GetSourceId()
70+
destID := msg.GetDestinationId()
71+
namespace := msg.GetNamespace()
72+
73+
payload := msg.GetPayloadUtf8()
74+
payloadBytes := []byte(payload)
75+
requestID, _ := jsonparser.GetInt(payloadBytes, "requestId")
76+
messageType, _ := jsonparser.GetString(payloadBytes, "type")
77+
// Only log requests that are broadcasted from the chromecast.
78+
if requestID != 0 {
4379
return
4480
}
45-
castApplication, castMedia, castVolume := app.Status()
46-
if castApplication == nil {
47-
fmt.Printf("Idle, volume=%0.2f muted=%t\n", castVolume.Level, castVolume.Muted)
48-
} else if castApplication.IsIdleScreen {
49-
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
50-
} else if castMedia == nil {
51-
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
52-
} else {
53-
metadata := "unknown"
54-
if castMedia.Media.Metadata.Title != "" {
55-
md := castMedia.Media.Metadata
56-
metadata = fmt.Sprintf("title=%q, artist=%q", md.Title, md.Artist)
57-
}
58-
switch castMedia.Media.ContentType {
59-
case "x-youtube/video":
60-
metadata = fmt.Sprintf("id=\"%s\", %s", castMedia.Media.ContentId, metadata)
61-
}
62-
fmt.Printf(">> %s (%s), %s, time remaining=%.2fs/%.2fs, volume=%0.2f, muted=%t\n", castApplication.DisplayName, castMedia.PlayerState, metadata, castMedia.CurrentTime, castMedia.Media.Duration, castVolume.Level, castVolume.Muted)
81+
82+
switch o {
83+
case outputJSON:
84+
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{
85+
"type": messageType,
86+
"proto_version": protocolVersion,
87+
"namespace": namespace,
88+
"source_id": sourceID,
89+
"destination_id": destID,
90+
"payload": payload,
91+
})
92+
case outputNormal:
93+
fmt.Printf("CHROMECAST BROADCAST MESSAGE: type=%s proto=%s (namespace=%s) %s -> %s | %s\n", messageType, protocolVersion, namespace, sourceID, destID, payload)
6394
}
64-
time.Sleep(time.Millisecond * time.Duration(interval * 1000))
95+
})
96+
<-done
97+
if retry {
98+
// Sleep a little bit in-between retries
99+
fmt.Println("attempting a retry...")
100+
time.Sleep(time.Second * 10)
65101
}
66-
}()
102+
}
103+
return
104+
},
105+
}
67106

68-
app.AddMessageFunc(func(msg *pb.CastMessage) {
69-
protocolVersion := msg.GetProtocolVersion()
70-
sourceID := msg.GetSourceId()
71-
destID := msg.GetDestinationId()
72-
namespace := msg.GetNamespace()
107+
type outputType int
73108

74-
payload := msg.GetPayloadUtf8()
75-
payloadBytes := []byte(payload)
76-
requestID, _ := jsonparser.GetInt(payloadBytes, "requestId")
77-
messageType, _ := jsonparser.GetString(payloadBytes, "type")
78-
// Only log requests that are broadcasted from the chromecast.
79-
if requestID != 0 {
80-
return
81-
}
109+
const (
110+
outputNormal outputType = iota
111+
outputJSON
112+
)
113+
114+
func outputStatus(app *application.Application, outputType outputType) {
115+
castApplication, castMedia, castVolume := app.Status()
82116

83-
fmt.Printf("CHROMECAST BROADCAST MESSAGE: type=%s proto=%s (namespace=%s) %s -> %s | %s\n", messageType, protocolVersion, namespace, sourceID, destID, payload)
117+
switch outputType {
118+
case outputJSON:
119+
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{
120+
"application": castApplication,
121+
"media": castMedia,
122+
"volume": castVolume,
84123
})
85-
// Wait forever
86-
c := make(chan bool, 1)
87-
<-c
88-
return
89-
},
124+
case outputNormal:
125+
if castApplication == nil {
126+
fmt.Printf("Idle, volume=%0.2f muted=%t\n", castVolume.Level, castVolume.Muted)
127+
} else if castApplication.IsIdleScreen {
128+
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
129+
} else if castMedia == nil {
130+
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
131+
} else {
132+
metadata := "unknown"
133+
if castMedia.Media.Metadata.Title != "" {
134+
md := castMedia.Media.Metadata
135+
metadata = fmt.Sprintf("title=%q, artist=%q", md.Title, md.Artist)
136+
}
137+
switch castMedia.Media.ContentType {
138+
case "x-youtube/video":
139+
metadata = fmt.Sprintf("id=\"%s\", %s", castMedia.Media.ContentId, metadata)
140+
}
141+
fmt.Printf(">> %s (%s), %s, time remaining=%.2fs/%.2fs, volume=%0.2f, muted=%t\n", castApplication.DisplayName, castMedia.PlayerState, metadata, castMedia.CurrentTime, castMedia.Media.Duration, castVolume.Level, castVolume.Muted)
142+
}
143+
}
90144
}
91145

92146
func init() {
93-
watchCmd.Flags().Float32Var(&interval, "interval", 10, "interval between status poll in seconds")
147+
watchCmd.Flags().Int("interval", 10, "interval between status poll in seconds")
148+
watchCmd.Flags().Int("retries", 10, "times to retry when losing chromecast connection")
149+
watchCmd.Flags().String("output", "normal", "output format: normal or json")
94150
rootCmd.AddCommand(watchCmd)
95151
}

0 commit comments

Comments
 (0)