diff --git a/.goreleaser.yml b/.goreleaser.yml index 37022675..4282a1a8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,11 +1,6 @@ # This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com -before: - hooks: - # you may remove this if you don't use vgo - #- go mod download - # you may remove this if you don't need go generate - #- go generate ./... +version: 2 builds: - env: - CGO_ENABLED=0 @@ -35,10 +30,10 @@ builds: - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} archives: - id: build - format: tar.gz + formats: tar.gz format_overrides: - goos: windows - format: zip + formats: zip checksum: name_template: 'checksums.txt' changelog: diff --git a/.mockery.yml b/.mockery.yml new file mode 100644 index 00000000..05f7c4fc --- /dev/null +++ b/.mockery.yml @@ -0,0 +1,2 @@ +all: True +dir: "{{.InterfaceDir}}/mocks" diff --git a/application/application.go b/application/application.go index f15f7c46..977bcf65 100644 --- a/application/application.go +++ b/application/application.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path" + "slices" "strconv" "strings" "sync" @@ -18,13 +19,14 @@ import ( "github.com/h2non/filetype" + "path/filepath" + "github.com/buger/jsonparser" "github.com/pkg/errors" "github.com/vishen/go-chromecast/cast" pb "github.com/vishen/go-chromecast/cast/proto" "github.com/vishen/go-chromecast/playlists" "github.com/vishen/go-chromecast/storage" - "path/filepath" ) var ( @@ -388,7 +390,7 @@ func (a *Application) Update() error { // Simple retry. We need this for when the device isn't currently // 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++ { + for i := range a.connectionRetries { recvStatus, err = a.getReceiverStatus() if err == nil { break @@ -496,13 +498,9 @@ func (a *Application) TogglePause() error { } switch a.media.PlayerState { case "PLAYING", "BUFFERING": - { - return a.Pause() - } + return a.Pause() default: - { - return a.Unpause() - } + return a.Unpause() } } @@ -604,11 +602,9 @@ func (a *Application) Seek(value int) error { "CC1AD845", // Default media } - for _, app := range appsSeekTo { - if app == a.application.AppId { - absolute := a.media.CurrentTime + float32(value) - return a.SeekToTime(absolute) - } + if slices.Contains(appsSeekTo, a.application.AppId) { + absolute := a.media.CurrentTime + float32(value) + return a.SeekToTime(absolute) } return a.sendMediaRecv(&cast.MediaHeader{ @@ -1197,7 +1193,7 @@ func (a *Application) startStreamingServer() error { if !liveStreaming { http.ServeFile(w, r, filename) } else { - a.serveLiveStreaming(w, r, filename) + a.serveLiveStreaming(w, filename) } } else { http.Error(w, "Invalid file", 400) @@ -1221,7 +1217,7 @@ func (a *Application) startStreamingServer() error { return nil } -func (a *Application) serveLiveStreaming(w http.ResponseWriter, r *http.Request, filename string) { +func (a *Application) serveLiveStreaming(w http.ResponseWriter, filename string) { cmd := exec.Command( "ffmpeg", "-re", // encode at 1x playback speed, to not burn the CPU @@ -1250,7 +1246,7 @@ func (a *Application) serveLiveStreaming(w http.ResponseWriter, r *http.Request, } } -func (a *Application) log(message string, args ...interface{}) { +func (a *Application) log(message string, args ...any) { if a.debug { log.WithField("package", "application").Infof(message, args...) } @@ -1318,21 +1314,10 @@ func (a *Application) sendMediaRecv(payload cast.Payload) error { return err } -func (a *Application) sendAndWaitDefaultConn(payload cast.Payload) (*pb.CastMessage, error) { - return a.sendAndWait(payload, defaultSender, defaultRecv, namespaceConn) -} - func (a *Application) sendAndWaitDefaultRecv(payload cast.Payload) (*pb.CastMessage, error) { return a.sendAndWait(payload, defaultSender, defaultRecv, namespaceRecv) } -func (a *Application) sendAndWaitMediaConn(payload cast.Payload) (*pb.CastMessage, error) { - if a.application == nil { - return nil, ErrApplicationNotSet - } - return a.sendAndWait(payload, defaultSender, a.application.TransportId, namespaceConn) -} - func (a *Application) sendAndWaitMediaRecv(payload cast.Payload) (*pb.CastMessage, error) { if a.application == nil { return nil, ErrApplicationNotSet diff --git a/application/info.go b/application/info.go index 0acf3eac..0e1fca4d 100644 --- a/application/info.go +++ b/application/info.go @@ -9,11 +9,10 @@ import ( "github.com/vishen/go-chromecast/cast" ) -// getInfo uses the http://:8008/setup/eureka_endpoint to obtain more +// GetInfo uses the http://:8008/setup/eureka_endpoint to obtain more // information about the cast-device. // OBS: The 8008 seems to be pure http, whereas 8009 is typically the port // to use for protobuf-communication, - func GetInfo(ip string) (info *cast.DeviceInfo, err error) { // Note: Services exposed not on 8009 port are "Google Cast Group"s // The only way to find the true device (group) name, is using mDNS outside of this function. diff --git a/application/mocks/App.go b/application/mocks/App.go index 9649ebef..96aa6028 100644 --- a/application/mocks/App.go +++ b/application/mocks/App.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.49.1. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mocks @@ -39,7 +39,7 @@ func (_m *App) Close(stopMedia bool) error { return r0 } -// Info provides a mock function with given fields: +// Info provides a mock function with no fields func (_m *App) Info() (*cast.DeviceInfo, error) { ret := _m.Called() @@ -105,7 +105,7 @@ func (_m *App) LoadApp(appID string, contentID string) error { return r0 } -// Next provides a mock function with given fields: +// Next provides a mock function with no fields func (_m *App) Next() error { ret := _m.Called() @@ -123,7 +123,7 @@ func (_m *App) Next() error { return r0 } -// Pause provides a mock function with given fields: +// Pause provides a mock function with no fields func (_m *App) Pause() error { ret := _m.Called() @@ -159,7 +159,7 @@ func (_m *App) PlayableMediaType(filename string) bool { return r0 } -// PlayedItems provides a mock function with given fields: +// PlayedItems provides a mock function with no fields func (_m *App) PlayedItems() map[string]application.PlayedItem { ret := _m.Called() @@ -179,7 +179,7 @@ func (_m *App) PlayedItems() map[string]application.PlayedItem { return r0 } -// Previous provides a mock function with given fields: +// Previous provides a mock function with no fields func (_m *App) Previous() error { ret := _m.Called() @@ -335,7 +335,7 @@ func (_m *App) SetVolume(value float32) error { return r0 } -// Skipad provides a mock function with given fields: +// Skipad provides a mock function with no fields func (_m *App) Skipad() error { ret := _m.Called() @@ -389,7 +389,7 @@ func (_m *App) Start(addr string, port int) error { return r0 } -// Status provides a mock function with given fields: +// Status provides a mock function with no fields func (_m *App) Status() (*cast.Application, *cast.Media, *cast.Volume) { ret := _m.Called() @@ -430,7 +430,7 @@ func (_m *App) Status() (*cast.Application, *cast.Media, *cast.Volume) { return r0, r1, r2 } -// Stop provides a mock function with given fields: +// Stop provides a mock function with no fields func (_m *App) Stop() error { ret := _m.Called() @@ -448,7 +448,7 @@ func (_m *App) Stop() error { return r0 } -// StopMedia provides a mock function with given fields: +// StopMedia provides a mock function with no fields func (_m *App) StopMedia() error { ret := _m.Called() @@ -466,7 +466,7 @@ func (_m *App) StopMedia() error { return r0 } -// TogglePause provides a mock function with given fields: +// TogglePause provides a mock function with no fields func (_m *App) TogglePause() error { ret := _m.Called() @@ -509,7 +509,7 @@ func (_m *App) Transcode(contentType string, command string, args ...string) err return r0 } -// Unpause provides a mock function with given fields: +// Unpause provides a mock function with no fields func (_m *App) Unpause() error { ret := _m.Called() @@ -527,7 +527,7 @@ func (_m *App) Unpause() error { return r0 } -// Update provides a mock function with given fields: +// Update provides a mock function with no fields func (_m *App) Update() error { ret := _m.Called() diff --git a/application/mocks/ApplicationOption.go b/application/mocks/ApplicationOption.go index 22d79ced..f120284c 100644 --- a/application/mocks/ApplicationOption.go +++ b/application/mocks/ApplicationOption.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.49.1. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mocks diff --git a/application/mocks/CastMessageFunc.go b/application/mocks/CastMessageFunc.go index 036569ce..1e0070df 100644 --- a/application/mocks/CastMessageFunc.go +++ b/application/mocks/CastMessageFunc.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.49.1. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mocks diff --git a/cast/connection.go b/cast/connection.go index 5e044d80..eb6cb45a 100644 --- a/cast/connection.go +++ b/cast/connection.go @@ -101,7 +101,7 @@ func (c *Connection) RemotePort() (port string, err error) { return port, err } -func (c *Connection) log(message string, args ...interface{}) { +func (c *Connection) log(message string, args ...any) { if c.debug { log.WithField("package", "cast").Debugf(message, args...) } @@ -212,12 +212,11 @@ func (c *Connection) receiveLoop(ctx context.Context) { continue } - c.handleMessage(requestIDi, message, &headers) + c.handleMessage(requestIDi, message) } } -func (c *Connection) handleMessage(requestID int, message *pb.CastMessage, headers *PayloadHeader) { - +func (c *Connection) handleMessage(requestID int, message *pb.CastMessage) { messageType, err := jsonparser.GetString([]byte(*message.PayloadUtf8), "type") if err != nil { c.log("could not find 'type' key in response message request_id=%d %q: %s", requestID, *message.PayloadUtf8, err) diff --git a/cast/mocks/Conn.go b/cast/mocks/Conn.go index f73946ba..8254d4b8 100644 --- a/cast/mocks/Conn.go +++ b/cast/mocks/Conn.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.49.1. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mocks @@ -14,7 +14,7 @@ type Conn struct { mock.Mock } -// Close provides a mock function with given fields: +// Close provides a mock function with no fields func (_m *Conn) Close() error { ret := _m.Called() @@ -32,7 +32,7 @@ func (_m *Conn) Close() error { return r0 } -// LocalAddr provides a mock function with given fields: +// LocalAddr provides a mock function with no fields func (_m *Conn) LocalAddr() (string, error) { ret := _m.Called() @@ -60,7 +60,7 @@ func (_m *Conn) LocalAddr() (string, error) { return r0, r1 } -// MsgChan provides a mock function with given fields: +// MsgChan provides a mock function with no fields func (_m *Conn) MsgChan() chan *api.CastMessage { ret := _m.Called() @@ -80,7 +80,7 @@ func (_m *Conn) MsgChan() chan *api.CastMessage { return r0 } -// RemoteAddr provides a mock function with given fields: +// RemoteAddr provides a mock function with no fields func (_m *Conn) RemoteAddr() (string, error) { ret := _m.Called() @@ -108,7 +108,7 @@ func (_m *Conn) RemoteAddr() (string, error) { return r0, r1 } -// RemotePort provides a mock function with given fields: +// RemotePort provides a mock function with no fields func (_m *Conn) RemotePort() (string, error) { ret := _m.Called() diff --git a/cast/mocks/Payload.go b/cast/mocks/Payload.go index 78986077..b1c9552e 100644 --- a/cast/mocks/Payload.go +++ b/cast/mocks/Payload.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.49.1. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mocks diff --git a/cast/payload.go b/cast/payload.go index 65a3290b..3258f523 100644 --- a/cast/payload.go +++ b/cast/payload.go @@ -96,11 +96,11 @@ type LaunchRequest struct { type LoadMediaCommand struct { PayloadHeader - Media MediaItem `json:"media"` - CurrentTime int `json:"currentTime"` - Autoplay bool `json:"autoplay"` - QueueData QueueData `json:"queueData"` - CustomData interface{} `json:"customData"` + Media MediaItem `json:"media"` + CurrentTime int `json:"currentTime"` + Autoplay bool `json:"autoplay"` + QueueData QueueData `json:"queueData"` + CustomData any `json:"customData"` } type QueueData struct { diff --git a/cmd/httpserver.go b/cmd/httpserver.go index c4752172..9cbbff13 100644 --- a/cmd/httpserver.go +++ b/cmd/httpserver.go @@ -23,19 +23,13 @@ import ( var httpserverCmd = &cobra.Command{ Use: "httpserver", Short: "Start the HTTP server", - Long: `Start the HTTP server which provides an HTTP -api to control chromecast devices on a network.`, + Long: `Start the HTTP server which provides an HTTP API to control chromecast devices on a network.`, Run: func(cmd *cobra.Command, args []string) { - addr, _ := cmd.Flags().GetString("http-addr") port, _ := cmd.Flags().GetString("http-port") - - // TODO: Should only need verbose, but debug has stupidly hijacked - // the -v flag... verbose, _ := cmd.Flags().GetBool("verbose") - debug, _ := cmd.Flags().GetBool("debug") - if err := http.NewHandler(verbose || debug).Serve(addr + ":" + port); err != nil { + if err := http.NewHandler(verbose).Serve(addr + ":" + port); err != nil { exit("unable to run http server: %v", err) } }, diff --git a/cmd/load-app.go b/cmd/load-app.go index 0478275f..d136188a 100644 --- a/cmd/load-app.go +++ b/cmd/load-app.go @@ -29,37 +29,44 @@ the chromecast receiver app to be specified. An older list can be found here https://gist.github.com/jloutsenhizer/8855258. `, Run: func(cmd *cobra.Command, args []string) { - if len(args) != 2 { - exit("requires exactly two arguments") - } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - - // Optionally run a UI when playing this media: runWithUI, _ := cmd.Flags().GetBool("with-ui") - if runWithUI { - go func() { - if err := app.LoadApp(args[0], args[1]); err != nil { - exit("unable to load media: %v", err) - } - }() - ccui, err := ui.NewUserInterface(app) - if err != nil { - exit("unable to prepare a new user-interface: %v", err) - } - if err := ccui.Run(); err != nil { - exit("unable to run ui: %v", err) + app := NewCast(cmd) + app.LoadApp(runWithUI, args) + }, +} + +// LoadApp exports the load-app command +func (a *App) LoadApp(runWithUI bool, args []string) { + if len(args) != 2 { + exit("requires exactly two arguments") + } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + + // Optionally run a UI when playing this media: + if runWithUI { + go func() { + if err := app.LoadApp(args[0], args[1]); err != nil { + exit("unable to load media: %v", err) } - } + }() - // Otherwise just run in CLI mode: - if err := app.LoadApp(args[0], args[1]); err != nil { - exit("unable to load media: %v", err) + ccui, err := ui.NewUserInterface(app) + if err != nil { + exit("unable to prepare a new user-interface: %v", err) } - }, + if err := ccui.Run(); err != nil { + exit("unable to run ui: %v", err) + } + } + + // Otherwise just run in CLI mode: + if err := app.LoadApp(args[0], args[1]); err != nil { + exit("unable to load media: %v", err) + } } func init() { diff --git a/cmd/load.go b/cmd/load.go index 21f90ed4..b645e4a3 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -32,43 +32,49 @@ If the media file is an unplayable media type by the chromecast, this will attempt to transcode the media file to mp4 using ffmpeg. This requires that ffmpeg is installed.`, Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - exit("requires exactly one argument, should be the media file to load") - } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - contentType, _ := cmd.Flags().GetString("content-type") + startTime, _ := cmd.Flags().GetInt("start-time") transcode, _ := cmd.Flags().GetBool("transcode") detach, _ := cmd.Flags().GetBool("detach") - startTime, _ := cmd.Flags().GetInt("start-time") - - // Optionally run a UI when playing this media: runWithUI, _ := cmd.Flags().GetBool("with-ui") - if runWithUI { - go func() { - if err := app.Load(args[0], startTime, contentType, transcode, detach, false); err != nil { - exit("unable to load media: %v", err) - } - }() - ccui, err := ui.NewUserInterface(app) - if err != nil { - exit("unable to prepare a new user-interface: %v", err) - } - if err := ccui.Run(); err != nil { - exit("unable to run ui: %v", err) + app := NewCast(cmd) + app.Load(contentType, startTime, transcode, detach, runWithUI, args) + }, +} + +// Load exports the load command +func (a *App) Load(contentType string, startTime int, transcode, detach, runWithUI bool, args []string) { + if len(args) != 1 { + exit("requires exactly one argument, should be the media file to load") + } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + + // Optionally run a UI when playing this media: + if runWithUI { + go func() { + if err := app.Load(args[0], startTime, contentType, transcode, detach, false); err != nil { + exit("unable to load media: %v", err) } - return - } + }() - // Otherwise just run in CLI mode: - if err := app.Load(args[0], startTime, contentType, transcode, detach, false); err != nil { - exit("unable to load media: %v", err) + ccui, err := ui.NewUserInterface(app) + if err != nil { + exit("unable to prepare a new user-interface: %v", err) } - }, + if err := ccui.Run(); err != nil { + exit("unable to run ui: %v", err) + } + return + } + + // Otherwise just run in CLI mode: + if err := app.Load(args[0], startTime, contentType, transcode, detach, false); err != nil { + exit("unable to load media: %v", err) + } } func init() { diff --git a/cmd/ls.go b/cmd/ls.go index b09f4ffc..46ad7897 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -30,30 +30,37 @@ var lsCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { ifaceName, _ := cmd.Flags().GetString("iface") dnsTimeoutSeconds, _ := cmd.Flags().GetInt("dns-timeout") - var iface *net.Interface - var err error - if ifaceName != "" { - if iface, err = net.InterfaceByName(ifaceName); err != nil { - exit("unable to find interface %q: %v", ifaceName, err) - } - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(dnsTimeoutSeconds)) - defer cancel() - castEntryChan, err := castdns.DiscoverCastDNSEntries(ctx, iface) - if err != nil { - exit("unable to discover chromecast devices: %v", err) - } - i := 1 - for d := range castEntryChan { - outputInfo("%d) device=%q device_name=%q address=\"%s:%d\" uuid=%q", i, d.Device, d.DeviceName, d.AddrV4, d.Port, d.UUID) - i++ - } - if i == 1 { - outputError("no cast devices found on network") - } + Ls(ifaceName, dnsTimeoutSeconds) }, } +// Ls exports the ls command +func Ls(ifaceName string, dnsTimeoutSeconds int) { + var ( + iface *net.Interface + err error + ) + if ifaceName != "" { + if iface, err = net.InterfaceByName(ifaceName); err != nil { + exit("unable to find interface %q: %v", ifaceName, err) + } + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(dnsTimeoutSeconds)) + defer cancel() + castEntryChan, err := castdns.DiscoverCastDNSEntries(ctx, iface) + if err != nil { + exit("unable to discover chromecast devices: %v", err) + } + i := 1 + for d := range castEntryChan { + outputInfo("%d) device=%q device_name=%q address=\"%s:%d\" uuid=%q", i, d.Device, d.DeviceName, d.AddrV4, d.Port, d.UUID) + i++ + } + if i == 1 { + outputError("no cast devices found on network") + } +} + func init() { rootCmd.AddCommand(lsCmd) } diff --git a/cmd/mute.go b/cmd/mute.go index 31b8e770..beaf10bc 100644 --- a/cmd/mute.go +++ b/cmd/mute.go @@ -23,16 +23,22 @@ var muteCmd = &cobra.Command{ Use: "mute", Short: "Mute the chromecast", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.SetMuted(true); err != nil { - exit("unable to mute cast application: %v", err) - } + app := NewCast(cmd) + app.Mute() }, } +// Mute exports the mute command +func (a *App) Mute() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.SetMuted(true); err != nil { + exit("unable to mute cast application: %v", err) + } +} + func init() { rootCmd.AddCommand(muteCmd) } diff --git a/cmd/next.go b/cmd/next.go index 1c06d3ae..a5155537 100644 --- a/cmd/next.go +++ b/cmd/next.go @@ -23,16 +23,22 @@ var nextCmd = &cobra.Command{ Use: "next", Short: "Play the next available media", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Next(); err != nil { - exit("unable to play next media: %v", err) - } + app := NewCast(cmd) + app.Next() }, } +// Next exports the next command +func (a *App) Next() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Next(); err != nil { + exit("unable to play next media: %v", err) + } +} + func init() { rootCmd.AddCommand(nextCmd) } diff --git a/cmd/pause.go b/cmd/pause.go index 05d53a7c..de140bf5 100644 --- a/cmd/pause.go +++ b/cmd/pause.go @@ -23,16 +23,22 @@ var pauseCmd = &cobra.Command{ Use: "pause", Short: "Pause the currently playing media on the chromecast", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Pause(); err != nil { - exit("unable to pause cast application: %v", err) - } + app := NewCast(cmd) + app.Pause() }, } +// Pause exports the pause command +func (a *App) Pause() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Pause(); err != nil { + exit("unable to pause cast application: %v", err) + } +} + func init() { rootCmd.AddCommand(pauseCmd) } diff --git a/cmd/playlist.go b/cmd/playlist.go index 6ebafc5c..98efdfd8 100644 --- a/cmd/playlist.go +++ b/cmd/playlist.go @@ -16,7 +16,6 @@ package cmd import ( "bufio" - "io/ioutil" "os" "path/filepath" "sort" @@ -45,178 +44,188 @@ If the media file is an unplayable media type by the chromecast, this will attempt to transcode the media file to mp4 using ffmpeg. This requires that ffmpeg is installed.`, Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - exit("requires exactly one argument, should be the folder to play media from") - } - if fileInfo, err := os.Stat(args[0]); err != nil { - exit("unable to find %q: %v", args[0], err) - } else if !fileInfo.Mode().IsDir() { - exit("%q is not a directory", args[0]) - } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - contentType, _ := cmd.Flags().GetString("content-type") transcode, _ := cmd.Flags().GetBool("transcode") forcePlay, _ := cmd.Flags().GetBool("force-play") continuePlaying, _ := cmd.Flags().GetBool("continue") selection, _ := cmd.Flags().GetBool("select") - files, err := ioutil.ReadDir(args[0]) - if err != nil { - exit("unable to list files from %q: %v", args[0], err) + runWithUI, _ := cmd.Flags().GetBool("with-ui") + + app := NewCast(cmd) + app.Playlist(contentType, + transcode, forcePlay, continuePlaying, selection, runWithUI, + args, + ) + }, +} + +// Playlist exports the playlist command +func (a *App) Playlist(contentType string, transcode, forcePlay, continuePlaying, selection, runWithUI bool, args []string) { + if len(args) != 1 { + exit("requires exactly one argument, should be the folder to play media from") + } + if fileInfo, err := os.Stat(args[0]); err != nil { + exit("unable to find %q: %v", args[0], err) + } else if !fileInfo.Mode().IsDir() { + exit("%q is not a directory", args[0]) + } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + + files, err := os.ReadDir(args[0]) + if err != nil { + exit("unable to list files from %q: %v", args[0], err) + } + filesToPlay := make([]mediaFile, 0, len(files)) + for _, f := range files { + if !forcePlay && !app.PlayableMediaType(filepath.Join(args[0], f.Name())) { + continue } - filesToPlay := make([]mediaFile, 0, len(files)) - for _, f := range files { - if !forcePlay && !app.PlayableMediaType(filepath.Join(args[0], f.Name())) { - continue - } - foundNum := false - numPos := 0 - foundNumbers := []int{} - for i, c := range f.Name() { - if c < '0' || c > '9' { - if foundNum { - val, _ := strconv.Atoi(f.Name()[numPos:i]) - foundNumbers = append(foundNumbers, val) - } - foundNum = false - continue + foundNum := false + numPos := 0 + foundNumbers := []int{} + for i, c := range f.Name() { + if c < '0' || c > '9' { + if foundNum { + val, _ := strconv.Atoi(f.Name()[numPos:i]) + foundNumbers = append(foundNumbers, val) } + foundNum = false + continue + } - if !foundNum { - numPos = i - foundNum = true - } + if !foundNum { + numPos = i + foundNum = true } + } - filesToPlay = append(filesToPlay, mediaFile{ - filename: f.Name(), - possibleNumbers: foundNumbers, - }) + filesToPlay = append(filesToPlay, mediaFile{ + filename: f.Name(), + possibleNumbers: foundNumbers, + }) - } + } - sort.Slice(filesToPlay, func(i, j int) bool { - iNum := filesToPlay[i].possibleNumbers - jNum := filesToPlay[j].possibleNumbers - if len(iNum) == 0 { + sort.Slice(filesToPlay, func(i, j int) bool { + iNum := filesToPlay[i].possibleNumbers + jNum := filesToPlay[j].possibleNumbers + if len(iNum) == 0 { + return false + } + if len(jNum) == 0 { + return true + } + max := len(iNum) + if len(iNum) < len(jNum) { + max = len(jNum) + } + for vi := range max { + if len(iNum) <= vi { return false } - if len(jNum) == 0 { + if len(jNum) <= vi { return true } - max := len(iNum) - if len(iNum) < len(jNum) { - max = len(jNum) + if iNum[vi] == jNum[vi] { + continue } - for vi := 0; vi < max; vi++ { - if len(iNum) <= vi { - return false - } - if len(jNum) <= vi { - return true - } - if iNum[vi] == jNum[vi] { - continue - } - if iNum[vi] > jNum[vi] { - return false - } - return true + if iNum[vi] > jNum[vi] { + return false } return true - }) - - filenames := make([]string, len(filesToPlay)) - for i, f := range filesToPlay { - filename := filepath.Join(args[0], f.filename) - filenames[i] = filename } - - indexToPlayFrom := 0 - if selection { - outputInfo("Will play the following items, select where to start from:") - for i, f := range filenames { - lastPlayed := "never" - if lp, ok := app.PlayedItems()[f]; ok { - t := time.Unix(lp.Started, 0) - lastPlayed = t.String() - } - outputInfo("%d) %s: last played %q", i+1, f, lastPlayed) - } - reader := bufio.NewReader(os.Stdin) - for { - outputInfo("Enter selection: ") - text, err := reader.ReadString('\n') - if err != nil { - outputError("reading console: %v", err) - continue - } - i, err := strconv.Atoi(strings.TrimSpace(text)) - if err != nil { - continue - } else if i < 1 || i > len(filenames) { - continue - } - indexToPlayFrom = i - 1 - break + return true + }) + + filenames := make([]string, len(filesToPlay)) + for i, f := range filesToPlay { + filename := filepath.Join(args[0], f.filename) + filenames[i] = filename + } + + indexToPlayFrom := 0 + if selection { + outputInfo("Will play the following items, select where to start from:") + for i, f := range filenames { + lastPlayed := "never" + if lp, ok := app.PlayedItems()[f]; ok { + t := time.Unix(lp.Started, 0) + lastPlayed = t.String() } - } else if continuePlaying { - var lastPlayedStartUnix int64 = 0 - var lastPlayedEndUnix int64 = 0 - lastPlayedIndex := 0 - for i, f := range filenames { - p, ok := app.PlayedItems()[f] - if ok && p.Started > lastPlayedStartUnix { - lastPlayedStartUnix = p.Started - lastPlayedEndUnix = p.Finished - lastPlayedIndex = i - } + outputInfo("%d) %s: last played %q", i+1, f, lastPlayed) + } + reader := bufio.NewReader(os.Stdin) + for { + outputInfo("Enter selection: ") + text, err := reader.ReadString('\n') + if err != nil { + outputError("reading console: %v", err) + continue } - - if lastPlayedIndex > 0 { - if lastPlayedStartUnix < lastPlayedEndUnix { - if len(filenames) > lastPlayedIndex { - // lastPlayedIndex += 1 - } else { - lastPlayedIndex = 0 - } - } + i, err := strconv.Atoi(strings.TrimSpace(text)) + if err != nil { + continue + } else if i < 1 || i > len(filenames) { + continue } - indexToPlayFrom = lastPlayedIndex + indexToPlayFrom = i - 1 + break } - - s := "Attemping to play the following media:" - for _, f := range filenames[indexToPlayFrom:] { - s += "- " + f + " " + } else if continuePlaying { + var lastPlayedStartUnix int64 = 0 + var lastPlayedEndUnix int64 = 0 + lastPlayedIndex := 0 + for i, f := range filenames { + p, ok := app.PlayedItems()[f] + if ok && p.Started > lastPlayedStartUnix { + lastPlayedStartUnix = p.Started + lastPlayedEndUnix = p.Finished + lastPlayedIndex = i + } } - outputInfo(s) - // Optionally run a UI when playing this media: - runWithUI, _ := cmd.Flags().GetBool("with-ui") - if runWithUI { - go func() { - if err := app.QueueLoad(filenames[indexToPlayFrom:], contentType, transcode); err != nil { - exit("unable to play playlist on cast application: %v", err) + if lastPlayedIndex > 0 { + if lastPlayedStartUnix < lastPlayedEndUnix { + if len(filenames) > lastPlayedIndex { + // lastPlayedIndex += 1 + } else { + lastPlayedIndex = 0 } - }() - - ccui, err := ui.NewUserInterface(app) - if err != nil { - exit("unable to prepare a new user-interface: %v", err) - } - if err := ccui.Run(); err != nil { - exit("unable to run ui: %v", err) } } + indexToPlayFrom = lastPlayedIndex + } + + s := "Attemping to play the following media:" + for _, f := range filenames[indexToPlayFrom:] { + s += "- " + f + " " + } + outputInfo(s) + + // Optionally run a UI when playing this media: + if runWithUI { + go func() { + if err := app.QueueLoad(filenames[indexToPlayFrom:], contentType, transcode); err != nil { + exit("unable to play playlist on cast application: %v", err) + } + }() - if err := app.QueueLoad(filenames[indexToPlayFrom:], contentType, transcode); err != nil { - exit("unable to play playlist on cast application: %v", err) + ccui, err := ui.NewUserInterface(app) + if err != nil { + exit("unable to prepare a new user-interface: %v", err) } - }, + if err := ccui.Run(); err != nil { + exit("unable to run ui: %v", err) + } + } + + if err := app.QueueLoad(filenames[indexToPlayFrom:], contentType, transcode); err != nil { + exit("unable to play playlist on cast application: %v", err) + } } func init() { diff --git a/cmd/previous.go b/cmd/previous.go index ce4f791f..93dae2f7 100644 --- a/cmd/previous.go +++ b/cmd/previous.go @@ -23,16 +23,22 @@ var previousCmd = &cobra.Command{ Use: "previous", Short: "Play the previous available media", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Previous(); err != nil { - exit("unable to play previous media: %v", err) - } + app := NewCast(cmd) + app.Previous() }, } +// Previous exports the previous command +func (a *App) Previous() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Previous(); err != nil { + exit("unable to play previous media: %v", err) + } +} + func init() { rootCmd.AddCommand(previousCmd) } diff --git a/cmd/restart.go b/cmd/restart.go index 3a43e84d..cae73622 100644 --- a/cmd/restart.go +++ b/cmd/restart.go @@ -23,16 +23,22 @@ var restartCmd = &cobra.Command{ Use: "restart", Short: "Restart the currently playing media", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.SeekFromStart(0); err != nil { - exit("unable to restart media: %v", err) - } + app := NewCast(cmd) + app.Restart() }, } +// Restart exports the restart command +func (a *App) Restart() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.SeekFromStart(0); err != nil { + exit("unable to restart media: %v", err) + } +} + func init() { rootCmd.AddCommand(restartCmd) } diff --git a/cmd/rewind.go b/cmd/rewind.go index 7c9ce8a9..ec2fb8db 100644 --- a/cmd/rewind.go +++ b/cmd/rewind.go @@ -25,23 +25,36 @@ var rewindCmd = &cobra.Command{ Use: "rewind ", Short: "Rewind by seconds the currently playing media", Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - exit("one argument required") - } - value, err := strconv.Atoi(args[0]) - if err != nil { - exit("unable to parse %q to an integer", args[0]) - } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Seek(-value); err != nil { - exit("unable to rewind current media: %v", err) - } + app := NewCast(cmd) + app.Rewind(args) }, } +// Rewind exports the rewind command +func (a *App) Rewind(args []string) { + if len(args) != 1 { + exit("one argument required") + } + value, err := strconv.Atoi(args[0]) + if err != nil { + exit("unable to parse %q to an integer", args[0]) + } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Seek(-value); err != nil { + exit("unable to rewind current media: %v", err) + } + + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Next(); err != nil { + exit("unable to play next media: %v", err) + } +} + func init() { rootCmd.AddCommand(rewindCmd) } diff --git a/cmd/root.go b/cmd/root.go index c78902ea..cd3cc6ac 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,8 +65,8 @@ func init() { rootCmd.PersistentFlags().Bool("version", false, "display command version") // TODO: clean up shortened "v" for debug and move to verbose, and ensure // verbose is used appropriately as debug. - rootCmd.PersistentFlags().BoolP("debug", "v", false, "debug logging") - rootCmd.PersistentFlags().Bool("verbose", false, "verbose logging") + // rootCmd.PersistentFlags().BoolP("debug", "v", false, "debug logging") + rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Debug/Verbose logging") rootCmd.PersistentFlags().Bool("disable-cache", false, "disable the cache") rootCmd.PersistentFlags().Bool("with-ui", false, "run with a UI") rootCmd.PersistentFlags().StringP("device", "d", "", "chromecast device, ie: 'Chromecast' or 'Google Home Mini'") diff --git a/cmd/scan.go b/cmd/scan.go index 160ce08d..fe0c6440 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -30,58 +30,66 @@ var scanCmd = &cobra.Command{ Use: "scan", Short: "Scan for chromecast devices", Run: func(cmd *cobra.Command, args []string) { - var ( - cidrAddr, _ = cmd.Flags().GetString("cidr") - port, _ = cmd.Flags().GetInt("port") - wg sync.WaitGroup - ipCh = make(chan *ipaddr.IPAddress) - logged = time.Unix(0, 0) - start = time.Now() - count int - ipRange, err = ipaddr.NewIPAddressString(cidrAddr).ToSequentialRange() - ) - if err != nil { - exit("could not parse cidr address expression: %v", err) + cidrAddr, _ := cmd.Flags().GetString("cidr") + port, _ := cmd.Flags().GetInt("port") + + app := NewCast(cmd) + app.Scan(cidrAddr, port) + }, +} + +// Scan exports the scan command +func (a *App) Scan(cidrAddr string, port int) { + var ( + wg sync.WaitGroup + count int + + ipCh = make(chan *ipaddr.IPAddress) + logged = time.Unix(0, 0) + start = time.Now() + ) + ipRange, err := ipaddr.NewIPAddressString(cidrAddr).ToSequentialRange() + if err != nil { + exit("could not parse cidr address expression: %v", err) + } + // Use one goroutine to send URIs over a channel + go func() { + it := ipRange.Iterator() + for it.HasNext() { + ip := it.Next() + if time.Since(logged) > 8*time.Second { + outputInfo("Scanning... scanned %d, current %v\n", count, ip.String()) + logged = time.Now() + } + ipCh <- ip + count++ } - // Use one goroutine to send URIs over a channel + close(ipCh) + }() + // Use a bunch of goroutines to do connect-attempts. + for range 64 { + wg.Add(1) go func() { - it := ipRange.Iterator() - for it.HasNext() { - ip := it.Next() - if time.Since(logged) > 8*time.Second { - outputInfo("Scanning... scanned %d, current %v\n", count, ip.String()) - logged = time.Now() - } - ipCh <- ip - count++ + defer wg.Done() + dialer := &net.Dialer{ + Timeout: 400 * time.Millisecond, } - close(ipCh) - }() - // Use a bunch of goroutines to do connect-attempts. - for i := 0; i < 64; i++ { - wg.Add(1) - go func() { - defer wg.Done() - dialer := &net.Dialer{ - Timeout: 400 * time.Millisecond, + for ip := range ipCh { + conn, err := dialer.Dial("tcp", fmt.Sprintf("%v:%d", ip, port)) + if err != nil { + continue } - for ip := range ipCh { - conn, err := dialer.Dial("tcp", fmt.Sprintf("%v:%d", ip, port)) - if err != nil { - continue - } - conn.Close() - if info, err := application.GetInfo(ip.String()); err != nil { - outputInfo(" - Device at %v:%d errored during discovery: %v", ip, port, err) - } else { - outputInfo(" - '%v' at %v:%d\n", info.Name, ip, port) - } + conn.Close() + if info, err := application.GetInfo(ip.String()); err != nil { + outputInfo(" - Device at %v:%d errored during discovery: %v", ip, port, err) + } else { + outputInfo(" - '%v' at %v:%d\n", info.Name, ip, port) } - }() - } - wg.Wait() - outputInfo("Scanned %d uris in %v\n", count, time.Since(start)) - }, + } + }() + } + wg.Wait() + outputInfo("Scanned %d uris in %v\n", count, time.Since(start)) } func init() { diff --git a/cmd/seek-to.go b/cmd/seek-to.go index b1eb3b27..7f7c7862 100644 --- a/cmd/seek-to.go +++ b/cmd/seek-to.go @@ -25,23 +25,29 @@ var seekToCmd = &cobra.Command{ Use: "seek-to ", Short: "Seek to the in the currently playing media", Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - exit("one argument required") - } - value, err := strconv.ParseFloat(args[0], 32) - if err != nil { - exit("unable to parse %q to an integer", args[0]) - } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.SeekToTime(float32(value)); err != nil { - exit("unable to seek to current media: %v", err) - } + app := NewCast(cmd) + app.SeekTo(args) }, } +// SeekTo exports the seekTo command +func (a *App) SeekTo(args []string) { + if len(args) != 1 { + exit("one argument required") + } + value, err := strconv.ParseFloat(args[0], 32) + if err != nil { + exit("unable to parse %q to an integer", args[0]) + } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.SeekToTime(float32(value)); err != nil { + exit("unable to seek to current media: %v", err) + } +} + func init() { rootCmd.AddCommand(seekToCmd) } diff --git a/cmd/seek.go b/cmd/seek.go index 0ef37403..e1c6d0c4 100644 --- a/cmd/seek.go +++ b/cmd/seek.go @@ -25,23 +25,29 @@ var seekCmd = &cobra.Command{ Use: "seek ", Short: "Seek by seconds into the currently playing media", Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - exit("one argument required") - } - value, err := strconv.Atoi(args[0]) - if err != nil { - exit("unable to parse %q to an integer", args[0]) - } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Seek(value); err != nil { - exit("unable to seek current media: %v", err) - } + app := NewCast(cmd) + app.Seek(args) }, } +// Seek exports the seek command +func (a *App) Seek(args []string) { + if len(args) != 1 { + exit("one argument required") + } + value, err := strconv.Atoi(args[0]) + if err != nil { + exit("unable to parse %q to an integer", args[0]) + } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Seek(value); err != nil { + exit("unable to seek current media: %v", err) + } +} + func init() { rootCmd.AddCommand(seekCmd) } diff --git a/cmd/skipad.go b/cmd/skipad.go index e3e2bc43..7d3caf61 100644 --- a/cmd/skipad.go +++ b/cmd/skipad.go @@ -4,23 +4,28 @@ import ( "github.com/spf13/cobra" ) -// skipadCmd represents the unpause command +// skipadCmd represents the skipad command var skipadCmd = &cobra.Command{ Use: "skipad", Short: "Skip the currently playing ad on the chromecast", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v\n", err) - return - } - if err := app.Skipad(); err != nil { - exit("unable to skip current ad: %v\n", err) - } - + app := NewCast(cmd) + app.Skipad() }, } +// Skipad exports the skipad command +func (a *App) Skipad() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v\n", err) + return + } + if err := app.Skipad(); err != nil { + exit("unable to skip current ad: %v\n", err) + } +} + func init() { rootCmd.AddCommand(skipadCmd) } diff --git a/cmd/slideshow.go b/cmd/slideshow.go index 4b1e8c7d..8488ceea 100644 --- a/cmd/slideshow.go +++ b/cmd/slideshow.go @@ -35,37 +35,43 @@ var slideshowCmd = &cobra.Command{ Use: "slideshow file1 file2 ...", Short: "Play a slideshow of photos", Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - exit("requires files (or directories) to play in slideshow") - } + duration, _ := cmd.Flags().GetInt("duration") + repeat, _ := cmd.Flags().GetBool("repeat") - s := &share{} + app := NewCast(cmd) + app.Slideshow(duration, repeat, args) + }, +} - for _, arg := range args { - if fileInfo, err := os.Stat(arg); err != nil { - log.Warn().Msgf("unable to find %q: %v", arg, err) - } else if fileInfo.Mode().IsDir() { - log.Debug().Msgf("%q is a directory", arg) +// Slideshow exports the slideshow command +func (a *App) Slideshow(duration int, repeat bool, args []string) { + if len(args) == 0 { + exit("requires files (or directories) to play in slideshow") + } - // recursively find files in directory - // TODO: this will consume large amounts of memory as it will hold references to each file (media item) to be served - filepath.WalkDir(arg, s.getFilesRecursively) - } else { - s.files = append(s.files, arg) - } - } + s := &share{} - app, err := castApplication(cmd, s.files) - if err != nil { - exit("unable to get cast application: %v", err) - } + for _, arg := range args { + if fileInfo, err := os.Stat(arg); err != nil { + log.Warn().Msgf("unable to find %q: %v", arg, err) + } else if fileInfo.Mode().IsDir() { + log.Debug().Msgf("%q is a directory", arg) - duration, _ := cmd.Flags().GetInt("duration") - repeat, _ := cmd.Flags().GetBool("repeat") - if err := app.Slideshow(s.files, duration, repeat); err != nil { - exit("unable to play slideshow on cast application: %v", err) + // recursively find files in directory + // TODO: this will consume large amounts of memory as it will hold references to each file (media item) to be served + filepath.WalkDir(arg, s.getFilesRecursively) + } else { + s.files = append(s.files, arg) } - }, + } + + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Slideshow(s.files, duration, repeat); err != nil { + exit("unable to play slideshow on cast application: %v", err) + } } func (s *share) getFilesRecursively(path string, d fs.DirEntry, err error) error { diff --git a/cmd/status.go b/cmd/status.go index 01bbd1c7..cb617293 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -25,31 +25,37 @@ var statusCmd = &cobra.Command{ Use: "status", Short: "Current chromecast status", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) + app := NewCast(cmd) + app.Status() + }, +} + +// Status exports the status command +func (a *App) Status() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + castApplication, castMedia, castVolume := app.Status() + if castApplication == nil { + outputInfo("Idle, volume=%0.2f muted=%t", castVolume.Level, castVolume.Muted) + } else if castApplication.IsIdleScreen { + outputInfo("Idle (%s), volume=%0.2f muted=%t", castApplication.DisplayName, castVolume.Level, castVolume.Muted) + } else if castMedia == nil { + outputInfo("Idle (%s), volume=%0.2f muted=%t", castApplication.DisplayName, castVolume.Level, castVolume.Muted) + } else { + metadata := "unknown" + var usefulID string + switch castMedia.Media.ContentType { + case "x-youtube/video": + usefulID = fmt.Sprintf("[%s] ", castMedia.Media.ContentId) } - castApplication, castMedia, castVolume := app.Status() - if castApplication == nil { - outputInfo("Idle, volume=%0.2f muted=%t", castVolume.Level, castVolume.Muted) - } else if castApplication.IsIdleScreen { - outputInfo("Idle (%s), volume=%0.2f muted=%t", castApplication.DisplayName, castVolume.Level, castVolume.Muted) - } else if castMedia == nil { - outputInfo("Idle (%s), volume=%0.2f muted=%t", castApplication.DisplayName, castVolume.Level, castVolume.Muted) - } else { - metadata := "unknown" - var usefulID string - switch castMedia.Media.ContentType { - case "x-youtube/video": - usefulID = fmt.Sprintf("[%s] ", castMedia.Media.ContentId) - } - if castMedia.Media.Metadata.Title != "" { - md := castMedia.Media.Metadata - metadata = fmt.Sprintf("title=%q, artist=%q", md.Title, md.Artist) - } - outputInfo("%s%s (%s), %s, time remaining=%.0fs/%.0fs, volume=%0.2f, muted=%t", usefulID, castApplication.DisplayName, castMedia.PlayerState, metadata, castMedia.CurrentTime, castMedia.Media.Duration, castVolume.Level, castVolume.Muted) + if castMedia.Media.Metadata.Title != "" { + md := castMedia.Media.Metadata + metadata = fmt.Sprintf("title=%q, artist=%q", md.Title, md.Artist) } - }, + outputInfo("%s%s (%s), %s, time remaining=%.0fs/%.0fs, volume=%0.2f, muted=%t", usefulID, castApplication.DisplayName, castMedia.PlayerState, metadata, castMedia.CurrentTime, castMedia.Media.Duration, castVolume.Level, castVolume.Muted) + } } func init() { diff --git a/cmd/stop.go b/cmd/stop.go index 1d348215..9694459f 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -23,16 +23,22 @@ var stopCmd = &cobra.Command{ Use: "stop", Short: "Stop casting", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Stop(); err != nil { - exit("unable to stop casting: %v", err) - } + app := NewCast(cmd) + app.Stop() }, } +// Stop exports the stop command +func (a *App) Stop() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Stop(); err != nil { + exit("unable to stop casting: %v", err) + } +} + func init() { rootCmd.AddCommand(stopCmd) } diff --git a/cmd/togglepause.go b/cmd/togglepause.go index 796df8ef..1e034b5a 100644 --- a/cmd/togglepause.go +++ b/cmd/togglepause.go @@ -24,16 +24,22 @@ var togglepauseCmd = &cobra.Command{ Aliases: []string{"tpause", "playpause"}, Short: "Toggle paused/unpaused state. Aliases: tpause, playpause", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.TogglePause(); err != nil { - exit("unable to (un)pause cast application: %v", err) - } + app := NewCast(cmd) + app.TogglePause() }, } +// TogglePause exports the togglepause command +func (a *App) TogglePause() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.TogglePause(); err != nil { + exit("unable to (un)pause cast application: %v", err) + } +} + func init() { rootCmd.AddCommand(togglepauseCmd) } diff --git a/cmd/transcode.go b/cmd/transcode.go index ff06da25..3f2fa443 100644 --- a/cmd/transcode.go +++ b/cmd/transcode.go @@ -30,45 +30,51 @@ locally and serve the output of the transcoding operation to the chromecast. This command requires the program or script to write the media content to stdout. The transcoded media content-type is required as well`, Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - contentType, _ := cmd.Flags().GetString("content-type") command, _ := cmd.Flags().GetString("command") + runWithUI, _ := cmd.Flags().GetBool("with-ui") - var commandArgs []string - if command == "" { - command = args[0] - commandArgs = args[1:] - } else { - s := strings.Split(command, " ") - command = s[0] - commandArgs = s[1:] - } + app := NewCast(cmd) + app.Transcode(contentType, command, runWithUI, args) + }, +} - runWithUI, _ := cmd.Flags().GetBool("with-ui") - if runWithUI { - go func() { - if err := app.Transcode(contentType, command, commandArgs...); err != nil { - exit("unable to load media: %v", err) - } - }() +// Transcode exports the transcode command +func (a *App) Transcode(contentType, command string, runWithUI bool, args []string) { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } - ccui, err := ui.NewUserInterface(app) - if err != nil { - exit("unable to prepare a new user-interface: %v", err) - } - if err := ccui.Run(); err != nil { - exit("unable to run ui: %v", err) + var commandArgs []string + if command == "" { + command = args[0] + commandArgs = args[1:] + } else { + s := strings.Split(command, " ") + command = s[0] + commandArgs = s[1:] + } + + if runWithUI { + go func() { + if err := app.Transcode(contentType, command, commandArgs...); err != nil { + exit("unable to load media: %v", err) } - } + }() - if err := app.Transcode(contentType, command, commandArgs...); err != nil { - exit("unable to transcode media: %v", err) + ccui, err := ui.NewUserInterface(app) + if err != nil { + exit("unable to prepare a new user-interface: %v", err) } - }, + if err := ccui.Run(); err != nil { + exit("unable to run ui: %v", err) + } + } + + if err := app.Transcode(contentType, command, commandArgs...); err != nil { + exit("unable to transcode media: %v", err) + } } func init() { diff --git a/cmd/tts.go b/cmd/tts.go index 66339293..de0e7360 100644 --- a/cmd/tts.go +++ b/cmd/tts.go @@ -15,7 +15,6 @@ package cmd import ( - "io/ioutil" "os" "github.com/spf13/cobra" @@ -27,56 +26,77 @@ var ttsCmd = &cobra.Command{ Use: "tts ", Short: "text-to-speech", Run: func(cmd *cobra.Command, args []string) { + GoogleServiceAccount, _ := cmd.Flags().GetString("google-service-account") + LanguageCode, _ := cmd.Flags().GetString("language-code") + VoiceName, _ := cmd.Flags().GetString("voice-name") + SpeakingRate, _ := cmd.Flags().GetFloat32("speaking-rate") + Pitch, _ := cmd.Flags().GetFloat32("pitch") + Ssml, _ := cmd.Flags().GetBool("ssml") - if len(args) != 1 || args[0] == "" { - exit("expected exactly one argument to convert to speech") - return - } + app := NewCast(cmd) + app.Tts(TTSOpts{ + GoogleServiceAccount, + LanguageCode, + VoiceName, + SpeakingRate, + Pitch, + Ssml, + }, args) + }, +} - googleServiceAccount, _ := cmd.Flags().GetString("google-service-account") - if googleServiceAccount == "" { - exit("--google-service-account is required") - return - } +type TTSOpts struct { + GoogleServiceAccount string + LanguageCode string + VoiceName string + SpeakingRate float32 + Pitch float32 + Ssml bool +} - languageCode, _ := cmd.Flags().GetString("language-code") - voiceName, _ := cmd.Flags().GetString("voice-name") - speakingRate, _ := cmd.Flags().GetFloat32("speaking-rate") - pitch, _ := cmd.Flags().GetFloat32("pitch") - ssml, _ := cmd.Flags().GetBool("ssml") +// Tts exports the tts command +func (a *App) Tts(opts TTSOpts, args []string) { + if len(args) != 1 || args[0] == "" { + exit("expected exactly one argument to convert to speech") + return + } - b, err := ioutil.ReadFile(googleServiceAccount) - if err != nil { - exit("unable to open google service account file: %v", err) - } + if opts.GoogleServiceAccount == "" { + exit("--google-service-account is required") + return + } - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } + b, err := os.ReadFile(opts.GoogleServiceAccount) + if err != nil { + exit("unable to open google service account file: %v", err) + } - data, err := tts.Create(args[0], b, languageCode, voiceName, speakingRate, pitch, ssml) - if err != nil { - exit("unable to create tts: %v", err) - } + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } - f, err := ioutil.TempFile("", "go-chromecast-tts") - if err != nil { - exit("unable to create temp file: %v", err) - } - defer os.Remove(f.Name()) + data, err := tts.Create(args[0], b, opts.LanguageCode, opts.VoiceName, opts.SpeakingRate, opts.Pitch, opts.Ssml) + if err != nil { + exit("unable to create tts: %v", err) + } - if _, err := f.Write(data); err != nil { - exit("unable to write to temp file: %v", err) - } - if err := f.Close(); err != nil { - exit("unable to close temp file: %v", err) - } + f, err := os.CreateTemp("", "go-chromecast-tts") + if err != nil { + exit("unable to create temp file: %v", err) + } + defer os.Remove(f.Name()) - if err := app.Load(f.Name(), 0, "audio/mp3", false, false, false); err != nil { - exit("unable to load media to device: %v", err) - } - }, + if _, err := f.Write(data); err != nil { + exit("unable to write to temp file: %v", err) + } + if err := f.Close(); err != nil { + exit("unable to close temp file: %v", err) + } + + if err := app.Load(f.Name(), 0, "audio/mp3", false, false, false); err != nil { + exit("unable to load media to device: %v", err) + } } func init() { diff --git a/cmd/ui.go b/cmd/ui.go index 3f658016..8137dfbc 100644 --- a/cmd/ui.go +++ b/cmd/ui.go @@ -25,23 +25,29 @@ var uiCmd = &cobra.Command{ Use: "ui", Short: "Run the UI", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - return - } - - ccui, err := ui.NewUserInterface(app) - if err != nil { - exit("unable to prepare a new user-interface: %v", err) - } - - if err := ccui.Run(); err != nil { - exit("unable to start the user-interface: %v", err) - } + app := NewCast(cmd) + app.Ui() }, } +// Ui exports the ui command +func (a *App) Ui() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + return + } + + ccui, err := ui.NewUserInterface(app) + if err != nil { + exit("unable to prepare a new user-interface: %v", err) + } + + if err := ccui.Run(); err != nil { + exit("unable to start the user-interface: %v", err) + } +} + func init() { rootCmd.AddCommand(uiCmd) } diff --git a/cmd/unmute.go b/cmd/unmute.go index 4d691a15..5064ef67 100644 --- a/cmd/unmute.go +++ b/cmd/unmute.go @@ -23,16 +23,22 @@ var unmuteCmd = &cobra.Command{ Use: "unmute", Short: "Unmute the chromecast", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.SetMuted(false); err != nil { - exit("unable to unmute cast application: %v", err) - } + app := NewCast(cmd) + app.UnMute() }, } +// UnMute exports the unmute command +func (a *App) UnMute() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.SetMuted(false); err != nil { + exit("unable to mute cast application: %v", err) + } +} + func init() { rootCmd.AddCommand(unmuteCmd) } diff --git a/cmd/unpause.go b/cmd/unpause.go index 19eb6020..0e9d1e85 100644 --- a/cmd/unpause.go +++ b/cmd/unpause.go @@ -23,16 +23,22 @@ var unpauseCmd = &cobra.Command{ Use: "unpause", Short: "Unpause the currently playing media on the chromecast", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - if err := app.Unpause(); err != nil { - exit("unable to pause cast application: %v", err) - } + app := NewCast(cmd) + app.UnPause() }, } +// UnPause exports the unpause command +func (a *App) UnPause() { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + if err := app.Unpause(); err != nil { + exit("unable to pause cast application: %v", err) + } +} + func init() { rootCmd.AddCommand(unpauseCmd) } diff --git a/cmd/utils.go b/cmd/utils.go index d835f125..8bb398d9 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -50,11 +50,25 @@ func (e CachedDNSEntry) GetPort() int { return e.Port } -func castApplication(cmd *cobra.Command, args []string) (application.App, error) { +type App struct { + DeviceName string + Device string + Uuid string + Debug bool + DisableCache bool + Addr string + Port string + Iface string + ServerPort int + DnsTimeout int + First bool +} + +func NewCast(cmd *cobra.Command) App { deviceName, _ := cmd.Flags().GetString("device-name") deviceUuid, _ := cmd.Flags().GetString("uuid") device, _ := cmd.Flags().GetString("device") - debug, _ := cmd.Flags().GetBool("debug") + debug, _ := cmd.Flags().GetBool("verbose") disableCache, _ := cmd.Flags().GetBool("disable-cache") addr, _ := cmd.Flags().GetString("addr") port, _ := cmd.Flags().GetString("port") @@ -63,50 +77,66 @@ func castApplication(cmd *cobra.Command, args []string) (application.App, error) dnsTimeoutSeconds, _ := cmd.Flags().GetInt("dns-timeout") useFirstDevice, _ := cmd.Flags().GetBool("first") + return App{ + DeviceName: deviceName, + Device: device, + Uuid: deviceUuid, + Debug: debug, + DisableCache: disableCache, + Addr: addr, + Port: port, + Iface: ifaceName, + ServerPort: serverPort, + DnsTimeout: dnsTimeoutSeconds, + First: useFirstDevice, + } +} + +func (app *App) castApplication() (application.App, error) { // Used to try and reconnect - if deviceUuid == "" && entry != nil { - deviceUuid = entry.GetUUID() + if app.Uuid == "" && entry != nil { + app.Uuid = entry.GetUUID() entry = nil } - if debug { + if app.Debug { log.SetLevel(log.DebugLevel) } applicationOptions := []application.ApplicationOption{ - application.WithServerPort(serverPort), - application.WithDebug(debug), - application.WithCacheDisabled(disableCache), + application.WithServerPort(app.ServerPort), + application.WithDebug(app.Debug), + application.WithCacheDisabled(app.DisableCache), } // If we need to look on a specific network interface for mdns or // for finding a network ip to host from, ensure that the network // interface exists. var iface *net.Interface - if ifaceName != "" { + if app.Iface != "" { var err error - if iface, err = net.InterfaceByName(ifaceName); err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("unable to find interface %q", ifaceName)) + if iface, err = net.InterfaceByName(app.Iface); err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unable to find interface %q", app.Iface)) } applicationOptions = append(applicationOptions, application.WithIface(iface)) } // If no address was specified, attempt to determine the address of any // local chromecast devices. - if addr == "" { + if app.Addr == "" { // If a device name or uuid was specified, check the cache for the ip+port found := false - if !disableCache && (deviceName != "" || deviceUuid != "") { - entry = findCachedCastDNS(deviceName, deviceUuid) + if !app.DisableCache && (app.DeviceName != "" || app.Uuid != "") { + entry = findCachedCastDNS(app.DeviceName, app.Uuid) found = entry.GetAddr() != "" } if !found { var err error - if entry, err = findCastDNS(iface, dnsTimeoutSeconds, device, deviceName, deviceUuid, useFirstDevice); err != nil { + if entry, err = findCastDNS(iface, app.DnsTimeout, app.Device, app.DeviceName, app.Uuid, app.First); err != nil { return nil, errors.Wrap(err, "unable to find cast dns entry") } } - if !disableCache { + if !app.DisableCache { cachedEntry := CachedDNSEntry{ UUID: entry.GetUUID(), Name: entry.GetName(), @@ -121,21 +151,21 @@ func castApplication(cmd *cobra.Command, args []string) (application.App, error) outputError("Failed to save name cache entry\n") } } - if debug { + if app.Debug { outputInfo("using device name=%s addr=%s port=%d uuid=%s", entry.GetName(), entry.GetAddr(), entry.GetPort(), entry.GetUUID()) } } else { - p, err := strconv.Atoi(port) + p, err := strconv.Atoi(app.Port) if err != nil { return nil, errors.Wrap(err, "port needs to be a number") } entry = CachedDNSEntry{ - Addr: addr, + Addr: app.Addr, Port: p, } } - app := application.NewApplication(applicationOptions...) - if err := app.Start(entry.GetAddr(), entry.GetPort()); err != nil { + appp := application.NewApplication(applicationOptions...) + if err := appp.Start(entry.GetAddr(), entry.GetPort()); err != nil { // NOTE: currently we delete the dns cache every time we get // an error, this is to make sure that if the device gets a new // ipaddress we will invalidate the cache. @@ -147,17 +177,7 @@ func castApplication(cmd *cobra.Command, args []string) (application.App, error) } return nil, err } - 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.App, error) { - return castApplication(cmd, args) + return appp, nil } func getCacheKey(suffix string) string { @@ -226,15 +246,15 @@ func findCastDNS(iface *net.Interface, dnsTimeoutSeconds int, device, deviceName } } -func outputError(msg string, args ...interface{}) { +func outputError(msg string, args ...any) { output(output_Error, msg, args...) } -func outputInfo(msg string, args ...interface{}) { +func outputInfo(msg string, args ...any) { output(output_Info, msg, args...) } -func exit(msg string, args ...interface{}) { +func exit(msg string, args ...any) { outputError(msg, args...) os.Exit(1) } @@ -246,7 +266,7 @@ const ( output_Error ) -func output(t outputLevel, msg string, args ...interface{}) { +func output(t outputLevel, msg string, args ...any) { switch t { case output_Error: fmt.Printf("%serror%s: ", RED, NC) diff --git a/cmd/volume-down.go b/cmd/volume-down.go index 25c2f6b5..e45becf5 100644 --- a/cmd/volume-down.go +++ b/cmd/volume-down.go @@ -25,32 +25,38 @@ var volumeDownCmd = &cobra.Command{ Use: "volume-down", Short: "Turn down volume", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - volumeStep, _ := cmd.Flags().GetFloat32("step") - if err = app.Update(); err != nil { - exit("unable to update cast info: %v", err) - } - _, _, castVolume := app.Status() - - nextVolume := max(castVolume.Level-volumeStep, math.SmallestNonzeroFloat32) - if err = app.SetVolume(float32(nextVolume)); err != nil { - exit("failed to set volume: %v", err) - } - - if err = app.Update(); err != nil { - exit("unable to update cast info: %v", err) - } - _, _, turnedCastVolume := app.Status() - - outputInfo("%0.2f", turnedCastVolume.Level) + app := NewCast(cmd) + app.VolumeDown(volumeStep) }, } +// VolumeDown exports the volume-down command +func (a *App) VolumeDown(volumeStep float32) { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + + if err = app.Update(); err != nil { + exit("unable to update cast info: %v", err) + } + _, _, castVolume := app.Status() + + nextVolume := max(castVolume.Level-volumeStep, math.SmallestNonzeroFloat32) + if err = app.SetVolume(float32(nextVolume)); err != nil { + exit("failed to set volume: %v", err) + } + + if err = app.Update(); err != nil { + exit("unable to update cast info: %v", err) + } + _, _, turnedCastVolume := app.Status() + + outputInfo("%0.2f", turnedCastVolume.Level) +} + func init() { rootCmd.AddCommand(volumeDownCmd) volumeDownCmd.Flags().Float32("step", 0.05, "step value for turning down volume") diff --git a/cmd/volume-up.go b/cmd/volume-up.go index f0d25841..6bf32385 100644 --- a/cmd/volume-up.go +++ b/cmd/volume-up.go @@ -23,32 +23,38 @@ var volumeUpCmd = &cobra.Command{ Use: "volume-up", Short: "Turn up volume", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) - if err != nil { - exit("unable to get cast application: %v", err) - } - volumeStep, _ := cmd.Flags().GetFloat32("step") - if err = app.Update(); err != nil { - exit("unable to update cast info: %v", err) - } - _, _, castVolume := app.Status() - - nextVolume := min(castVolume.Level+volumeStep, 1) - if err = app.SetVolume(float32(nextVolume)); err != nil { - exit("failed to set volume: %v", err) - } - - if err = app.Update(); err != nil { - exit("unable to update cast info: %v", err) - } - _, _, turnedCastVolume := app.Status() - - outputInfo("%0.2f", turnedCastVolume.Level) + app := NewCast(cmd) + app.VolumeUp(volumeStep) }, } +// VolumeUp exports the volume-up command +func (a *App) VolumeUp(volumeStep float32) { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + + if err = app.Update(); err != nil { + exit("unable to update cast info: %v", err) + } + _, _, castVolume := app.Status() + + nextVolume := min(castVolume.Level+volumeStep, 1) + if err = app.SetVolume(float32(nextVolume)); err != nil { + exit("failed to set volume: %v", err) + } + + if err = app.Update(); err != nil { + exit("unable to update cast info: %v", err) + } + _, _, turnedCastVolume := app.Status() + + outputInfo("%0.2f", turnedCastVolume.Level) +} + func init() { rootCmd.AddCommand(volumeUpCmd) volumeUpCmd.Flags().Float32("step", 0.05, "step value for turning up volume") diff --git a/cmd/volume.go b/cmd/volume.go index 6ce49a9c..2d7a3418 100644 --- a/cmd/volume.go +++ b/cmd/volume.go @@ -26,28 +26,34 @@ var volumeCmd = &cobra.Command{ Short: "Get or set volume", Long: "Get or set volume (float in range from 0 to 1)", Run: func(cmd *cobra.Command, args []string) { - app, err := castApplication(cmd, args) + app := NewCast(cmd) + app.Volume(args) + }, +} + +// Volume exports the volume command +func (a *App) Volume(args []string) { + app, err := a.castApplication() + if err != nil { + exit("unable to get cast application: %v", err) + } + + if len(args) == 1 && args[0] != "" { + newVolume, err := strconv.ParseFloat(args[0], 32) if err != nil { - exit("unable to get cast application: %v", err) + exit("invalid volume: %v", err) } - - if len(args) == 1 && args[0] != "" { - newVolume, err := strconv.ParseFloat(args[0], 32) - if err != nil { - exit("invalid volume: %v", err) - } - if err = app.SetVolume(float32(newVolume)); err != nil { - exit("failed to set volume: %v", err) - } + if err = app.SetVolume(float32(newVolume)); err != nil { + exit("failed to set volume: %v", err) } + } - if err = app.Update(); err != nil { - exit("unable to update cast info: %v", err) - } - _, _, castVolume := app.Status() + if err = app.Update(); err != nil { + exit("unable to update cast info: %v", err) + } + _, _, castVolume := app.Status() - outputInfo("%0.2f", castVolume.Level) - }, + outputInfo("%0.2f", castVolume.Level) } func init() { diff --git a/cmd/watch.go b/cmd/watch.go index 89c5559a..501d5951 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -37,70 +37,76 @@ var watchCmd = &cobra.Command{ retries, _ := cmd.Flags().GetInt("retries") output, _ := cmd.Flags().GetString("output") - o := outputNormal - if strings.ToLower(output) == "json" { - o = outputJSON - } + app := NewCast(cmd) + app.Watch(interval, retries, output) + }, +} - for i := 0; i < retries; i++ { - retry := false - app, err := castApplication(cmd, args) - if err != nil { - outputError("unable to get cast application: %v", err) - time.Sleep(time.Second * 10) - continue - } - done := make(chan struct{}, 1) - go func() { - for { - if err := app.Update(); err != nil { - outputError("unable to update cast application: %v", 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 { +// Watch exports the watch command +func (a *App) Watch(interval, retries int, output string) { + o := outputNormal + if strings.ToLower(output) == "json" { + o = outputJSON + } + + for range retries { + retry := false + app, err := a.castApplication() + if err != nil { + outputError("unable to get cast application: %v", err) + time.Sleep(time.Second * 10) + continue + } + done := make(chan struct{}, 1) + go func() { + for { + if err := app.Update(); err != nil { + outputError("unable to update cast application: %v", err) + retry = true + close(done) return } + outputStatus(app, o) + time.Sleep(time.Second * time.Duration(interval)) + } + }() - 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: - outputInfo("CHROMECAST BROADCAST MESSAGE: type=%s proto=%s (namespace=%s) %s -> %s | %s", messageType, protocolVersion, namespace, sourceID, destID, payload) - } - }) - <-done - if retry { - // Sleep a little bit in-between retries - outputInfo("attempting a retry...") - time.Sleep(time.Second * 10) + 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 + } + + switch o { + case outputJSON: + json.NewEncoder(os.Stdout).Encode(map[string]any{ + "type": messageType, + "proto_version": protocolVersion, + "namespace": namespace, + "source_id": sourceID, + "destination_id": destID, + "payload": payload, + }) + case outputNormal: + outputInfo("CHROMECAST BROADCAST MESSAGE: type=%s proto=%s (namespace=%s) %s -> %s | %s", messageType, protocolVersion, namespace, sourceID, destID, payload) } + }) + <-done + if retry { + // Sleep a little bit in-between retries + outputInfo("attempting a retry...") + time.Sleep(time.Second * 10) } - }, + } } type outputType int @@ -115,7 +121,7 @@ func outputStatus(app application.App, outputType outputType) { switch outputType { case outputJSON: - json.NewEncoder(os.Stdout).Encode(map[string]interface{}{ + json.NewEncoder(os.Stdout).Encode(map[string]any{ "application": castApplication, "media": castMedia, "volume": castVolume, diff --git a/go.mod b/go.mod index c128cda2..940fcb27 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect google.golang.org/api v0.209.0 - google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 ) require github.com/seancfoley/ipaddress-go v1.7.0 diff --git a/go.sum b/go.sum index 671cd7d5..c2bc1877 100644 --- a/go.sum +++ b/go.sum @@ -225,8 +225,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= diff --git a/main.go b/main.go index 5a9bfce8..21f34c9f 100644 --- a/main.go +++ b/main.go @@ -28,9 +28,9 @@ var ( ) func main() { - os.Exit(main1()) + os.Exit(exec()) } -func main1() int { +func exec() int { return cmd.Execute(version, commit, date) } diff --git a/main_test.go b/main_test.go index 22e38879..457abc60 100644 --- a/main_test.go +++ b/main_test.go @@ -9,7 +9,7 @@ import ( func TestMain(m *testing.M) { os.Exit(testscript.RunMain(m, map[string]func() int{ - "go-chromecast": main1, + "go-chromecast": exec, })) } diff --git a/regenerate_mocks.sh b/regenerate_mocks.sh index cedf5094..660c4d2d 100755 --- a/regenerate_mocks.sh +++ b/regenerate_mocks.sh @@ -1,8 +1,7 @@ -#!/bin/bash -set -e -x +#!/bin/bash -ex cd ./cast -go run github.com/vektra/mockery/v2@v2.49.1 --all +go run github.com/vektra/mockery/v2@v2.53.3 cd ../application -go run github.com/vektra/mockery/v2@v2.49.1 --all \ No newline at end of file +go run github.com/vektra/mockery/v2@v2.53.3 diff --git a/storage/storage.go b/storage/storage.go index a34dc0e5..600c469e 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -2,7 +2,6 @@ package storage import ( "encoding/json" - "io/ioutil" "os" homedir "github.com/mitchellh/go-homedir" @@ -38,7 +37,7 @@ func (s *Storage) lazyLoadCacheDir() error { // Check if file exists, if so then load it if _, err := os.Stat(filename); err == nil { s.cacheFilename = filename - if fileContents, err := ioutil.ReadFile(filename); err == nil { + if fileContents, err := os.ReadFile(filename); err == nil { if err := json.Unmarshal(fileContents, &s.cache); err == nil { return nil } @@ -63,7 +62,7 @@ func (s *Storage) Save(key string, data []byte) error { s.cache[key] = data cacheJson, _ := json.Marshal(s.cache) - ioutil.WriteFile(s.cacheFilename, cacheJson, 0644) + os.WriteFile(s.cacheFilename, cacheJson, 0644) return nil } diff --git a/tts/tts.go b/tts/tts.go index fc91b0e3..96f48a6e 100644 --- a/tts/tts.go +++ b/tts/tts.go @@ -7,8 +7,8 @@ import ( "github.com/pkg/errors" texttospeech "cloud.google.com/go/texttospeech/apiv1" + texttospeechpb "cloud.google.com/go/texttospeech/apiv1/texttospeechpb" "google.golang.org/api/option" - texttospeechpb "google.golang.org/genproto/googleapis/cloud/texttospeech/v1" ) const ( @@ -40,8 +40,8 @@ func Create(sentence string, serviceAccountKey []byte, languageCode string, voic }, AudioConfig: &texttospeechpb.AudioConfig{ AudioEncoding: texttospeechpb.AudioEncoding_MP3, - SpeakingRate: float64(speakingRate), - Pitch: float64(pitch), + SpeakingRate: float64(speakingRate), + Pitch: float64(pitch), }, }