From fc7a234f687013fe0c6ea3622c0bc9ec592225c5 Mon Sep 17 00:00:00 2001 From: jbannon Date: Mon, 20 Jun 2022 06:40:26 +0000 Subject: [PATCH] begin --- src/ytdl_sub/config/preset_class_mappings.py | 2 + .../downloaders/youtube/merge_playlist.py | 106 ++++++++++++++++++ tests/e2e/youtube/test_merge_playlist.py | 103 +++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/ytdl_sub/downloaders/youtube/merge_playlist.py create mode 100644 tests/e2e/youtube/test_merge_playlist.py diff --git a/src/ytdl_sub/config/preset_class_mappings.py b/src/ytdl_sub/config/preset_class_mappings.py index 73a668d5..af42b1c9 100644 --- a/src/ytdl_sub/config/preset_class_mappings.py +++ b/src/ytdl_sub/config/preset_class_mappings.py @@ -4,6 +4,7 @@ from typing import Type from ytdl_sub.downloaders.downloader import Downloader from ytdl_sub.downloaders.soundcloud_downloader import SoundcloudAlbumsAndSinglesDownloader +from ytdl_sub.downloaders.youtube.merge_playlist import YoutubeMergePlaylistDownloader from ytdl_sub.downloaders.youtube.split_video import YoutubeSplitVideoDownloader from ytdl_sub.downloaders.youtube_downloader import YoutubeChannelDownloader from ytdl_sub.downloaders.youtube_downloader import YoutubePlaylistDownloader @@ -25,6 +26,7 @@ class DownloadStrategyMapping: "playlist": YoutubePlaylistDownloader, "channel": YoutubeChannelDownloader, "split_video": YoutubeSplitVideoDownloader, + "merge_playlist": YoutubeMergePlaylistDownloader, }, "soundcloud": { "albums_and_singles": SoundcloudAlbumsAndSinglesDownloader, diff --git a/src/ytdl_sub/downloaders/youtube/merge_playlist.py b/src/ytdl_sub/downloaders/youtube/merge_playlist.py new file mode 100644 index 00000000..b61e873e --- /dev/null +++ b/src/ytdl_sub/downloaders/youtube/merge_playlist.py @@ -0,0 +1,106 @@ +import copy +import os.path +import re +from pathlib import Path +from shutil import copyfile +from typing import Dict +from typing import List +from typing import Tuple + +from ytdl_sub.downloaders.youtube_downloader import YoutubeDownloader, \ + YoutubePlaylistDownloaderOptions +from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloaderOptions +from ytdl_sub.entries.youtube import YoutubePlaylistVideo +from ytdl_sub.entries.youtube import YoutubeVideo +from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.ffmpeg import FFMPEG +from ytdl_sub.utils.thumbnail import convert_download_thumbnail +from ytdl_sub.validators.validators import StringValidator + +############################################################################### +# Youtube split video downloader + options + + +class YoutubeMergePlaylistDownloaderOptions(YoutubePlaylistDownloaderOptions): + """ + 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: "TODO" + chapter_name: "{title}" + + CLI usage: + + .. code-block:: bash + + ytdl-sub dl \ + --preset "example_preset" \ + --youtube.video_url "youtube.com/watch?v=VMAPTo7RVDo" \ + --youtube.split_timestamps "path/to/timestamps.txt" + + ``split_timestamps`` file format: + + .. code-block:: markdown + + 0:00 Intro + 0:24 Blackwater Park + 10:23 Bleak + 16:39 Jokes + 1:02:23 Ending + + The above will create 5 videos in total. The first timestamp must start with ``0:00`` + and the last timestamp, in this example, would create a video starting at ``1:02:23`` and + end at Youtube video's ending. + """ + + _required_keys = {"playlist_url"} + + # def __init__(self, name, value): + # super().__init__(name, value) + + +class YoutubeMergePlaylistDownloader( + YoutubeDownloader[YoutubeMergePlaylistDownloaderOptions, YoutubeVideo] +): + downloader_options_type = YoutubeMergePlaylistDownloaderOptions + downloader_entry_type = YoutubeVideo + + @classmethod + def ytdl_option_defaults(cls) -> Dict: + """ + Default `ytdl_options`_ for ``split_video`` + + .. code-block:: yaml + + ytdl_options: + ignoreerrors: True # ignore errors like hidden videos, age restriction, etc + """ + return dict( + super().ytdl_option_defaults(), + **{"postprocessors": [ + { + "key": "FFmpegVideoRemuxer", + "when": "post_process", + "preferedformat": "mkv", + }, + { + "key": "FFmpegConcat", + "when": "playlist", + } + ]}, + ) + + def download(self) -> List[YoutubeVideo]: + """Download a single Youtube video, then split it into multiple videos""" + split_videos: List[YoutubePlaylistVideo] = [] + entry_dict = self.extract_info(url=self.download_options.playlist_url) + + return [] diff --git a/tests/e2e/youtube/test_merge_playlist.py b/tests/e2e/youtube/test_merge_playlist.py new file mode 100644 index 00000000..d9e7a950 --- /dev/null +++ b/tests/e2e/youtube/test_merge_playlist.py @@ -0,0 +1,103 @@ +from pathlib import Path + +import mergedeep +import pytest +from conftest import assert_debug_log +from e2e.expected_download import ExpectedDownload + +import ytdl_sub.downloaders.downloader +from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.config.preset import Preset +from ytdl_sub.subscriptions.subscription import Subscription + + +@pytest.fixture +def config_path(): + return "examples/kodi_music_videos_config.yaml" + + +@pytest.fixture +def subscription_name(): + return "jmc" + + +@pytest.fixture +def config(config_path): + return ConfigFile.from_file_path(config_path=config_path) + + +@pytest.fixture +def subscription_dict(output_directory, subscription_name): + return { + "preset": "yt_music_video_playlist", + "youtube": { + "download_strategy": "merge_playlist", + "playlist_url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35" + }, + # override the output directory with our fixture-generated dir + "output_options": {"output_directory": output_directory}, + # download the worst format so it is fast + "ytdl_options": { + "format": "worst", + }, + "overrides": {"artist": "JMC"}, + } + + +#################################################################################################### +# PLAYLIST FIXTURES + + +@pytest.fixture +def playlist_subscription(config, subscription_name, subscription_dict): + playlist_preset = Preset.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=subscription_dict, + ) + + return Subscription.from_preset( + preset=playlist_preset, + config=config, + ) + + +@pytest.fixture +def expected_playlist_download(): + # turn off black formatter here for readability + # fmt: off + return ExpectedDownload( + expected_md5_file_hashes={ + # Download mapping + Path(".ytdl-sub-jmc-download-archive.json"): "7541aa75606b86bff5ff276895520cf0", + + # Entry files + Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].jpg"): "048a19cf0f674437351872c3f312ebf1", + Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4"): "e66287b9832277b6a4d1554e29d9fdcc", + Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo"): "3d272fe58487b6011ad049b6000b046f", + + Path("JMC - Given to Fly.jpg"): "2e58e4d5f06ce5d1c3336fa493470135", + Path("JMC - Given to Fly.mp4"): "04ab5cb3cc12325d0c96a7cd04a8b91d", + Path("JMC - Given to Fly.nfo"): "0dc578bf5f1ceb6e069a57d329894f35", + + Path("JMC - Indifference (Remastered).jpg"): "9baaddc6b62f5b9ae3781eb4eef0e3b3", + Path("JMC - Indifference (Remastered).mp4"): "025de6099a5c98e6397153c7a62d517d", + Path("JMC - Indifference (Remastered).nfo"): "061b86d9dc8fb39d39feab3292dafeb0", + } + ) + # fmt: on + + + +class TestYoutubeMergePlaylist: + """ + Downloads my old minecraft youtube channel, pretends they are music videos. Ensure the above + files exist and have the expected md5 file hashes. + """ + + def test_playlist_download( + self, playlist_subscription, expected_playlist_download, output_directory + ): + playlist_subscription.download() + expected_playlist_download.assert_files_exist(relative_directory=output_directory) +