2023-02-17 10:38:45 +01:00
|
|
|
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 {
|
2023-02-17 13:03:10 +01:00
|
|
|
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 DeviceStatus struct {
|
2023-02-17 13:03:10 +01:00
|
|
|
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
|
|
|
|
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
|
2023-02-17 10:38:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type DeviceStatusCurrentApp struct {
|
2023-02-17 13:03:10 +01:00
|
|
|
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 {
|
2023-02-17 13:03:10 +01:00
|
|
|
Level float64 `goja:"level" json:"level"`
|
|
|
|
Muted bool `goja:"muted" json:"muted"`
|
2023-02-17 10:38:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
serviceDiscoveryPollingInterval time.Duration = 2 * time.Second
|
|
|
|
)
|
|
|
|
|
|
|
|
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
|
2023-04-05 15:12:51 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-05 15:12:51 +02:00
|
|
|
func FindDeviceByUUID(ctx context.Context, uuid string) (*Device, error) {
|
2023-02-17 10:38:45 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-04-05 15:12:51 +02:00
|
|
|
func FindDevices(ctx context.Context) ([]*Device, error) {
|
2023-02-17 10:38:45 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-05 15:12:51 +02:00
|
|
|
func LoadURL(ctx context.Context, deviceUUID string, url string) error {
|
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"
|
|
|
|
}
|
|
|
|
|
2023-04-05 15:12:51 +02:00
|
|
|
func StopCast(ctx context.Context, deviceUUID string) error {
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|