mirror of
				https://github.com/yt-dlp/yt-dlp.git
				synced 2025-10-30 22:25:19 +00:00 
			
		
		
		
	[SponsorBlock] Support chapter category (#5260)
				
					
				
			Authored by: ajayyy, pukkandan
This commit is contained in:
		 Ajay Ramachandran
					Ajay Ramachandran
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							814bba3933
						
					
				
				
					commit
					63c547d71c
				
			| @@ -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 | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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', | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user