Skip to content

Commit

Permalink
cmd: watch command output json and add retries for dropped connections
Browse files Browse the repository at this point in the history
Updates: #85
  • Loading branch information
vishen committed Dec 13, 2020
1 parent 3f1f017 commit 801326d
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 55 deletions.
6 changes: 2 additions & 4 deletions application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,8 @@ func (a *Application) Update() error {
var recvStatus *cast.ReceiverStatusResponse
var err error
// Simple retry. We need this for when the device isn't currently
// available, but it is likely that it will come up soon.
// TODO: This seems to happen when changing media on the cast device,
// not sure how to fix but there might be some way of knowing from the
// payload?
// available, but it is likely that it will come up soon. If the device
// has switch network addresses the caller is expected to handle that situation.
for i := 0; i < a.connectionRetries; i++ {
recvStatus, err = a.getReceiverStatus()
if err == nil {
Expand Down
20 changes: 19 additions & 1 deletion cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func init() {

var (
cache = storage.NewStorage()

// Set up a global dns entry so we can attempt reconnects
entry castdns.CastDNSEntry
)

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

// Used to try and reconnect
if deviceUuid == "" && entry != nil {
deviceUuid = entry.GetUUID()
entry = nil
}

applicationOptions := []application.ApplicationOption{
application.WithDebug(debug),
application.WithCacheDisabled(disableCache),
Expand All @@ -82,7 +91,6 @@ func castApplication(cmd *cobra.Command, args []string) (*application.Applicatio
applicationOptions = append(applicationOptions, application.WithIface(iface))
}

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

// reconnect will attempt to reconnect to the cast device
// TODO: This is all very hacky, currently a global dns entry is set which
// contains the device UUID, and this is then used to reconnect. This should
// be handled much nicer and we shouldn't need to pass around the cmd and args everywhere
// just to reconnect. This might require adding something that wraps the application and
// dns?
func reconnect(cmd *cobra.Command, args []string) (*application.Application, error) {
return castApplication(cmd, args)
}

func getCacheKey(suffix string) string {
return fmt.Sprintf("cmd/utils/dns/%s", suffix)
}
Expand Down
156 changes: 106 additions & 50 deletions cmd/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,81 +15,137 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/buger/jsonparser"
"github.com/spf13/cobra"

"github.com/vishen/go-chromecast/application"
pb "github.com/vishen/go-chromecast/cast/proto"
)

var interval float32

// watchCmd represents the watch command
var watchCmd = &cobra.Command{
Use: "watch",
Short: "Watch all events sent from a chromecast device",
Run: func(cmd *cobra.Command, args []string) {
app, err := castApplication(cmd, args)
if err != nil {
fmt.Printf("unable to get cast application: %v\n", err)
return
interval, _ := cmd.Flags().GetInt("interval")
retries, _ := cmd.Flags().GetInt("retries")
output, _ := cmd.Flags().GetString("output")

o := outputNormal
if strings.ToLower(output) == "json" {
o = outputJSON
}
go func() {
for {
if err := app.Update(); err != nil {
fmt.Printf("unable to update cast application: %v\n", err)

for i := 0; i < retries; i++ {
retry := false
app, err := castApplication(cmd, args)
if err != nil {
fmt.Printf("unable to get cast application: %v\n", err)
time.Sleep(time.Second * 10)
continue
}
done := make(chan struct{}, 1)
go func() {
for {
if err := app.Update(); err != nil {
fmt.Printf("unable to update cast application: %v\n", err)
retry = true
close(done)
return
}
outputStatus(app, o)
time.Sleep(time.Second * time.Duration(interval))
}
}()

app.AddMessageFunc(func(msg *pb.CastMessage) {
protocolVersion := msg.GetProtocolVersion()
sourceID := msg.GetSourceId()
destID := msg.GetDestinationId()
namespace := msg.GetNamespace()

payload := msg.GetPayloadUtf8()
payloadBytes := []byte(payload)
requestID, _ := jsonparser.GetInt(payloadBytes, "requestId")
messageType, _ := jsonparser.GetString(payloadBytes, "type")
// Only log requests that are broadcasted from the chromecast.
if requestID != 0 {
return
}
castApplication, castMedia, castVolume := app.Status()
if castApplication == nil {
fmt.Printf("Idle, volume=%0.2f muted=%t\n", castVolume.Level, castVolume.Muted)
} else if castApplication.IsIdleScreen {
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
} else if castMedia == nil {
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
} else {
metadata := "unknown"
if castMedia.Media.Metadata.Title != "" {
md := castMedia.Media.Metadata
metadata = fmt.Sprintf("title=%q, artist=%q", md.Title, md.Artist)
}
switch castMedia.Media.ContentType {
case "x-youtube/video":
metadata = fmt.Sprintf("id=\"%s\", %s", castMedia.Media.ContentId, metadata)
}
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)

switch o {
case outputJSON:
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{
"type": messageType,
"proto_version": protocolVersion,
"namespace": namespace,
"source_id": sourceID,
"destination_id": destID,
"payload": payload,
})
case outputNormal:
fmt.Printf("CHROMECAST BROADCAST MESSAGE: type=%s proto=%s (namespace=%s) %s -> %s | %s\n", messageType, protocolVersion, namespace, sourceID, destID, payload)
}
time.Sleep(time.Millisecond * time.Duration(interval * 1000))
})
<-done
if retry {
// Sleep a little bit in-between retries
fmt.Println("attempting a retry...")
time.Sleep(time.Second * 10)
}
}()
}
return
},
}

app.AddMessageFunc(func(msg *pb.CastMessage) {
protocolVersion := msg.GetProtocolVersion()
sourceID := msg.GetSourceId()
destID := msg.GetDestinationId()
namespace := msg.GetNamespace()
type outputType int

payload := msg.GetPayloadUtf8()
payloadBytes := []byte(payload)
requestID, _ := jsonparser.GetInt(payloadBytes, "requestId")
messageType, _ := jsonparser.GetString(payloadBytes, "type")
// Only log requests that are broadcasted from the chromecast.
if requestID != 0 {
return
}
const (
outputNormal outputType = iota
outputJSON
)

func outputStatus(app *application.Application, outputType outputType) {
castApplication, castMedia, castVolume := app.Status()

fmt.Printf("CHROMECAST BROADCAST MESSAGE: type=%s proto=%s (namespace=%s) %s -> %s | %s\n", messageType, protocolVersion, namespace, sourceID, destID, payload)
switch outputType {
case outputJSON:
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{
"application": castApplication,
"media": castMedia,
"volume": castVolume,
})
// Wait forever
c := make(chan bool, 1)
<-c
return
},
case outputNormal:
if castApplication == nil {
fmt.Printf("Idle, volume=%0.2f muted=%t\n", castVolume.Level, castVolume.Muted)
} else if castApplication.IsIdleScreen {
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
} else if castMedia == nil {
fmt.Printf("Idle (%s), volume=%0.2f muted=%t\n", castApplication.DisplayName, castVolume.Level, castVolume.Muted)
} else {
metadata := "unknown"
if castMedia.Media.Metadata.Title != "" {
md := castMedia.Media.Metadata
metadata = fmt.Sprintf("title=%q, artist=%q", md.Title, md.Artist)
}
switch castMedia.Media.ContentType {
case "x-youtube/video":
metadata = fmt.Sprintf("id=\"%s\", %s", castMedia.Media.ContentId, metadata)
}
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)
}
}
}

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

0 comments on commit 801326d

Please sign in to comment.