edge/pkg/module/cast/cast.go

288 lines
5.8 KiB
Go
Raw Normal View History

2023-02-17 10:38:45 +01:00
package cast
import (
"context"
"net"
"sync"
2023-02-17 10:38:45 +01:00
"time"
"github.com/barnybug/go-cast"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Device struct {
UUID string `goja:"uuid" json:"uuid"`
Host net.IP `goja:"host" json:"host"`
Port int `goja:"port" json:"port"`
Name string `goja:"name" json:"name"`
2023-02-17 10:38:45 +01:00
}
type CachedDevice struct {
Device
UpdatedAt time.Time
}
func (d CachedDevice) Expired() bool {
return d.UpdatedAt.Add(30 * time.Minute).Before(time.Now())
}
2023-02-17 10:38:45 +01:00
type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
2023-02-17 10:38:45 +01:00
}
type DeviceStatusCurrentApp struct {
ID string `goja:"id" json:"id"`
DisplayName string `goja:"displayName" json:"displayName"`
StatusText string `goja:"statusText" json:"statusText"`
2023-02-17 10:38:45 +01:00
}
type DeviceStatusVolume struct {
Level float64 `goja:"level" json:"level"`
Muted bool `goja:"muted" json:"muted"`
2023-02-17 10:38:45 +01:00
}
const (
serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
2023-02-17 10:38:45 +01:00
)
var cache sync.Map
func getCachedDevice(uuid string) (Device, bool) {
value, exists := cache.Load(uuid)
if !exists {
return Device{}, false
}
cachedDevice, ok := value.(CachedDevice)
if !ok {
return Device{}, false
}
if cachedDevice.Expired() {
return Device{}, false
}
return cachedDevice.Device, true
}
func cacheDevice(dev Device) {
cache.Store(dev.UUID, CachedDevice{
Device: dev,
UpdatedAt: time.Now(),
})
}
2023-02-17 10:38:45 +01:00
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
device, err := FindDeviceByUUID(ctx, uuid)
2023-02-17 10:38:45 +01:00
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) {
device, exists := getCachedDevice(uuid)
if exists {
return device, nil
}
2023-02-17 10:38:45 +01:00
ctx, cancel := context.WithCancel(ctx)
defer cancel()
devices, err := SearchDevices(ctx)
if err != nil {
return Device{}, nil
}
for dev := range devices {
if dev.UUID == uuid {
return dev, nil
2023-02-17 10:38:45 +01:00
}
}
2023-02-17 10:38:45 +01:00
return Device{}, errors.Errorf("could not find device '%s'", uuid)
}
func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
devices := make([]Device, 0)
if !refresh {
cache.Range(func(key, value any) bool {
cached, ok := value.(CachedDevice)
if !ok || cached.Expired() {
return true
2023-02-17 10:38:45 +01:00
}
devices = append(devices, cached.Device)
return true
})
return devices, nil
2023-02-17 10:38:45 +01:00
}
ch, err := SearchDevices(ctx)
if err != nil {
2023-02-17 10:38:45 +01:00
return nil, errors.WithStack(err)
}
for dev := range ch {
devices = append(devices, dev)
}
return devices, nil
2023-02-17 10:38:45 +01:00
}
var searchDevicesMutex sync.Mutex
func SearchDevices(ctx context.Context) (chan Device, error) {
service := NewService(ctx)
2023-02-17 10:38:45 +01:00
defer service.Stop()
go func() {
searchDevicesMutex.Lock()
defer searchDevicesMutex.Unlock()
2023-02-17 10:38:45 +01:00
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.CapturedE(errors.WithStack(err)))
2023-02-17 10:38:45 +01:00
}
}()
devices := make(chan Device)
2023-02-17 10:38:45 +01:00
go func() {
defer close(devices)
2023-02-17 10:38:45 +01:00
found := make(map[string]struct{})
2023-02-17 10:38:45 +01:00
LOOP:
for {
select {
case c := <-service.Found():
dev := Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}
2023-02-17 10:38:45 +01:00
if _, exists := found[dev.UUID]; !exists {
devices <- dev
found[dev.UUID] = struct{}{}
}
cacheDevice(dev)
case <-ctx.Done():
break LOOP
}
}
}()
2023-02-17 10:38:45 +01:00
return devices, nil
}
var loadURLMutex sync.Mutex
func LoadURL(ctx context.Context, deviceUUID string, url string) error {
loadURLMutex.Lock()
defer loadURLMutex.Unlock()
2023-02-17 10:38:45 +01:00
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"
}
var stopCastMutex sync.Mutex
func StopCast(ctx context.Context, deviceUUID string) error {
stopCastMutex.Lock()
defer stopCastMutex.Unlock()
2023-02-17 10:38:45 +01:00
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
}
var getStatusMutex sync.Mutex
2023-02-17 10:38:45 +01:00
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
getStatusMutex.Lock()
defer getStatusMutex.Unlock()
2023-02-17 10:38:45 +01:00
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
}