ready
This commit is contained in:
parent
7450f0e727
commit
65d4a3e934
5 changed files with 63 additions and 24 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue