edge/pkg/module/cast/cast.go

210 lines
4.6 KiB
Go

package cast
import (
"context"
"net"
"time"
"github.com/barnybug/go-cast"
"github.com/barnybug/go-cast/discovery"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Device struct {
UUID string `goja:"uuid"`
Host net.IP `goja:"host"`
Port int `goja:"port"`
Name string `goja:"name"`
}
type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp"`
Volume DeviceStatusVolume `goja:"volume"`
}
type DeviceStatusCurrentApp struct {
ID string `goja:"id"`
DisplayName string `goja:"displayName"`
StatusText string `goja:"statusText"`
}
type DeviceStatusVolume struct {
Level float64 `goja:"level"`
Muted bool `goja:"muted"`
}
const (
serviceDiscoveryPollingInterval time.Duration = 2 * time.Second
)
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
device, err := findDeviceByUUID(ctx, uuid)
if err != nil {
return nil, errors.WithStack(err)
}
client := cast.NewClient(device.Host, device.Port)
return client, nil
}
func findDeviceByUUID(ctx context.Context, uuid string) (*Device, error) {
service := discovery.NewService(ctx)
defer service.Stop()
go func() {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
LOOP:
for {
select {
case c := <-service.Found():
if c.Uuid() == uuid {
return &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}, nil
}
case <-ctx.Done():
break LOOP
}
}
if err := ctx.Err(); err != nil {
return nil, errors.WithStack(err)
}
return nil, errors.WithStack(ErrDeviceNotFound)
}
func findDevices(ctx context.Context) ([]*Device, error) {
service := discovery.NewService(ctx)
defer service.Stop()
go func() {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
devices := make([]*Device, 0)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
if _, exists := found[c.Uuid()]; exists {
continue
}
devices = append(devices, &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
})
found[c.Uuid()] = struct{}{}
case <-ctx.Done():
break LOOP
}
}
if err := ctx.Err(); err != nil && !errors.Is(err, context.DeadlineExceeded) {
return nil, errors.WithStack(err)
}
return devices, nil
}
func loadURL(ctx context.Context, deviceUUID string, url string) error {
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return errors.WithStack(err)
}
defer client.Close()
controller, err := client.URL(ctx)
if err != nil {
return errors.WithStack(err)
}
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
return errors.WithStack(err)
}
return nil
}
// False positive workaround.
func isLoadURLContextExceeded(err error) bool {
return err.Error() == "Failed to send load command: context deadline exceeded"
}
func stopCast(ctx context.Context, deviceUUID string) error {
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return errors.WithStack(err)
}
defer client.Close()
if _, err := client.Receiver().QuitApp(ctx); err != nil {
return errors.WithStack(err)
}
return nil
}
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return nil, errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return nil, errors.WithStack(err)
}
defer client.Close()
ctrlStatus, err := client.Receiver().GetStatus(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
status := &DeviceStatus{
CurrentApp: DeviceStatusCurrentApp{
ID: "",
DisplayName: "",
StatusText: "",
},
Volume: DeviceStatusVolume{
Level: *ctrlStatus.Volume.Level,
Muted: *ctrlStatus.Volume.Muted,
},
}
if len(ctrlStatus.Applications) > 0 {
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
}
return status, nil
}