diff --git a/README.md b/README.md index c869570..e03d7e7 100644 --- a/README.md +++ b/README.md @@ -156,14 +156,15 @@ CastSponsorSkip can be configured with envs, command-line flags, or a config fil To use an env that is not listed here, capitalize all characters, replace `-` with `_`, and prefix with `CSS_`. For example, `--paused-interval=1m` would become `CSS_PAUSED_INTERVAL=1m`. ### Notable Envs -| Env | Description | Default | -|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| -| `CSS_DISCOVER_INTERVAL` | Interval to restart the DNS discovery client. | `5m` | -| `CSS_PAUSED_INTERVAL` | Time to wait between each poll of the Cast device status when paused. | `1m` | -| `CSS_PLAYING_INTERVAL` | Time to wait between each poll of the Cast device status when playing. | `500ms` | -| `CSS_CATEGORIES` | Comma-separated list of SponsorBlock categories to skip, see [category list](https://wiki.sponsor.ajay.app/w/Types#Category). | `sponsor` | -| `CSS_YOUTUBE_API_KEY` | [YouTube API key](https://developers.google.com/youtube/registering_an_application) for fallback video identification (required on some Chromecast devices). | ` ` | -| `CSS_MUTE_ADS` | Mutes the device while an ad is playing. | `true` | +| Env | Description | Default | +|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| `CSS_DISCOVER_INTERVAL` | Interval to restart the DNS discovery client. | `5m` | +| `CSS_PAUSED_INTERVAL` | Time to wait between each poll of the Cast device status when paused. | `1m` | +| `CSS_PLAYING_INTERVAL` | Time to wait between each poll of the Cast device status when playing. | `500ms` | +| `CSS_CATEGORIES` | Comma-separated list of SponsorBlock categories to skip, see [category list](https://wiki.sponsor.ajay.app/w/Types#Category). | `sponsor` | +| `CSS_YOUTUBE_API_KEY` | [YouTube API key](https://developers.google.com/youtube/registering_an_application) for fallback video identification (required on some Chromecast devices). | ` ` | +| `CSS_MUTE_ADS` | Mutes the device while an ad is playing. | `true` | +| `CSS_DEVICES` | Comma-separated list of device addresses. This will disable discovery and is not recommended unless discovery fails. | `[]` | > **Note** > [sponsorblockcast envs](https://github.com/nichobi/sponsorblockcast#configuration) are also supported to simplify the migration to CastSponsorSkip. When used, a deprecation warning will be logged with an updated env key and value. There are currently no plans to remove these envs. diff --git a/docs/castsponsorskip.md b/docs/castsponsorskip.md index 665f167..6a20f7a 100644 --- a/docs/castsponsorskip.md +++ b/docs/castsponsorskip.md @@ -26,6 +26,7 @@ castsponsorskip [flags] --action-types strings SponsorBlock action types to handle. Shorter segments that overlap with content can be muted instead of skipped. (default [skip,mute]) -c, --categories strings Comma-separated list of SponsorBlock categories to skip (default [sponsor]) --completion string Output command-line completion code for the specified shell. Can be 'bash', 'zsh', 'fish', or 'powershell'. + --devices strings Comma-separated list of device addresses. This will disable discovery and is not recommended unless discovery fails --discover-interval duration Interval to restart the DNS discovery client (default 5m0s) -h, --help help for castsponsorskip --log-level string Log level (debug, info, warn, error) (default "info") diff --git a/internal/config/config.go b/internal/config/config.go index 8132642..c358ccc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,11 +3,14 @@ package config import ( "fmt" "net" + "net/url" + "strconv" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/viper" + castdns "github.com/vishen/go-chromecast/dns" ) var Default Config @@ -41,10 +44,12 @@ type Config struct { LogLevel string `mapstructure:"log-level"` - DiscoverInterval time.Duration `mapstructure:"discover-interval"` - PausedInterval time.Duration `mapstructure:"paused-interval"` - PlayingInterval time.Duration `mapstructure:"playing-interval"` - SkipDelay time.Duration `mapstructure:"skip-delay"` + DeviceAddrStrs []string `mapstructure:"devices"` + DeviceAddrs []castdns.CastEntry `mapstructure:"-"` + DiscoverInterval time.Duration `mapstructure:"discover-interval"` + PausedInterval time.Duration `mapstructure:"paused-interval"` + PlayingInterval time.Duration `mapstructure:"playing-interval"` + SkipDelay time.Duration `mapstructure:"skip-delay"` NetworkInterfaceName string `mapstructure:"network-interface"` NetworkInterface *net.Interface @@ -59,6 +64,7 @@ type Config struct { func (c *Config) RegisterFlags(cmd *cobra.Command) { c.viper = viper.New() + c.RegisterDevices(cmd) c.RegisterLogLevel(cmd) c.RegisterNetworkInterface(cmd) c.RegisterDiscoverInterval(cmd) @@ -111,5 +117,38 @@ func (c *Config) Load() error { c.ActionTypes[i] = strings.TrimSpace(actionType) } + if len(c.DeviceAddrStrs) != 0 { + c.DeviceAddrs = make([]castdns.CastEntry, 0, len(c.DeviceAddrStrs)) + for _, device := range c.DeviceAddrStrs { + u := url.URL{Host: device} + + castEntry := castdns.CastEntry{ + DeviceName: device, + UUID: device, + } + + if port := u.Port(); port == "" { + castEntry.Port = 8009 + } else { + port, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return err + } + + castEntry.Port = int(port) + } + + if ip := net.ParseIP(u.Hostname()); ip == nil { + return fmt.Errorf("failed to parse IP %q", device) + } else if ip.To4() != nil { + castEntry.AddrV4 = ip + } else { + castEntry.AddrV6 = ip + } + + c.DeviceAddrs = append(c.DeviceAddrs, castEntry) + } + } + return nil } diff --git a/internal/config/device.go b/internal/config/device.go new file mode 100644 index 0000000..6f24311 --- /dev/null +++ b/internal/config/device.go @@ -0,0 +1,13 @@ +package config + +import ( + "github.com/spf13/cobra" +) + +func (c *Config) RegisterDevices(cmd *cobra.Command) { + key := "devices" + cmd.PersistentFlags().StringSlice(key, Default.DeviceAddrStrs, "Comma-separated list of device addresses. This will disable discovery and is not recommended unless discovery fails") + if err := c.viper.BindPFlag(key, cmd.PersistentFlags().Lookup(key)); err != nil { + panic(err) + } +} diff --git a/internal/device/dns.go b/internal/device/dns.go index 8810e4a..47ba541 100644 --- a/internal/device/dns.go +++ b/internal/device/dns.go @@ -55,26 +55,50 @@ func DiscoverCastDNSEntries(ctx context.Context, iface *net.Interface, ch chan c } func BeginDiscover(ctx context.Context) (<-chan castdns.CastEntry, error) { - if config.Default.NetworkInterface != nil { - slog.Info("Searching for devices...", "interface", config.Default.NetworkInterfaceName) - } else { - slog.Info("Searching for devices...") - } - ch := make(chan castdns.CastEntry) + ctx, cancel := context.WithCancel(ctx) + go func() { - defer func() { - close(ch) - }() + defer close(ch) + defer cancel() - for { - if ctx.Err() != nil { - return + if len(config.Default.DeviceAddrs) == 0 { + if config.Default.NetworkInterface != nil { + slog.Info("Searching for devices...", "interface", config.Default.NetworkInterfaceName) + } else { + slog.Info("Searching for devices...") + } + + for { + select { + case <-ctx.Done(): + return + default: + if err := DiscoverCastDNSEntries(ctx, config.Default.NetworkInterface, ch); err != nil { + slog.Error("Failed to discover devices.", "error", err.Error()) + continue + } + } + } + } else { + if config.Default.NetworkInterface != nil { + slog.Info("Connecting to configured devices...", "interface", config.Default.NetworkInterfaceName) + } else { + slog.Info("Connecting to configured devices...") } - if err := DiscoverCastDNSEntries(ctx, config.Default.NetworkInterface, ch); err != nil { - slog.Error("Failed to discover devices.", "error", err.Error()) - continue + timer := time.NewTimer(0) + defer timer.Stop() + for { + select { + case <-ctx.Done(): + return + case <-timer.C: + for _, castEntry := range config.Default.DeviceAddrs { + ch <- castEntry + } + timer.Reset(config.Default.DiscoverInterval) + } } } }()