6 from .ffmpeg
import FFmpegPostProcessor
9 class SponsorBlockPP(FFmpegPostProcessor
):
10 # https://wiki.sponsor.ajay.app/w/Types
15 'poi_highlight': 'Highlight',
17 NON_SKIPPABLE_CATEGORIES
= {
23 'intro': 'Intermission/Intro Animation',
24 'outro': 'Endcards/Credits',
25 'selfpromo': 'Unpaid/Self Promotion',
26 'preview': 'Preview/Recap',
27 'filler': 'Filler Tangent',
28 'interaction': 'Interaction Reminder',
29 'music_offtopic': 'Non-Music Section',
30 **NON_SKIPPABLE_CATEGORIES
,
33 def __init__(self
, downloader
, categories
=None, api
='https://sponsor.ajay.app'):
34 FFmpegPostProcessor
.__init
__(self
, downloader
)
35 self
._categories
= tuple(categories
or self
.CATEGORIES
.keys())
36 self
._API
_URL
= api
if re
.match('https?://', api
) else 'https://' + api
39 extractor
= info
['extractor_key']
40 if extractor
not in self
.EXTRACTORS
:
41 self
.to_screen(f
'SponsorBlock is not supported for {extractor}')
44 self
.to_screen('Fetching SponsorBlock segments')
45 info
['sponsorblock_chapters'] = self
._get
_sponsor
_chapters
(info
, info
.get('duration'))
48 def _get_sponsor_chapters(self
, info
, duration
):
49 segments
= self
._get
_sponsor
_segments
(info
['id'], self
.EXTRACTORS
[info
['extractor_key']])
51 def duration_filter(s
):
52 start_end
= s
['segment']
53 # Ignore entire video segments (https://wiki.sponsor.ajay.app/w/Types).
54 if start_end
== (0, 0):
56 # Ignore milliseconds difference at the start.
59 # Make POI chapters 1 sec so that we can properly mark them
60 if s
['category'] in self
.POI_CATEGORIES
:
62 # Ignore milliseconds difference at the end.
63 # Never allow the segment to exceed the video.
64 if duration
and duration
- start_end
[1] <= 1:
65 start_end
[1] = duration
66 # SponsorBlock duration may be absent or it may deviate from the real one.
67 diff
= abs(duration
- s
['videoDuration']) if s
['videoDuration'] else 0
68 return diff
< 1 or (diff
< 5 and diff
/ (start_end
[1] - start_end
[0]) < 0.05)
70 duration_match
= [s
for s
in segments
if duration_filter(s
)]
71 if len(duration_match
) != len(segments
):
72 self
.report_warning('Some SponsorBlock segments are from a video of different duration, maybe from an old version of this video')
75 (start
, end
), cat
= s
['segment'], s
['category']
76 title
= s
['description'] if cat
== 'chapter' else self
.CATEGORIES
[cat
]
82 'type': s
['actionType'],
83 '_categories': [(cat
, start
, end
, title
)],
86 sponsor_chapters
= [to_chapter(s
) for s
in duration_match
]
87 if not sponsor_chapters
:
88 self
.to_screen('No matching segments were found in the SponsorBlock database')
90 self
.to_screen(f
'Found {len(sponsor_chapters)} segments in the SponsorBlock database')
91 return sponsor_chapters
93 def _get_sponsor_segments(self
, video_id
, service
):
94 video_hash
= hashlib
.sha256(video_id
.encode('ascii')).hexdigest()
95 # SponsorBlock API recommends using first 4 hash characters.
96 url
= f
'{self._API_URL}/api/skipSegments/{video_hash[:4]}?' + urllib
.parse
.urlencode({
98 'categories': json
.dumps(self
._categories
),
99 'actionTypes': json
.dumps(['skip', 'poi', 'chapter']),
101 for d
in self
._download
_json
(url
) or []:
102 if d
['videoID'] == video_id
: