siku2 / script.service.sponsorblock

Kodi add-on for SponsorBlock
MIT License
124 stars 14 forks source link

Per channel, skip certain number of seconds #39

Open probonopd opened 1 year ago

probonopd commented 1 year ago

Would it be possible to skip a certain number of seconds at beginning and end of each video in a certain channel?

My use case is that a channel I am watching is using a fixed into and outro and I want to filter those out; as far as I know the SponsorBlock database does not allow for this (yet?).

If you don't want this kind of functionality in this addon, would you know how to do it in a fork?

siku2 commented 1 year ago

If you don't want this kind of functionality in this addon, would you know how to do it in a fork?

I would absolutely accept this kind of feature as a contribution. I realize it doesn't fully align with the stated goal of this project, but it aligns with the core idea behind it.

probonopd commented 1 year ago

I would absolutely accept this kind of feature as a contribution.

That's great to hear, especially since upstream is not interested in channel-wide skips.

This being said, would you know where/how to best start?

siku2 commented 1 year ago

The first problem that needs to be solved is how to identify the channel from the video ID. The addon only deals with the video id, because that's all it needs to interact with the API. I would start by looking into the youtube addon itself to see whether it exposes an API to fetch this information. If it doesn't this addon would have to interact with the YouTube API directly, but we should avoid that if at all possible.

Once you have that, you can just add inject another "skip segment" here:

https://github.com/siku2/script.service.sponsorblock/blob/e9ba63b5263129ce1d34929469e49d21d2cac591/resources/lib/player_listener.py#L109-L117

probonopd commented 1 year ago

We can get the channel_id in the same way we are already getting the video_id:

INFO <general>: [script.service.sponsorblock] monitor: {'video_id': 'AowJGns66_4', 'channel_id': 'UC5I2hjZYiW9gZPVkvzM8_Cw', 'status': {'unlisted': False, 'private': False, 'crawlable': True, 'family_safe': False, 'live': False}}

So, in resources/lib/monitor.py we could do:

    def __handle_playback_init(self, data):
        try:
            video_id = data["video_id"]
        except KeyError:
            logger.warning("received playbackinit notification without video id")
            return
        try:
            channel_id = data["channel_id"]
        except KeyError:
            logger.warning("received playbackinit notification without channel id")
            return
        unlisted = data.get("unlisted", False)
        if unlisted and addon.get_config(CONF_IGNORE_UNLISTED, bool):
            logger.info("ignoring video %s because it's unlisted", video_id)
            self._player_listener.ignore_next_video(video_id)
            return

        # preload the segments
        self._player_listener.preload_segments(video_id, channel_id)

But it looks like resources/lib/player_listener.py actually uses get_video_id from youtube_api.py, so we might need to do a likewise get_channel_id in youtube_api.py? youtube_api.py is using JSON RPC, e.g., to get the video_id (in video_id_from_list_item) by using a Player.GetItem JSON RPC request.

Also, to be able to handle outros, we also need to check whether the video is live (if it is, we can't skip the outro), and get the duration from Player.GetProperties JSON RPC (example curl below). Is this the way?

curl -d '{ "jsonrpc": "2.0", "method": "Player.GetProperties", "params": { "playerid": 1, "properties": ["totaltime", "live"] }, "id": 1 }' -H 'Content-type: application/json' -X P
OST http://localhost:8080/jsonrpc
{"id":1,"jsonrpc":"2.0","result":{"live":false,"totaltime":{"hours":0,"milliseconds":0,"minutes":21,"seconds":25}}}

you can just add inject another "skip segment" here

How so? Can you give an example how to skip 3 seconds starting at 0:00.000?

siku2 commented 1 year ago

But it looks like resources/lib/player_listener.py actually uses get_video_id from youtube_api.py, so we might need to do a likewise get_channel_id in youtube_api.py? youtube_api.py is using JSON RPC, e.g., to get the video_id (in video_id_from_list_item) by using a Player.GetItem JSON RPC request.

That's mainly so this addon works with videos being played outside of the youtube addon. The addon will even work just based on the video's thumbnail. Historically, the RPC stuff was added to the youtube addon later on and now it's mainly used for pre-loading the segments before the video even starts. In this case I think it's fine if the feature only works for videos played in the youtube addon, so if you can do it with the playback_init method, go for it!

Also, to be able to handle outros, we also need to check whether the video is live (if it is, we can't skip the outro), and get the duration from Player.GetProperties JSON RPC (example curl below). Is this the way?

This looks correct, but I've been tricked by Kodi before, so make sure this actually switches to true when you play a live stream.

How so? Can you give an example how to skip 3 seconds starting at 0:00.000?

Here you go:

def get_channel_related_segments(channel_id: str, total_video_duration: float) -> list[SponsorSegment]:
  skip_intro_duration: float | None = 3.0   # TODO: read this from the config based on channel_id
  skip_outro_duration: float | None = 17.0  # TODO: also read from config

  segments: list[SponsorSegment] = []
  if skip_intro_duration is not None and skip_intro_duration > 0.0:
    segments.append(SponsorSegment(
      # TODO: if we want to clean this up a bit we could add a new class 'SkipSegment' which only 
      #       has 'start' and 'end' and then have two subclasses `SponsorSegment` and `ChannelConfigSegment`
      #       (or a better name :P).
      #       We need to handle these cases separately anyway because of the popup that's shown after a skip.
      uuid=None,
      category=None,
      # TODO: maybe the start could be a config as well?
      start=0.00,
      end=skip_intro_duration,
    ))

  if skip_outro_duration is not None and skip_outro_duration > 0.0:
    segments.append(SponsorSegment(
      uuid=None,
      category=None,
      start=total_video_duration - skip_outro_duration,
      end=total_video_duration,
    ))

  return segments

# and now in _prepare_segments

channel_id = None # TODO: get the channel id

self._segments = get_sponsor_segments(self._api, video_id)
if channel_id is not None:
  self._segments.extend(get_channel_related_segments(channel_id, total_video_duration))

# make sure the segments are sorted in ascending order over time!
self._segments.sort(key=lambda seg: seg.start)

This is just an example to illustrate the idea, feel free to do it differently.