153 lines
5.3 KiB
Python
153 lines
5.3 KiB
Python
from typing import Dict
|
|
from typing import List
|
|
from typing import Optional
|
|
from typing import Tuple
|
|
|
|
from ytdl_sub.downloaders.downloader import Downloader
|
|
from ytdl_sub.downloaders.youtube.playlist import YoutubePlaylistDownloaderOptions
|
|
from ytdl_sub.entries.entry import Entry
|
|
from ytdl_sub.utils.chapters import Chapters
|
|
from ytdl_sub.utils.chapters import Timestamp
|
|
from ytdl_sub.utils.ffmpeg import set_ffmpeg_metadata_chapters
|
|
from ytdl_sub.utils.file_handler import FileMetadata
|
|
from ytdl_sub.validators.validators import BoolValidator
|
|
|
|
|
|
class YoutubeMergePlaylistDownloaderOptions(YoutubePlaylistDownloaderOptions):
|
|
r"""
|
|
Downloads all videos in a playlist and merges them into a single video.
|
|
|
|
Usage:
|
|
|
|
.. code-block:: yaml
|
|
|
|
presets:
|
|
example_preset:
|
|
youtube:
|
|
# required
|
|
download_strategy: "merge_playlist"
|
|
playlist_url: "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
|
|
# optional
|
|
add_chapters: False
|
|
|
|
CLI usage:
|
|
|
|
.. code-block:: bash
|
|
|
|
ytdl-sub dl \
|
|
--preset "example_preset" \
|
|
--youtube.playlist_url "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg" \
|
|
--youtube.add_chapters True
|
|
"""
|
|
|
|
_required_keys = {"playlist_url"}
|
|
_optional_keys = {"add_chapters"}
|
|
|
|
def __init__(self, name, value):
|
|
super().__init__(name, value)
|
|
self._add_chapters = self._validate_key_if_present(
|
|
"add_chapters", validator=BoolValidator, default=False
|
|
).value
|
|
|
|
@property
|
|
def add_chapters(self) -> Optional[bool]:
|
|
"""
|
|
Optional. Whether to add chapters using each video's title in the merged playlist.
|
|
Defaults to False.
|
|
"""
|
|
return self._add_chapters
|
|
|
|
|
|
class YoutubeMergePlaylistDownloader(Downloader[YoutubeMergePlaylistDownloaderOptions]):
|
|
downloader_options_type = YoutubeMergePlaylistDownloaderOptions
|
|
supports_download_archive = False
|
|
supports_subtitles = False
|
|
supports_chapters = False
|
|
|
|
@classmethod
|
|
def ytdl_option_defaults(cls) -> Dict:
|
|
"""
|
|
Default `ytdl_options`_ for ``merge_playlist``
|
|
|
|
.. code-block:: yaml
|
|
|
|
ytdl_options:
|
|
ignoreerrors: True # ignore errors like hidden videos, age restriction, etc
|
|
postprocessors:
|
|
# Convert the videos to mkv format
|
|
- key: "FFmpegVideoConvertor"
|
|
when: "post_process"
|
|
preferedformat: "mkv"
|
|
# Concatenate all the playlist videos into a single file
|
|
- key: "FFmpegConcat"
|
|
when: "playlist"
|
|
"""
|
|
return dict(
|
|
super().ytdl_option_defaults(),
|
|
**{
|
|
"postprocessors": [
|
|
{
|
|
"key": "FFmpegVideoConvertor",
|
|
"when": "post_process",
|
|
"preferedformat": "mkv",
|
|
},
|
|
{
|
|
"key": "FFmpegConcat",
|
|
"when": "playlist",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
|
|
def _get_chapters(self, merged_video: Entry, add_chapters: bool) -> FileMetadata:
|
|
titles: List[str] = []
|
|
timestamps: List[Timestamp] = []
|
|
|
|
current_timestamp_sec = 0
|
|
for video_entry in merged_video.kwargs("entries"):
|
|
timestamps.append(Timestamp(current_timestamp_sec))
|
|
titles.append(video_entry["title"])
|
|
|
|
current_timestamp_sec += video_entry["duration"]
|
|
|
|
chapters = Chapters(timestamps=timestamps, titles=titles)
|
|
|
|
if not self.is_dry_run and add_chapters:
|
|
set_ffmpeg_metadata_chapters(
|
|
file_path=merged_video.get_download_file_path(),
|
|
chapters=chapters,
|
|
file_duration_sec=merged_video.kwargs("duration"),
|
|
)
|
|
|
|
return chapters.to_file_metadata(title="Timestamps of playlist videos in the merged file")
|
|
|
|
def _to_merged_video(self, entry_dict: Dict) -> Entry:
|
|
"""
|
|
Adds a few entries not included in a playlist entry to make it look like a merged video
|
|
entry_dict
|
|
"""
|
|
# Set the upload date to be the latest playlist video date
|
|
entry_dict["upload_date"] = max(
|
|
playlist_entry["upload_date"] for playlist_entry in entry_dict["entries"]
|
|
)
|
|
entry_dict["duration"] = sum(
|
|
playlist_entry["duration"] for playlist_entry in entry_dict["entries"]
|
|
)
|
|
entry_dict["ext"] = (
|
|
entry_dict["requested_downloads"][0]["ext"]
|
|
if "requested_downloads" in entry_dict
|
|
else "mkv"
|
|
)
|
|
entry_dict["webpage_url"] = self.download_options.playlist_url
|
|
|
|
return Entry(entry_dict=entry_dict, working_directory=self.working_directory)
|
|
|
|
def download(self) -> List[Tuple[Entry, FileMetadata]]:
|
|
"""Download a single Youtube video, then split it into multiple videos"""
|
|
entry_dict = self.extract_info(url=self.collection.collection_urls.list[0].url)
|
|
merged_video = self._to_merged_video(entry_dict=entry_dict)
|
|
|
|
merged_video_metadata = self._get_chapters(
|
|
merged_video=merged_video, add_chapters=self.download_options.add_chapters
|
|
)
|
|
return [(merged_video, merged_video_metadata)]
|