[SponsorBlock] Support chapter category (#5260)

Authored by: ajayyy, pukkandan
This commit is contained in:
Ajay Ramachandran 2022-10-18 12:51:57 -04:00 committed by GitHub
parent 814bba3933
commit 63c547d71c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 46 additions and 24 deletions

View file

@ -1042,7 +1042,7 @@ ## SponsorBlock Options:
for, separated by commas. Available for, separated by commas. Available
categories are sponsor, intro, outro, categories are sponsor, intro, outro,
selfpromo, preview, filler, interaction, selfpromo, preview, filler, interaction,
music_offtopic, poi_highlight, all and music_offtopic, poi_highlight, chapter, all and
default (=all). You can prefix the category default (=all). You can prefix the category
with a "-" to exclude it. See [1] for with a "-" to exclude it. See [1] for
description of the categories. E.g. description of the categories. E.g.
@ -1054,8 +1054,8 @@ ## SponsorBlock Options:
remove takes precedence. The syntax and remove takes precedence. The syntax and
available categories are the same as for available categories are the same as for
--sponsorblock-mark except that "default" --sponsorblock-mark except that "default"
refers to "all,-filler" and poi_highlight is refers to "all,-filler" and poi_highlight and
not available chapter are not available
--sponsorblock-chapter-title TEMPLATE --sponsorblock-chapter-title TEMPLATE
An output template for the title of the An output template for the title of the
SponsorBlock chapters created by SponsorBlock chapters created by

View file

@ -16,6 +16,7 @@
MetadataFromFieldPP, MetadataFromFieldPP,
MetadataParserPP, MetadataParserPP,
ModifyChaptersPP, ModifyChaptersPP,
SponsorBlockPP,
) )
@ -76,11 +77,15 @@ def setUp(self):
self._pp = ModifyChaptersPP(YoutubeDL()) self._pp = ModifyChaptersPP(YoutubeDL())
@staticmethod @staticmethod
def _sponsor_chapter(start, end, cat, remove=False): def _sponsor_chapter(start, end, cat, remove=False, title=None):
c = {'start_time': start, 'end_time': end, '_categories': [(cat, start, end)]} if title is None:
if remove: title = SponsorBlockPP.CATEGORIES[cat]
c['remove'] = True return {
return c 'start_time': start,
'end_time': end,
'_categories': [(cat, start, end, title)],
**({'remove': True} if remove else {}),
}
@staticmethod @staticmethod
def _chapter(start, end, title=None, remove=False): def _chapter(start, end, title=None, remove=False):
@ -130,6 +135,19 @@ def test_remove_marked_arrange_sponsors_ChapterWithSponsors(self):
'c', '[SponsorBlock]: Filler Tangent', 'c']) 'c', '[SponsorBlock]: Filler Tangent', 'c'])
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, []) self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
def test_remove_marked_arrange_sponsors_SponsorBlockChapters(self):
chapters = self._chapters([70], ['c']) + [
self._sponsor_chapter(10, 20, 'chapter', title='sb c1'),
self._sponsor_chapter(15, 16, 'chapter', title='sb c2'),
self._sponsor_chapter(30, 40, 'preview'),
self._sponsor_chapter(50, 60, 'filler')]
expected = self._chapters(
[10, 15, 16, 20, 30, 40, 50, 60, 70],
['c', '[SponsorBlock]: sb c1', '[SponsorBlock]: sb c1, sb c2', '[SponsorBlock]: sb c1',
'c', '[SponsorBlock]: Preview/Recap',
'c', '[SponsorBlock]: Filler Tangent', 'c'])
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
def test_remove_marked_arrange_sponsors_UniqueNamesForOverlappingSponsors(self): def test_remove_marked_arrange_sponsors_UniqueNamesForOverlappingSponsors(self):
chapters = self._chapters([120], ['c']) + [ chapters = self._chapters([120], ['c']) + [
self._sponsor_chapter(10, 45, 'sponsor'), self._sponsor_chapter(20, 40, 'selfpromo'), self._sponsor_chapter(10, 45, 'sponsor'), self._sponsor_chapter(20, 40, 'selfpromo'),
@ -173,7 +191,7 @@ def test_remove_marked_arrange_sponsors_ChapterWithSponsorCutInTheMiddle(self):
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts) self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
def test_remove_marked_arrange_sponsors_ChapterWithCutHidingSponsor(self): def test_remove_marked_arrange_sponsors_ChapterWithCutHidingSponsor(self):
cuts = [self._sponsor_chapter(20, 50, 'selpromo', remove=True)] cuts = [self._sponsor_chapter(20, 50, 'selfpromo', remove=True)]
chapters = self._chapters([60], ['c']) + [ chapters = self._chapters([60], ['c']) + [
self._sponsor_chapter(10, 20, 'intro'), self._sponsor_chapter(10, 20, 'intro'),
self._sponsor_chapter(30, 40, 'sponsor'), self._sponsor_chapter(30, 40, 'sponsor'),
@ -199,7 +217,7 @@ def test_remove_marked_arrange_sponsors_ChapterWithAdjacentCuts(self):
self._sponsor_chapter(10, 20, 'sponsor'), self._sponsor_chapter(10, 20, 'sponsor'),
self._sponsor_chapter(20, 30, 'interaction', remove=True), self._sponsor_chapter(20, 30, 'interaction', remove=True),
self._chapter(30, 40, remove=True), self._chapter(30, 40, remove=True),
self._sponsor_chapter(40, 50, 'selpromo', remove=True), self._sponsor_chapter(40, 50, 'selfpromo', remove=True),
self._sponsor_chapter(50, 60, 'interaction')] self._sponsor_chapter(50, 60, 'interaction')]
expected = self._chapters([10, 20, 30, 40], expected = self._chapters([10, 20, 30, 40],
['c', '[SponsorBlock]: Sponsor', ['c', '[SponsorBlock]: Sponsor',
@ -282,7 +300,7 @@ def test_remove_marked_arrange_sponsors_SponsorsNoLongerOverlapAfterCut(self):
chapters = self._chapters([70], ['c']) + [ chapters = self._chapters([70], ['c']) + [
self._sponsor_chapter(10, 30, 'sponsor'), self._sponsor_chapter(10, 30, 'sponsor'),
self._sponsor_chapter(20, 50, 'interaction'), self._sponsor_chapter(20, 50, 'interaction'),
self._sponsor_chapter(30, 50, 'selpromo', remove=True), self._sponsor_chapter(30, 50, 'selfpromo', remove=True),
self._sponsor_chapter(40, 60, 'sponsor'), self._sponsor_chapter(40, 60, 'sponsor'),
self._sponsor_chapter(50, 60, 'interaction')] self._sponsor_chapter(50, 60, 'interaction')]
expected = self._chapters( expected = self._chapters(

View file

@ -1737,7 +1737,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
'--sponsorblock-remove', metavar='CATS', '--sponsorblock-remove', metavar='CATS',
dest='sponsorblock_remove', default=set(), action='callback', type='str', dest='sponsorblock_remove', default=set(), action='callback', type='str',
callback=_set_from_options_callback, callback_kwargs={ callback=_set_from_options_callback, callback_kwargs={
'allowed_values': set(SponsorBlockPP.CATEGORIES.keys()) - set(SponsorBlockPP.POI_CATEGORIES.keys()), 'allowed_values': set(SponsorBlockPP.CATEGORIES.keys()) - set(SponsorBlockPP.NON_SKIPPABLE_CATEGORIES.keys()),
# Note: From https://wiki.sponsor.ajay.app/w/Types: # Note: From https://wiki.sponsor.ajay.app/w/Types:
# The filler category is very aggressive. # The filler category is very aggressive.
# It is strongly recommended to not use this in a client by default. # It is strongly recommended to not use this in a client by default.
@ -1747,7 +1747,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
'If a category is present in both mark and remove, remove takes precedence. ' 'If a category is present in both mark and remove, remove takes precedence. '
'The syntax and available categories are the same as for --sponsorblock-mark ' 'The syntax and available categories are the same as for --sponsorblock-mark '
'except that "default" refers to "all,-filler" ' 'except that "default" refers to "all,-filler" '
f'and {", ".join(SponsorBlockPP.POI_CATEGORIES.keys())} is not available')) f'and {", ".join(SponsorBlockPP.NON_SKIPPABLE_CATEGORIES.keys())} are not available'))
sponsorblock.add_option( sponsorblock.add_option(
'--sponsorblock-chapter-title', metavar='TEMPLATE', '--sponsorblock-chapter-title', metavar='TEMPLATE',
default=DEFAULT_SPONSORBLOCK_CHAPTER_TITLE, dest='sponsorblock_chapter_title', default=DEFAULT_SPONSORBLOCK_CHAPTER_TITLE, dest='sponsorblock_chapter_title',

View file

@ -16,7 +16,7 @@ def __init__(self, downloader, remove_chapters_patterns=None, remove_sponsor_seg
*, sponsorblock_chapter_title=DEFAULT_SPONSORBLOCK_CHAPTER_TITLE, force_keyframes=False): *, sponsorblock_chapter_title=DEFAULT_SPONSORBLOCK_CHAPTER_TITLE, force_keyframes=False):
FFmpegPostProcessor.__init__(self, downloader) FFmpegPostProcessor.__init__(self, downloader)
self._remove_chapters_patterns = set(remove_chapters_patterns or []) self._remove_chapters_patterns = set(remove_chapters_patterns or [])
self._remove_sponsor_segments = set(remove_sponsor_segments or []) - set(SponsorBlockPP.POI_CATEGORIES.keys()) self._remove_sponsor_segments = set(remove_sponsor_segments or []) - set(SponsorBlockPP.NON_SKIPPABLE_CATEGORIES.keys())
self._ranges_to_remove = set(remove_ranges or []) self._ranges_to_remove = set(remove_ranges or [])
self._sponsorblock_chapter_title = sponsorblock_chapter_title self._sponsorblock_chapter_title = sponsorblock_chapter_title
self._force_keyframes = force_keyframes self._force_keyframes = force_keyframes
@ -99,7 +99,7 @@ def _mark_chapters_to_remove(self, chapters, sponsor_chapters):
'start_time': start, 'start_time': start,
'end_time': end, 'end_time': end,
'category': 'manually_removed', 'category': 'manually_removed',
'_categories': [('manually_removed', start, end)], '_categories': [('manually_removed', start, end, 'Manually removed')],
'remove': True, 'remove': True,
} for start, end in self._ranges_to_remove) } for start, end in self._ranges_to_remove)
@ -290,13 +290,12 @@ def _remove_tiny_rename_sponsors(self, chapters):
c.pop('_was_cut', None) c.pop('_was_cut', None)
cats = c.pop('_categories', None) cats = c.pop('_categories', None)
if cats: if cats:
category = min(cats, key=lambda c: c[2] - c[1])[0] category, _, _, category_name = min(cats, key=lambda c: c[2] - c[1])
cats = orderedSet(x[0] for x in cats)
c.update({ c.update({
'category': category, 'category': category,
'categories': cats, 'categories': orderedSet(x[0] for x in cats),
'name': SponsorBlockPP.CATEGORIES[category], 'name': category_name,
'category_names': [SponsorBlockPP.CATEGORIES[c] for c in cats] 'category_names': orderedSet(x[3] for x in cats),
}) })
c['title'] = self._downloader.evaluate_outtmpl(self._sponsorblock_chapter_title, c.copy()) c['title'] = self._downloader.evaluate_outtmpl(self._sponsorblock_chapter_title, c.copy())
# Merge identically named sponsors. # Merge identically named sponsors.

View file

@ -14,6 +14,10 @@ class SponsorBlockPP(FFmpegPostProcessor):
POI_CATEGORIES = { POI_CATEGORIES = {
'poi_highlight': 'Highlight', 'poi_highlight': 'Highlight',
} }
NON_SKIPPABLE_CATEGORIES = {
**POI_CATEGORIES,
'chapter': 'Chapter',
}
CATEGORIES = { CATEGORIES = {
'sponsor': 'Sponsor', 'sponsor': 'Sponsor',
'intro': 'Intermission/Intro Animation', 'intro': 'Intermission/Intro Animation',
@ -23,7 +27,7 @@ class SponsorBlockPP(FFmpegPostProcessor):
'filler': 'Filler Tangent', 'filler': 'Filler Tangent',
'interaction': 'Interaction Reminder', 'interaction': 'Interaction Reminder',
'music_offtopic': 'Non-Music Section', 'music_offtopic': 'Non-Music Section',
**POI_CATEGORIES, **NON_SKIPPABLE_CATEGORIES
} }
def __init__(self, downloader, categories=None, api='https://sponsor.ajay.app'): def __init__(self, downloader, categories=None, api='https://sponsor.ajay.app'):
@ -68,12 +72,13 @@ def duration_filter(s):
def to_chapter(s): def to_chapter(s):
(start, end), cat = s['segment'], s['category'] (start, end), cat = s['segment'], s['category']
title = s['description'] if cat == 'chapter' else self.CATEGORIES[cat]
return { return {
'start_time': start, 'start_time': start,
'end_time': end, 'end_time': end,
'category': cat, 'category': cat,
'title': self.CATEGORIES[cat], 'title': title,
'_categories': [(cat, start, end)] '_categories': [(cat, start, end, title)],
} }
sponsor_chapters = [to_chapter(s) for s in duration_match] sponsor_chapters = [to_chapter(s) for s in duration_match]
@ -89,7 +94,7 @@ def _get_sponsor_segments(self, video_id, service):
url = f'{self._API_URL}/api/skipSegments/{hash[:4]}?' + urllib.parse.urlencode({ url = f'{self._API_URL}/api/skipSegments/{hash[:4]}?' + urllib.parse.urlencode({
'service': service, 'service': service,
'categories': json.dumps(self._categories), 'categories': json.dumps(self._categories),
'actionTypes': json.dumps(['skip', 'poi']) 'actionTypes': json.dumps(['skip', 'poi', 'chapter'])
}) })
for d in self._download_json(url) or []: for d in self._download_json(url) or []:
if d['videoID'] == video_id: if d['videoID'] == video_id: