This commit is contained in:
Jesse Bannon 2022-08-18 17:27:38 -07:00
parent 7450f0e727
commit 65d4a3e934
5 changed files with 63 additions and 24 deletions

View file

@ -219,6 +219,14 @@ regex
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
split_by_chapters
'''''''''''''''''
.. autoclass:: ytdl_sub.plugins.split_by_chapters.SplitByChaptersOptions()
:members: when_no_chapters
:member-order: bysource
-------------------------------------------------------------------------------
subtitles subtitles
''''''''' '''''''''
.. autoclass:: ytdl_sub.plugins.subtitles.SubtitleOptions() .. autoclass:: ytdl_sub.plugins.subtitles.SubtitleOptions()

View file

@ -31,6 +31,11 @@ class PluginPriority:
@property @property
def modify_entry_after_split(self) -> bool: def modify_entry_after_split(self) -> bool:
"""
Returns
-------
True if the plugin should modify an entry after a potential split. False otherwise.
"""
return self.modify_entry >= PluginPriority.MODIFY_ENTRY_AFTER_SPLIT return self.modify_entry >= PluginPriority.MODIFY_ENTRY_AFTER_SPLIT
@ -96,6 +101,7 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
# TODO pass yaml snake case name in the class somewhere, and use it for the logger # TODO pass yaml snake case name in the class somewhere, and use it for the logger
self._logger = Logger.get(self.__class__.__name__) self._logger = Logger.get(self.__class__.__name__)
# pylint: disable=no-self-use,unused-argument
def ytdl_options(self) -> Optional[Dict]: def ytdl_options(self) -> Optional[Dict]:
""" """
Returns Returns
@ -117,9 +123,8 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
------- -------
List of entries and metadata created from the source entry List of entries and metadata created from the source entry
""" """
raise NotImplemented() return []
# pylint: disable=no-self-use
def modify_entry(self, entry: Entry) -> Optional[Entry]: def modify_entry(self, entry: Entry) -> Optional[Entry]:
""" """
For each entry downloaded, modify the entry in some way before sending it to For each entry downloaded, modify the entry in some way before sending it to
@ -136,8 +141,6 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
""" """
return entry return entry
# pylint: enable=no-self-use
def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]: def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]:
""" """
For each entry downloaded, apply post processing to it. For each entry downloaded, apply post processing to it.
@ -151,6 +154,9 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
------- -------
Optional file metadata for the entry media file. Optional file metadata for the entry media file.
""" """
return None
# pylint: enable=no-self-use,unused-argument
def post_process_subscription(self): def post_process_subscription(self):
""" """

View file

@ -5,6 +5,8 @@ from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
from yt_dlp.utils import sanitize_filename
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import Plugin
from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.plugins.plugin import PluginOptions
@ -17,16 +19,6 @@ from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.validators.string_select_validator import StringSelectValidator from ytdl_sub.validators.string_select_validator import StringSelectValidator
#
# modify_entry BEFORE SPLIT
# - audio_extract
# - subtitles?
#
# TODO: make regex's modify_entry into a new function
# and call modify_entry before split
#
# maybe modify_downloaded_entry ??
def _split_video_ffmpeg_cmd( def _split_video_ffmpeg_cmd(
input_file: str, output_file: str, timestamps: List[Timestamp], idx: int input_file: str, output_file: str, timestamps: List[Timestamp], idx: int
@ -53,8 +45,14 @@ class WhenNoChaptersValidator(StringSelectValidator):
class SplitByChaptersOptions(PluginOptions): class SplitByChaptersOptions(PluginOptions):
""" """
Splits a file by chapters into multiple files. Each file becomes its own entry with the Splits a file by chapters into multiple files. Each file becomes its own entry with the
new source variables ``chapter_title``, ``chapter_index``, ``chapter_index_padded``, new source variables ``chapter_title``, ``chapter_title_sanitized``, ``chapter_index``,
``chapter_count``. ``chapter_index_padded``, ``chapter_count``.
If a file has no chapters, and ``when_no_chapters`` is set to "pass", then ``chapter_title`` is
set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1.
Note that when using this plugin and performing dry-run, it assumes embedded chapters are being
used with no modifications.
Usage: Usage:
@ -63,7 +61,7 @@ class SplitByChaptersOptions(PluginOptions):
presets: presets:
my_example_preset: my_example_preset:
split_by_chapters: split_by_chapters:
when_no_chapters: "pass" # "drop"/"error" when_no_chapters: "pass"
""" """
_required_keys = {"when_no_chapters"} _required_keys = {"when_no_chapters"}
@ -75,7 +73,13 @@ class SplitByChaptersOptions(PluginOptions):
).value ).value
def added_source_variables(self) -> List[str]: def added_source_variables(self) -> List[str]:
return ["chapter_title", "chapter_index", "chapter_index_padded", "chapter_count"] return [
"chapter_title",
"chapter_title_sanitized",
"chapter_index",
"chapter_index_padded",
"chapter_count",
]
@property @property
def when_no_chapters(self) -> str: def when_no_chapters(self) -> str:
@ -91,7 +95,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
is_split_plugin = True is_split_plugin = True
def _create_split_entry( def _create_split_entry(
self, dry_run: bool, source_entry: Entry, title: str, idx: int, chapters: Chapters self, source_entry: Entry, title: str, idx: int, chapters: Chapters
) -> Tuple[Entry, FileMetadata]: ) -> Tuple[Entry, FileMetadata]:
""" """
Runs ffmpeg to create the split video Runs ffmpeg to create the split video
@ -101,17 +105,20 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
entry.add_variables( entry.add_variables(
{ {
"chapter_title": title, "chapter_title": title,
"chapter_title_sanitized": sanitize_filename(title),
"chapter_index": idx + 1, "chapter_index": idx + 1,
"chapter_index_padded": f"{(idx + 1):02d}", "chapter_index_padded": f"{(idx + 1):02d}",
"chapter_count": len(chapters.timestamps), "chapter_count": len(chapters.timestamps),
} }
) )
entry._kwargs["id"] = _split_video_uid(source_uid=entry.uid, idx=idx)
# pylint: disable=protected-access
entry._kwargs["id"] = _split_video_uid(source_uid=entry.uid, idx=idx)
if "chapters" in entry._kwargs: if "chapters" in entry._kwargs:
del entry._kwargs["chapters"] del entry._kwargs["chapters"]
if "sponsorblock_chapters" in entry._kwargs: if "sponsorblock_chapters" in entry._kwargs:
del entry._kwargs["sponsorblock_chapters"] del entry._kwargs["sponsorblock_chapters"]
# pylint: enable=protected-access
timestamp_begin = chapters.timestamps[idx].readable_str timestamp_begin = chapters.timestamps[idx].readable_str
timestamp_end = Timestamp(entry.kwargs("duration")).readable_str timestamp_end = Timestamp(entry.kwargs("duration")).readable_str
@ -119,7 +126,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
timestamp_end = chapters.timestamps[idx + 1].readable_str timestamp_end = chapters.timestamps[idx + 1].readable_str
metadata_value_dict = {} metadata_value_dict = {}
if dry_run: if self.is_dry_run:
metadata_value_dict[ metadata_value_dict[
"Warning" "Warning"
] = "Dry-run assumes embedded chapters with no modifications" ] = "Dry-run assumes embedded chapters with no modifications"
@ -200,7 +207,6 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
# Format the split video as a YoutubePlaylistVideo # Format the split video as a YoutubePlaylistVideo
split_videos_and_metadata.append( split_videos_and_metadata.append(
self._create_split_entry( self._create_split_entry(
dry_run=self.is_dry_run,
source_entry=entry, source_entry=entry,
title=title, title=title,
idx=idx, idx=idx,

View file

@ -2,7 +2,6 @@ import json
import os import os
import re import re
import subprocess import subprocess
from io import BytesIO
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
@ -222,6 +221,16 @@ class Chapters:
@classmethod @classmethod
def from_embedded_chapters(cls, file_path: str) -> "Chapters": def from_embedded_chapters(cls, file_path: str) -> "Chapters":
"""
Parameters
----------
file_path
File to read ffmpeg chapter metadata from
Returns
-------
Chapters object
"""
proc = subprocess.run( proc = subprocess.run(
[ [
"ffprobe", "ffprobe",
@ -249,6 +258,16 @@ class Chapters:
@classmethod @classmethod
def from_entry_chapters(cls, entry: Entry) -> "Chapters": def from_entry_chapters(cls, entry: Entry) -> "Chapters":
"""
Parameters
----------
entry
Entry with yt-dlp chapter metadata
Returns
-------
Chapters object
"""
timestamps: List[Timestamp] = [] timestamps: List[Timestamp] = []
titles: List[str] = [] titles: List[str] = []

View file

@ -5,7 +5,7 @@ from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.ffmpeg import FFMPEG from ytdl_sub.utils.ffmpeg import FFMPEG
def convert_download_thumbnail(entry: Entry, error_if_not_found=True) -> None: def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None:
""" """
Converts an entry's downloaded thumbnail into jpg format Converts an entry's downloaded thumbnail into jpg format