diff --git a/application/application.go b/application/application.go index ee32b64..94eadb8 100644 --- a/application/application.go +++ b/application/application.go @@ -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 { diff --git a/cmd/utils.go b/cmd/utils.go index 3f2b8d5..f5144c3 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -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 { @@ -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), @@ -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 == "" { @@ -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) } diff --git a/cmd/watch.go b/cmd/watch.go index 4efcf14..dae6891 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -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) }