diff --git a/application/application.go b/application/application.go index e859cae..8013c13 100644 --- a/application/application.go +++ b/application/application.go @@ -20,7 +20,6 @@ import ( "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" @@ -66,6 +65,7 @@ type App interface { Close(stopMedia bool) error LoadApp(appID, contentID string) error Status() (*cast.Application, *cast.Media, *cast.Volume) + Info() (*cast.DeviceInfo, error) Update() error Pause() error Unpause() error @@ -93,6 +93,9 @@ type Application struct { conn cast.Conn debug bool + // Device name override (originating e.g. from mdns lookup). + deviceNameOverride string + // Internal mapping of request id to result channel resultChanMap map[int]chan *pb.CastMessage @@ -107,6 +110,7 @@ type Application struct { // Current values from the chromecast. application *cast.Application // It is possible that there is no current application, can happen for google home. media *cast.Media + info *cast.DeviceInfo // There seems to be two different volumes returned from the chromecast, // one for the receiver and one for the playing media. It looks we update // the receiver volume from go-chromecast, so we should use that one. But @@ -187,6 +191,12 @@ func WithSkipadRetries(retries int) ApplicationOption { } } +func WithDeviceNameOverride(deviceName string) ApplicationOption { + return func(a *Application) { + a.SetDeviceNameOverride(deviceName) + } +} + func NewApplication(opts ...ApplicationOption) *Application { a := &Application{ conn: cast.NewConnection(), @@ -223,6 +233,10 @@ func (a *Application) SetIface(iface *net.Interface) { a.iface = iface } func (a *Application) SetSkipadSleep(sleep time.Duration) { a.skipadSleep = sleep } func (a *Application) SetSkipadRetries(retries int) { a.skipadRetries = retries } +func (a *Application) SetDeviceNameOverride(deviceName string) { + a.deviceNameOverride = deviceName +} + func (a *Application) App() *cast.Application { return a.application } func (a *Application) Media() *cast.Media { return a.media } func (a *Application) Volume() *cast.Volume { return a.volumeReceiver } @@ -437,6 +451,25 @@ func (a *Application) Status() (*cast.Application, *cast.Media, *cast.Volume) { return a.application, a.media, a.volumeReceiver } +func (a *Application) Info() (*cast.DeviceInfo, error) { + if a.info != nil { + return a.info, nil + } + addr, err := a.conn.RemoteAddr() + if err != nil { + return nil, err + } + info, err := GetInfo(addr) + if err != nil { + return nil, err + } + if len(a.deviceNameOverride) > 0 { + info.Name = a.deviceNameOverride + } + a.info = info + return info, err +} + func (a *Application) Pause() error { if a.media == nil { return ErrNoMediaPause diff --git a/application/info.go b/application/info.go new file mode 100644 index 0000000..0acf3ea --- /dev/null +++ b/application/info.go @@ -0,0 +1,35 @@ +package application + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/vishen/go-chromecast/cast" +) + +// 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. + url := fmt.Sprintf("http://%v:8008/setup/eureka_info", ip) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + info = new(cast.DeviceInfo) + if err := json.Unmarshal(data, info); err != nil { + return nil, err + } + return info, nil +} diff --git a/application/mocks/App.go b/application/mocks/App.go index 8426eaa..9649ebe 100644 --- a/application/mocks/App.go +++ b/application/mocks/App.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.49.1. DO NOT EDIT. package mocks @@ -25,6 +25,10 @@ func (_m *App) AddMessageFunc(f application.CastMessageFunc) { func (_m *App) Close(stopMedia bool) error { ret := _m.Called(stopMedia) + if len(ret) == 0 { + panic("no return value specified for Close") + } + var r0 error if rf, ok := ret.Get(0).(func(bool) error); ok { r0 = rf(stopMedia) @@ -35,10 +39,44 @@ func (_m *App) Close(stopMedia bool) error { return r0 } -// Load provides a mock function with given fields: filenameOrUrl, contentType, transcode, detach, forceDetach +// Info provides a mock function with given fields: +func (_m *App) Info() (*cast.DeviceInfo, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Info") + } + + var r0 *cast.DeviceInfo + var r1 error + if rf, ok := ret.Get(0).(func() (*cast.DeviceInfo, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *cast.DeviceInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*cast.DeviceInfo) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Load provides a mock function with given fields: filenameOrUrl, startTime, contentType, transcode, detach, forceDetach func (_m *App) Load(filenameOrUrl string, startTime int, contentType string, transcode bool, detach bool, forceDetach bool) error { ret := _m.Called(filenameOrUrl, startTime, contentType, transcode, detach, forceDetach) + if len(ret) == 0 { + panic("no return value specified for Load") + } + var r0 error if rf, ok := ret.Get(0).(func(string, int, string, bool, bool, bool) error); ok { r0 = rf(filenameOrUrl, startTime, contentType, transcode, detach, forceDetach) @@ -53,6 +91,10 @@ func (_m *App) Load(filenameOrUrl string, startTime int, contentType string, tra func (_m *App) LoadApp(appID string, contentID string) error { ret := _m.Called(appID, contentID) + if len(ret) == 0 { + panic("no return value specified for LoadApp") + } + var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(appID, contentID) @@ -67,6 +109,10 @@ func (_m *App) LoadApp(appID string, contentID string) error { func (_m *App) Next() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Next") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -81,6 +127,10 @@ func (_m *App) Next() error { func (_m *App) Pause() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Pause") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -95,6 +145,10 @@ func (_m *App) Pause() error { func (_m *App) PlayableMediaType(filename string) bool { ret := _m.Called(filename) + if len(ret) == 0 { + panic("no return value specified for PlayableMediaType") + } + var r0 bool if rf, ok := ret.Get(0).(func(string) bool); ok { r0 = rf(filename) @@ -109,6 +163,10 @@ func (_m *App) PlayableMediaType(filename string) bool { func (_m *App) PlayedItems() map[string]application.PlayedItem { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for PlayedItems") + } + var r0 map[string]application.PlayedItem if rf, ok := ret.Get(0).(func() map[string]application.PlayedItem); ok { r0 = rf() @@ -125,6 +183,10 @@ func (_m *App) PlayedItems() map[string]application.PlayedItem { func (_m *App) Previous() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Previous") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -139,6 +201,10 @@ func (_m *App) Previous() error { func (_m *App) QueueLoad(filenames []string, contentType string, transcode bool) error { ret := _m.Called(filenames, contentType, transcode) + if len(ret) == 0 { + panic("no return value specified for QueueLoad") + } + var r0 error if rf, ok := ret.Get(0).(func([]string, string, bool) error); ok { r0 = rf(filenames, contentType, transcode) @@ -153,6 +219,10 @@ func (_m *App) QueueLoad(filenames []string, contentType string, transcode bool) func (_m *App) Seek(value int) error { ret := _m.Called(value) + if len(ret) == 0 { + panic("no return value specified for Seek") + } + var r0 error if rf, ok := ret.Get(0).(func(int) error); ok { r0 = rf(value) @@ -167,6 +237,10 @@ func (_m *App) Seek(value int) error { func (_m *App) SeekFromStart(value int) error { ret := _m.Called(value) + if len(ret) == 0 { + panic("no return value specified for SeekFromStart") + } + var r0 error if rf, ok := ret.Get(0).(func(int) error); ok { r0 = rf(value) @@ -181,6 +255,10 @@ func (_m *App) SeekFromStart(value int) error { func (_m *App) SeekToTime(value float32) error { ret := _m.Called(value) + if len(ret) == 0 { + panic("no return value specified for SeekToTime") + } + var r0 error if rf, ok := ret.Get(0).(func(float32) error); ok { r0 = rf(value) @@ -220,6 +298,10 @@ func (_m *App) SetIface(_a0 *net.Interface) { func (_m *App) SetMuted(value bool) error { ret := _m.Called(value) + if len(ret) == 0 { + panic("no return value specified for SetMuted") + } + var r0 error if rf, ok := ret.Get(0).(func(bool) error); ok { r0 = rf(value) @@ -239,6 +321,10 @@ func (_m *App) SetServerPort(_a0 int) { func (_m *App) SetVolume(value float32) error { ret := _m.Called(value) + if len(ret) == 0 { + panic("no return value specified for SetVolume") + } + var r0 error if rf, ok := ret.Get(0).(func(float32) error); ok { r0 = rf(value) @@ -253,6 +339,10 @@ func (_m *App) SetVolume(value float32) error { func (_m *App) Skipad() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Skipad") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -267,6 +357,10 @@ func (_m *App) Skipad() error { func (_m *App) Slideshow(filenames []string, duration int, repeat bool) error { ret := _m.Called(filenames, duration, repeat) + if len(ret) == 0 { + panic("no return value specified for Slideshow") + } + var r0 error if rf, ok := ret.Get(0).(func([]string, int, bool) error); ok { r0 = rf(filenames, duration, repeat) @@ -281,6 +375,10 @@ func (_m *App) Slideshow(filenames []string, duration int, repeat bool) error { func (_m *App) Start(addr string, port int) error { ret := _m.Called(addr, port) + if len(ret) == 0 { + panic("no return value specified for Start") + } + var r0 error if rf, ok := ret.Get(0).(func(string, int) error); ok { r0 = rf(addr, port) @@ -295,7 +393,16 @@ func (_m *App) Start(addr string, port int) error { func (_m *App) Status() (*cast.Application, *cast.Media, *cast.Volume) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Status") + } + var r0 *cast.Application + var r1 *cast.Media + var r2 *cast.Volume + if rf, ok := ret.Get(0).(func() (*cast.Application, *cast.Media, *cast.Volume)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() *cast.Application); ok { r0 = rf() } else { @@ -304,7 +411,6 @@ func (_m *App) Status() (*cast.Application, *cast.Media, *cast.Volume) { } } - var r1 *cast.Media if rf, ok := ret.Get(1).(func() *cast.Media); ok { r1 = rf() } else { @@ -313,7 +419,6 @@ func (_m *App) Status() (*cast.Application, *cast.Media, *cast.Volume) { } } - var r2 *cast.Volume if rf, ok := ret.Get(2).(func() *cast.Volume); ok { r2 = rf() } else { @@ -329,6 +434,10 @@ func (_m *App) Status() (*cast.Application, *cast.Media, *cast.Volume) { func (_m *App) Stop() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Stop") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -343,6 +452,28 @@ func (_m *App) Stop() error { func (_m *App) StopMedia() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for StopMedia") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TogglePause provides a mock function with given fields: +func (_m *App) TogglePause() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for TogglePause") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -364,6 +495,10 @@ func (_m *App) Transcode(contentType string, command string, args ...string) err _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for Transcode") + } + var r0 error if rf, ok := ret.Get(0).(func(string, string, ...string) error); ok { r0 = rf(contentType, command, args...) @@ -378,6 +513,10 @@ func (_m *App) Transcode(contentType string, command string, args ...string) err func (_m *App) Unpause() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Unpause") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -392,6 +531,10 @@ func (_m *App) Unpause() error { func (_m *App) Update() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Update") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -402,13 +545,12 @@ func (_m *App) Update() error { return r0 } -type mockConstructorTestingTNewApp interface { +// NewApp creates a new instance of App. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewApp(t interface { mock.TestingT Cleanup(func()) -} - -// NewApp creates a new instance of App. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewApp(t mockConstructorTestingTNewApp) *App { +}) *App { mock := &App{} mock.Mock.Test(t) diff --git a/application/mocks/ApplicationOption.go b/application/mocks/ApplicationOption.go index e380a3f..22d79ce 100644 --- a/application/mocks/ApplicationOption.go +++ b/application/mocks/ApplicationOption.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.49.1. DO NOT EDIT. package mocks @@ -17,13 +17,12 @@ func (_m *ApplicationOption) Execute(_a0 *application.Application) { _m.Called(_a0) } -type mockConstructorTestingTNewApplicationOption interface { +// NewApplicationOption creates a new instance of ApplicationOption. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewApplicationOption(t interface { mock.TestingT Cleanup(func()) -} - -// NewApplicationOption creates a new instance of ApplicationOption. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewApplicationOption(t mockConstructorTestingTNewApplicationOption) *ApplicationOption { +}) *ApplicationOption { mock := &ApplicationOption{} mock.Mock.Test(t) diff --git a/application/mocks/CastMessageFunc.go b/application/mocks/CastMessageFunc.go index 6d97a47..036569c 100644 --- a/application/mocks/CastMessageFunc.go +++ b/application/mocks/CastMessageFunc.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.49.1. DO NOT EDIT. package mocks @@ -18,13 +18,12 @@ func (_m *CastMessageFunc) Execute(_a0 *api.CastMessage) { _m.Called(_a0) } -type mockConstructorTestingTNewCastMessageFunc interface { +// NewCastMessageFunc creates a new instance of CastMessageFunc. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCastMessageFunc(t interface { mock.TestingT Cleanup(func()) -} - -// NewCastMessageFunc creates a new instance of CastMessageFunc. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewCastMessageFunc(t mockConstructorTestingTNewCastMessageFunc) *CastMessageFunc { +}) *CastMessageFunc { mock := &CastMessageFunc{} mock.Mock.Test(t) diff --git a/cast/connection.go b/cast/connection.go index 258b871..5e044d8 100644 --- a/cast/connection.go +++ b/cast/connection.go @@ -31,6 +31,8 @@ type Conn interface { Close() error SetDebug(debug bool) LocalAddr() (addr string, err error) + RemoteAddr() (addr string, err error) + RemotePort() (addr string, err error) Send(requestID int, payload Payload, sourceID, destinationID, namespace string) error } @@ -89,6 +91,16 @@ func (c *Connection) LocalAddr() (addr string, err error) { return host, err } +func (c *Connection) RemoteAddr() (addr string, err error) { + addr, _, err = net.SplitHostPort(c.conn.RemoteAddr().String()) + return addr, err +} + +func (c *Connection) RemotePort() (port string, err error) { + _, port, err = net.SplitHostPort(c.conn.RemoteAddr().String()) + return port, err +} + func (c *Connection) log(message string, args ...interface{}) { if c.debug { log.WithField("package", "cast").Debugf(message, args...) diff --git a/cast/mocks/Conn.go b/cast/mocks/Conn.go index d01d39a..f73946b 100644 --- a/cast/mocks/Conn.go +++ b/cast/mocks/Conn.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.9.4. DO NOT EDIT. +// Code generated by mockery v2.49.1. DO NOT EDIT. package mocks @@ -18,6 +18,10 @@ type Conn struct { func (_m *Conn) Close() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Close") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -32,14 +36,21 @@ func (_m *Conn) Close() error { func (_m *Conn) LocalAddr() (string, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for LocalAddr") + } + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() string); ok { r0 = rf() } else { r0 = ret.Get(0).(string) } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -53,6 +64,10 @@ func (_m *Conn) LocalAddr() (string, error) { func (_m *Conn) MsgChan() chan *api.CastMessage { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for MsgChan") + } + var r0 chan *api.CastMessage if rf, ok := ret.Get(0).(func() chan *api.CastMessage); ok { r0 = rf() @@ -65,10 +80,70 @@ func (_m *Conn) MsgChan() chan *api.CastMessage { return r0 } +// RemoteAddr provides a mock function with given fields: +func (_m *Conn) RemoteAddr() (string, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RemoteAddr") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemotePort provides a mock function with given fields: +func (_m *Conn) RemotePort() (string, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RemotePort") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Send provides a mock function with given fields: requestID, payload, sourceID, destinationID, namespace func (_m *Conn) Send(requestID int, payload cast.Payload, sourceID string, destinationID string, namespace string) error { ret := _m.Called(requestID, payload, sourceID, destinationID, namespace) + if len(ret) == 0 { + panic("no return value specified for Send") + } + var r0 error if rf, ok := ret.Get(0).(func(int, cast.Payload, string, string, string) error); ok { r0 = rf(requestID, payload, sourceID, destinationID, namespace) @@ -88,6 +163,10 @@ func (_m *Conn) SetDebug(debug bool) { func (_m *Conn) Start(addr string, port int) error { ret := _m.Called(addr, port) + if len(ret) == 0 { + panic("no return value specified for Start") + } + var r0 error if rf, ok := ret.Get(0).(func(string, int) error); ok { r0 = rf(addr, port) @@ -97,3 +176,17 @@ func (_m *Conn) Start(addr string, port int) error { return r0 } + +// NewConn creates a new instance of Conn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewConn(t interface { + mock.TestingT + Cleanup(func()) +}) *Conn { + mock := &Conn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/cast/mocks/Payload.go b/cast/mocks/Payload.go index 88103e3..7898607 100644 --- a/cast/mocks/Payload.go +++ b/cast/mocks/Payload.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.9.4. DO NOT EDIT. +// Code generated by mockery v2.49.1. DO NOT EDIT. package mocks @@ -13,3 +13,17 @@ type Payload struct { func (_m *Payload) SetRequestId(id int) { _m.Called(id) } + +// NewPayload creates a new instance of Payload. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPayload(t interface { + mock.TestingT + Cleanup(func()) +}) *Payload { + mock := &Payload{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/cast/payload.go b/cast/payload.go index ca02446..76e9591 100644 --- a/cast/payload.go +++ b/cast/payload.go @@ -156,3 +156,13 @@ type SetVolume struct { PayloadHeader Volume Volume `json:"volume"` } + +type DeviceInfo struct { + Name string `json:"name"` + IpAddress string `json:"ip_address"` + Locale string `json:"locale"` + MacAddress string `json:"mac_address"` + Ssid string `json:"ssid"` + Timezone string `json:"timezone"` + UptimeSec float64 `json:"uptime"` +} diff --git a/cmd/scan.go b/cmd/scan.go index b3bf4d8..160ce08 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -20,38 +20,11 @@ import ( "sync" "time" - "encoding/json" "github.com/seancfoley/ipaddress-go/ipaddr" "github.com/spf13/cobra" - "io" - "net/http" + "github.com/vishen/go-chromecast/application" ) -type deviceInfo struct { - Name string -} - -// 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 *ipaddr.IPAddress) (info *deviceInfo, err error) { - resp, err := http.Get(fmt.Sprintf("http://%v:8008/setup/eureka_info", ip)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - info = new(deviceInfo) - if err := json.Unmarshal(data, info); err != nil { - return nil, err - } - return info, nil -} - // scanCmd triggers a scan var scanCmd = &cobra.Command{ Use: "scan", @@ -98,7 +71,7 @@ var scanCmd = &cobra.Command{ continue } conn.Close() - if info, err := getInfo(ip); err != nil { + 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) diff --git a/go.mod b/go.mod index ea571ac..490dee8 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require github.com/seancfoley/ipaddress-go v1.7.0 require ( github.com/rs/zerolog v1.33.0 + golang.org/x/sync v0.9.0 gopkg.in/ini.v1 v1.67.0 ) @@ -64,7 +65,6 @@ require ( golang.org/x/crypto v0.29.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sync v0.9.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.27.0 // indirect diff --git a/http/handlers.go b/http/handlers.go index 9c132d1..7d09e5c 100644 --- a/http/handlers.go +++ b/http/handlers.go @@ -10,6 +10,8 @@ import ( "sync" "time" + "golang.org/x/sync/errgroup" + log "github.com/sirupsen/logrus" "github.com/vishen/go-chromecast/application" "github.com/vishen/go-chromecast/dns" @@ -194,10 +196,11 @@ func (h *Handler) connect(w http.ResponseWriter, r *http.Request) { deviceAddr := q.Get("addr") devicePort := q.Get("port") + deviceName := q.Get("name") iface := q.Get("interface") wait := q.Get("wait") - if deviceAddr == "" || devicePort == "" { + if deviceAddr == "" || devicePort == "" || (deviceName == "" && devicePort != "8009") { h.log("device addr and/or port are missing, trying to lookup address for uuid %q", deviceUUID) devices := h.discoverDnsEntries(context.Background(), iface, wait) @@ -208,6 +211,7 @@ func (h *Handler) connect(w http.ResponseWriter, r *http.Request) { // TODO: This is an unnessecary conversion since // we cast back to int a bit later. devicePort = strconv.Itoa(device.Port) + deviceName = device.DeviceName } } } @@ -226,7 +230,7 @@ func (h *Handler) connect(w http.ResponseWriter, r *http.Request) { return } - app, err := h.connectInternal(deviceAddr, devicePortI) + app, err := h.connectInternal(deviceAddr, devicePortI, deviceName) if err != nil { h.log("unable to start application: %v", err) httpError(w, fmt.Errorf("unable to start application: %v", err)) @@ -244,11 +248,14 @@ func (h *Handler) connect(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) connectInternal(deviceAddr string, devicePort int) (application.App, error) { +func (h *Handler) connectInternal(deviceAddr string, devicePort int, deviceName string) (application.App, error) { applicationOptions := []application.ApplicationOption{ application.WithDebug(h.verbose), application.WithCacheDisabled(true), } + if deviceName != "" { + applicationOptions = append(applicationOptions, application.WithDeviceNameOverride(deviceName)) + } app := application.NewApplication(applicationOptions...) if err := app.Start(deviceAddr, devicePort); err != nil { @@ -287,7 +294,7 @@ func (h *Handler) connectAllInternal(iface string, waitSec string) error { devices := h.discoverDnsEntries(context.Background(), iface, waitSec) uuidMap := map[string]application.App{} for _, device := range devices { - app, err := h.connectInternal(device.Addr, device.Port) + app, err := h.connectInternal(device.Addr, device.Port, device.DeviceName) if err != nil { return err } @@ -348,7 +355,15 @@ func (h *Handler) status(w http.ResponseWriter, r *http.Request) { h.log("status for device") castApplication, castMedia, castVolume := app.Status() + info, err := app.Info() + if err != nil { + werr := fmt.Errorf("error getting device info: %v", err) + h.log("%v", werr) + httpError(w, werr) + return + } statusResponse := fromApplicationStatus( + info, castApplication, castMedia, castVolume, @@ -366,22 +381,32 @@ func (h *Handler) statuses(w http.ResponseWriter, r *http.Request) { h.log("statuses for devices") uuids := h.ConnectedDeviceUUIDs() mapUUID2Ch := map[string]chan statusResponse{} - + g := new(errgroup.Group) for _, deviceUUID := range uuids { app, ok := h.app(deviceUUID) if ok { ch := make(chan statusResponse, 1) mapUUID2Ch[deviceUUID] = ch - go func() { + g.Go(func() error { castApplication, castMedia, castVolume := app.Status() + info, err := app.Info() + if err != nil { + return fmt.Errorf("error getting device info: %v", err) + } ch <- fromApplicationStatus( + info, castApplication, castMedia, castVolume, ) - }() + return nil + }) } } + if err := g.Wait(); err != nil { + h.log("%v", err) + httpError(w, err) + } statusResponses := map[string]statusResponse{} for deviceUUID, ch := range mapUUID2Ch { diff --git a/http/types.go b/http/types.go index 3a68b7e..234dde5 100644 --- a/http/types.go +++ b/http/types.go @@ -12,6 +12,8 @@ type volumeResponse struct { } type statusResponse struct { + Info *cast.DeviceInfo `json:"info,omitempty"` + AppID string `json:"app_id"` DisplayName string `json:"display_name"` IsIdleScreen bool `json:"is_idle_screen"` @@ -44,9 +46,13 @@ type statusResponse struct { PlayerStateId int `json:"player_state_id"` } -func fromApplicationStatus(app *cast.Application, media *cast.Media, volume *cast.Volume) statusResponse { +func fromApplicationStatus(info *cast.DeviceInfo, app *cast.Application, media *cast.Media, volume *cast.Volume) statusResponse { status := statusResponse{} + if info != nil { + status.Info = info + } + if app != nil { status.AppID = app.AppId status.DisplayName = app.DisplayName diff --git a/regenerate_mocks.sh b/regenerate_mocks.sh new file mode 100755 index 0000000..cedf509 --- /dev/null +++ b/regenerate_mocks.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e -x + +cd ./cast +go run github.com/vektra/mockery/v2@v2.49.1 --all + +cd ../application +go run github.com/vektra/mockery/v2@v2.49.1 --all \ No newline at end of file