diff --git a/docs/castsponsorskip.md b/docs/castsponsorskip.md index 6a20f7a..f3ccbef 100644 --- a/docs/castsponsorskip.md +++ b/docs/castsponsorskip.md @@ -23,20 +23,21 @@ castsponsorskip [flags] ### Options ``` - --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") - --mute-ads Mutes the device while an ad is playing (default true) - -i, --network-interface string Network interface to use for multicast dns discovery. (default all interfaces) - --paused-interval duration Interval to scan paused devices (default 1m0s) - --playing-interval duration Interval to scan playing devices (default 500ms) - --skip-delay duration Delay skipping the start of a segment - --skip-sponsors Skip sponsored segments with SponsorBlock (default true) - -v, --version version for castsponsorskip - --youtube-api-key string YouTube API key for fallback video identification (required on some Chromecast devices). + --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 + --ignore-segment-duration duration Ignores the previous sponsored segment for a set amount of time. Useful if you want to to go back and watch a segment. (default 1m0s) + --log-level string Log level (debug, info, warn, error) (default "info") + --mute-ads Mutes the device while an ad is playing (default true) + -i, --network-interface string Network interface to use for multicast dns discovery. (default all interfaces) + --paused-interval duration Interval to scan paused devices (default 1m0s) + --playing-interval duration Interval to scan playing devices (default 500ms) + --skip-delay duration Delay skipping the start of a segment + --skip-sponsors Skip sponsored segments with SponsorBlock (default true) + -v, --version version for castsponsorskip + --youtube-api-key string YouTube API key for fallback video identification (required on some Chromecast devices). ``` diff --git a/internal/config/config.go b/internal/config/config.go index c358ccc..418a08d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,10 +23,11 @@ func Reset() { Default = Config{ LogLevel: "info", - DiscoverInterval: 5 * time.Minute, - PausedInterval: time.Minute, - PlayingInterval: 500 * time.Millisecond, - SkipDelay: 0, + DiscoverInterval: 5 * time.Minute, + PausedInterval: time.Minute, + PlayingInterval: 500 * time.Millisecond, + SkipDelay: 0, + IgnoreSegmentDuration: time.Minute, NetworkInterface: nil, @@ -44,12 +45,13 @@ type Config struct { LogLevel string `mapstructure:"log-level"` - 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"` + 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"` + IgnoreSegmentDuration time.Duration `mapstructure:"ignore-segment-duration"` NetworkInterfaceName string `mapstructure:"network-interface"` NetworkInterface *net.Interface @@ -71,6 +73,7 @@ func (c *Config) RegisterFlags(cmd *cobra.Command) { c.RegisterPausedInterval(cmd) c.RegisterPlayingInterval(cmd) c.RegisterSkipDelay(cmd) + c.RegisterSegmentIgnore(cmd) c.RegisterSkipSponsors(cmd) c.RegisterCategories(cmd) c.RegisterActionTypes(cmd) diff --git a/internal/config/intervals.go b/internal/config/intervals.go index d8541d1..5e05313 100644 --- a/internal/config/intervals.go +++ b/internal/config/intervals.go @@ -79,3 +79,16 @@ func (c *Config) RegisterSkipDelay(cmd *cobra.Command) { panic(err) } } + +func (c *Config) RegisterSegmentIgnore(cmd *cobra.Command) { + key := "ignore-segment-duration" + cmd.PersistentFlags().Duration(key, Default.IgnoreSegmentDuration, "Ignores the previous sponsored segment for a set amount of time. Useful if you want to to go back and watch a segment.") + if err := c.viper.BindPFlag(key, cmd.PersistentFlags().Lookup(key)); err != nil { + panic(err) + } + if err := cmd.RegisterFlagCompletionFunc(key, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"30s", "1m", "2m", "5m"}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveKeepOrder + }); err != nil { + panic(err) + } +} diff --git a/internal/device/watch.go b/internal/device/watch.go index b02e08e..2e8ce6c 100644 --- a/internal/device/watch.go +++ b/internal/device/watch.go @@ -26,7 +26,8 @@ const ( StateIdle = "IDLE" StateAd = 1081 - NoMutedSegment = -1 + NoMutedSegment = -1 + NoSkippedSegment = -1 ) var ( @@ -47,10 +48,12 @@ type Device struct { tickInterval time.Duration ticker *time.Ticker - state string - meta VideoMeta - segments []sponsorblock.Segment - mutedSegmentId int + state string + meta VideoMeta + segments []sponsorblock.Segment + prevSegmentIdx int + prevSegmentIgnore time.Time + mutedSegmentId int } func NewDevice(entry castdns.CastEntry, opts ...Option) *Device { @@ -85,6 +88,7 @@ func NewDevice(entry castdns.CastEntry, opts ...Option) *Device { entry: entry, logger: logger, mutedSegmentId: NoMutedSegment, + prevSegmentIdx: NoSkippedSegment, } for _, opt := range opts { @@ -201,6 +205,7 @@ func (d *Device) tick() error { if d.meta.CurrVideoId != d.meta.PrevVideoId { d.segments = nil + d.prevSegmentIdx = NoSkippedSegment if d.meta.CurrVideoId != "" { d.logger.Info("Detected video stream.", "video_id", d.meta.CurrVideoId) d.meta.PrevVideoId = d.meta.CurrVideoId @@ -304,6 +309,7 @@ func (d *Device) onMessage(msg *api.CastMessage) { case "CLOSE": d.unmuteSegment() d.segments = nil + d.prevSegmentIdx = NoSkippedSegment d.meta.Clear() } } @@ -337,6 +343,7 @@ func (d *Device) queryVideoId() { d.meta.PrevTitle = d.meta.CurrTitle d.unmuteSegment() d.segments = nil + d.prevSegmentIdx = NoSkippedSegment if config.Default.YouTubeAPIKey == "" { d.logger.Error("Video ID not set. Please configure a YouTube API key.") @@ -393,12 +400,22 @@ func (d *Device) handleSegment(castMedia *cast.Media, castVol *cast.Volume, segm to := time.Duration(segment.Segment[1]) * time.Second switch segment.ActionType { case sponsorblock.ActionTypeSkip: + if i == d.prevSegmentIdx { + if now := time.Now(); now.Before(d.prevSegmentIgnore) { + d.logger.Debug("Ignoring segment.", "category", segment.Category, "from", from, "to", to, "until", d.prevSegmentIgnore.Truncate(time.Second).String()) + d.prevSegmentIgnore = now.Add(config.Default.IgnoreSegmentDuration) + return + } + } + d.logger.Info("Skipping to timestamp.", "category", segment.Category, "from", from, "to", to) // Cast API seems to ignore decimals, so add 100ms to seek time in case sponsorship ends at 0.9 seconds. if err := d.app.SeekToTime(segment.Segment[1] + 0.1); err != nil { d.logger.Warn("Failed to seek to timestamp.", "to", segment.Segment[1], "error", err.Error()) } castMedia.CurrentTime = segment.Segment[1] + d.prevSegmentIdx = i + d.prevSegmentIgnore = time.Now().Add(config.Default.IgnoreSegmentDuration) case sponsorblock.ActionTypeMute: if !castVol.Muted || i != d.mutedSegmentId { d.logger.Info("Mute segment.", "category", segment.Category, "from", from, "to", to)