[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 @@ Make chapter entries for, or remove various segments (sponsor,
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 @@ Make chapter entries for, or remove various segments (sponsor,
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 @@ from yt_dlp.postprocessor import (
MetadataFromFieldPP, MetadataFromFieldPP,
MetadataParserPP, MetadataParserPP,
ModifyChaptersPP, ModifyChaptersPP,
SponsorBlockPP,
) )
@ -76,11 +77,15 @@ class TestModifyChaptersPP(unittest.TestCase):
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 @@ class TestModifyChaptersPP(unittest.TestCase):
'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 @@ class TestModifyChaptersPP(unittest.TestCase):
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 @@ class TestModifyChaptersPP(unittest.TestCase):
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 @@ class TestModifyChaptersPP(unittest.TestCase):
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 create_parser():
'--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 create_parser():
'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 @@ class ModifyChaptersPP(FFmpegPostProcessor):
*, 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 @@ class ModifyChaptersPP(FFmpegPostProcessor):
'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 @@ class ModifyChaptersPP(FFmpegPostProcessor):
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 @@ class SponsorBlockPP(FFmpegPostProcessor):
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 @@ class SponsorBlockPP(FFmpegPostProcessor):
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: