diff --git a/src/ytdl_sub/plugins/chapters.py b/src/ytdl_sub/plugins/chapters.py index 0c4e9bc0..4b25ff0d 100644 --- a/src/ytdl_sub/plugins/chapters.py +++ b/src/ytdl_sub/plugins/chapters.py @@ -28,6 +28,18 @@ SPONSORBLOCK_CATEGORIES: Set[str] = SPONSORBLOCK_HIGHLIGHT_CATEGORIES | { } +def _chapters(entry: Entry) -> List[Dict]: + if entry.kwargs_contains("chapters"): + return entry.kwargs("chapters") + return [] + + +def _sponsorblock_chapters(entry: Entry) -> List[Dict]: + if entry.kwargs_contains("sponsorblock_chapters"): + return entry.kwargs("sponsorblock_chapters") + return [] + + class SponsorBlockCategoriesValidator(StringSelectValidator): _expected_value_type_name = "sponsorblock category" _select_values = {"all"} | SPONSORBLOCK_CATEGORIES @@ -40,8 +52,8 @@ class SponsorBlockCategoryListValidator(ListValidator[SponsorBlockCategoriesVali class ChaptersOptions(PluginOptions): """ - Add chapters to video files if they are present. Additional options to add SponsorBlock - chapters and remove specific ones. Can also remove chapters using regex patterns. + Embeds chapters to video files if they are present. Additional options to add SponsorBlock + chapters and remove specific ones. Can also remove chapters using regex. Note that at this time, chapter removal with regex will not work with chapters added via timestamp file. @@ -66,6 +78,14 @@ class ChaptersOptions(PluginOptions): - "Intro" - "Outro" force_key_frames: False + + To simply embed chapters from the entry file and nothing more, specify the following: + + .. code-block:: yaml + + presets: + my_example_preset: + chapters: """ _optional_keys = { @@ -97,6 +117,11 @@ class ChaptersOptions(PluginOptions): @property def sponsorblock_categories(self) -> Optional[List[str]]: + """ + Optional. List of SponsorBlock categories to embed as chapters. Supports "sponsor", + "intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic", + "poi_highlight", or "all" to include all categories. + """ if self._sponsorblock_categories: category_list = [validator.value for validator in self._sponsorblock_categories.list] if "all" in category_list: @@ -106,6 +131,11 @@ class ChaptersOptions(PluginOptions): @property def remove_sponsorblock_categories(self) -> Optional[List[str]]: + """ + Optional. List of SponsorBlock categories to remove from the output file. Can only remove + categories that are specified in ``sponsorblock_categories`` or "all", which removes + everything specified in ``sponsorblock_categories``. + """ if self._remove_sponsorblock_categories: category_list = [ validator.value for validator in self._remove_sponsorblock_categories.list @@ -117,12 +147,21 @@ class ChaptersOptions(PluginOptions): @property def remove_chapters_regex(self) -> Optional[List[re.Pattern]]: + """ + Optional. List of regex patterns to match chapter titles against and remove them from the + entry. + """ if self._remove_chapters_regex: return [validator.compiled_regex for validator in self._remove_chapters_regex.list] return None @property def force_key_frames(self) -> bool: + """ + Optional. Force keyframes at cuts when removing sections. This is slow due to needing a + re-encode, but the resulting video may have fewer artifacts around the cuts. Defaults to + False. + """ return self._force_key_frames @@ -137,6 +176,11 @@ class ChaptersPlugin(Plugin[ChaptersOptions]): ) def ytdl_options(self) -> Optional[Dict]: + """ + Returns + ------- + YTDL options to embed chapters, add/remove SponsorBlock segments, remove chapters via regex + """ builder = YTDLOptionsBuilder() if self.plugin_options.sponsorblock_categories: builder.add( @@ -180,20 +224,10 @@ class ChaptersPlugin(Plugin[ChaptersOptions]): return builder.to_dict() - def _chapters(self, entry: Entry) -> List[Dict]: - if entry.kwargs_contains("chapters"): - return entry.kwargs("chapters") - return [] - - def _sponsorblock_chapters(self, entry: Entry) -> List[Dict]: - if entry.kwargs_contains("sponsorblock_chapters"): - return entry.kwargs("sponsorblock_chapters") - return [] - def _get_removed_chapters(self, entry: Entry) -> List[str]: removed_chapters: List[str] = [] for pattern in self.plugin_options.remove_chapters_regex or []: - for chapter in self._chapters(entry): + for chapter in _chapters(entry): if pattern.search(chapter["title"]): removed_chapters.append(chapter["title"]) return removed_chapters @@ -201,7 +235,7 @@ class ChaptersPlugin(Plugin[ChaptersOptions]): def _get_removed_sponsorblock_category_counts(self, entry: Entry) -> Dict: removed_category_counts = collections.Counter() for category in self.plugin_options.remove_sponsorblock_categories or []: - for chapter in self._sponsorblock_chapters(entry): + for chapter in _sponsorblock_chapters(entry): if chapter["category"] == category: removed_category_counts.update({chapter["title"]: 1}) @@ -214,6 +248,16 @@ class ChaptersPlugin(Plugin[ChaptersOptions]): ) def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]: + """ + Parameters + ---------- + entry: + Entry with possibly removed chapters + + Returns + ------- + FileMetadata outlining which chapters/SponsorBlock segments got removed + """ metadata_dict = {} removed_chapters = self._get_removed_chapters(entry) removed_sponsorblock = self._get_removed_sponsorblock_category_counts(entry) diff --git a/src/ytdl_sub/plugins/split_by_chapters.py b/src/ytdl_sub/plugins/split_by_chapters.py deleted file mode 100644 index 6fdc8d3c..00000000 --- a/src/ytdl_sub/plugins/split_by_chapters.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Optional - -from ytdl_sub.entries.entry import Entry -from ytdl_sub.plugins.plugin import Plugin -from ytdl_sub.plugins.plugin import PluginOptions -from ytdl_sub.validators.string_select_validator import StringSelectValidator - - -class WhenNoChaptersValidator(StringSelectValidator): - _expected_value_type = "when no chapters option" - _select_values = {"pass", "drop", "error"} - - -class SplitByChaptersOptions(PluginOptions): - """ - Splits a file by chapters into multiple files. Each file becomes its own entry with ``title`` - set to its chapter name, and is processed separately by other plugins. - - Usage: - - .. code-block:: yaml - - presets: - my_example_preset: - split_by_chapters: - when_no_chapters: "pass" # "drop"/"error" - """ - - _required_keys = {"when_no_chapters"} - - def __init__(self, name, value): - super().__init__(name, value) - self._when_no_chapters = self._validate_key( - key="when_no_chapters", validator=WhenNoChaptersValidator - ).value - - @property - def when_no_chapters(self) -> str: - """ - Behavior to perform when no chapters are present. Supports "pass" (continue processing), - "drop" (exclude it from output), and "error" (stop processing for everything). - """ - return self._when_no_chapters - - -class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]): - plugin_options_type = SplitByChaptersOptions - - def modify_entry(self, entry: Entry) -> Optional[Entry]: - """ - Tags the entry's audio file using values defined in the metadata options - """ - return entry