[FEATURE] download_individually for youtube playlists (#102)

This commit is contained in:
Jesse Bannon 2022-07-17 22:02:50 -07:00 committed by GitHub
parent 8e35c1674b
commit 605de74697
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 45 deletions

View file

@ -1,10 +1,12 @@
from typing import Dict
from typing import List
from typing import Generator
from typing import Optional
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloaderOptions
from ytdl_sub.entries.youtube import YoutubePlaylistVideo
from ytdl_sub.validators.url_validator import YoutubePlaylistUrlValidator
from ytdl_sub.validators.validators import BoolValidator
class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
@ -21,15 +23,21 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
# required
download_strategy: "playlist"
playlist_url: "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
# optional
download_individually: True
"""
_required_keys = {"playlist_url"}
_optional_keys = {"download_individually"}
def __init__(self, name, value):
super().__init__(name, value)
self._playlist_url = self._validate_key(
"playlist_url", YoutubePlaylistUrlValidator
).playlist_url
self._download_individually = self._validate_key_if_present(
"download_individually", BoolValidator, default=True
)
@property
def playlist_url(self) -> str:
@ -39,6 +47,15 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
"""
return self._playlist_url
@property
def download_individually(self) -> Optional[bool]:
"""
Optional. Downloads files from the playlist individually instead of in bulk. Setting to True
is safer when downloading large amounts of videos in case an error occurs. Downloading by
bulk (by setting to False) can increase speeds. Defaults to True.
"""
return self._download_individually.value
class YoutubePlaylistDownloader(
YoutubeDownloader[YoutubePlaylistDownloaderOptions, YoutubePlaylistVideo]
@ -65,19 +82,36 @@ class YoutubePlaylistDownloader(
# pylint: enable=line-too-long
def download(self) -> List[YoutubePlaylistVideo]:
def download(self) -> Generator[YoutubePlaylistVideo, None, None]:
"""
Downloads all videos in a Youtube playlist
"""
playlist_videos: List[YoutubePlaylistVideo] = []
ytdl_options_overrides = {}
# If downloading individually, dry-run the entire channel download first, this will get the
# videos that will be downloaded. Afterwards, download each video one-by-one
if self.download_options.download_individually:
ytdl_options_overrides["skip_download"] = True
entry_dicts = self.extract_info_via_info_json(
ytdl_options_overrides=ytdl_options_overrides, url=self.download_options.playlist_url
)
# If downloading individually, remove the skip_download to actually download the video
if self.download_options.download_individually:
del ytdl_options_overrides["skip_download"]
entry_dicts = self.extract_info_via_info_json(url=self.download_options.playlist_url)
for entry_dict in entry_dicts:
if entry_dict.get("extractor") == "youtube":
playlist_videos.append(
YoutubePlaylistVideo(
entry_dict=entry_dict, working_directory=self.working_directory
)
)
return playlist_videos
# Only do the individual download if it is not dry-run and downloading individually
if not self.is_dry_run and self.download_options.download_individually:
ytdl_options_overrides["playlist_items"] = str(entry_dict.get("playlist_index"))
_ = self.extract_info(
ytdl_options_overrides=ytdl_options_overrides,
url=self.download_options.playlist_url,
)
yield YoutubePlaylistVideo(
entry_dict=entry_dict, working_directory=self.working_directory
)

View file

@ -8,12 +8,11 @@ from e2e.expected_download import ExpectedDownloads
from e2e.expected_transaction_log import assert_transaction_log_matches
import ytdl_sub.downloaders.downloader
from ytdl_sub.config.preset import Preset
from ytdl_sub.subscriptions.subscription import Subscription
@pytest.fixture
def subscription_dict(output_directory):
def playlist_preset_dict(output_directory):
return {
"preset": "yt_music_video_playlist",
"youtube": {"playlist_url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35"},
@ -31,20 +30,6 @@ def subscription_dict(output_directory):
# PLAYLIST FIXTURES
@pytest.fixture
def playlist_subscription(music_video_config, subscription_dict):
playlist_preset = Preset.from_dict(
config=music_video_config,
preset_name="music_video_playlist_test",
preset_dict=subscription_dict,
)
return Subscription.from_preset(
preset=playlist_preset,
config=music_video_config,
)
@pytest.fixture
def expected_playlist_download():
# turn off black formatter here for readability
@ -74,11 +59,11 @@ def expected_playlist_download():
####################################################################################################
# SINGLE VIDEO FIXTURES
@pytest.fixture
def single_video_subscription_dict(subscription_dict):
del subscription_dict["youtube"]
def single_video_preset_dict(playlist_preset_dict):
del playlist_preset_dict["youtube"]
return mergedeep.merge(
subscription_dict,
playlist_preset_dict,
{
"preset": "yt_music_video",
"youtube": {"video_url": "https://youtube.com/watch?v=HKTNxEqsN3Q"},
@ -86,20 +71,6 @@ def single_video_subscription_dict(subscription_dict):
)
@pytest.fixture
def single_video_subscription(music_video_config, single_video_subscription_dict):
single_video_preset = Preset.from_dict(
config=music_video_config,
preset_name="music_video_single_video_test",
preset_dict=single_video_subscription_dict,
)
return Subscription.from_preset(
preset=single_video_preset,
config=music_video_config,
)
@pytest.fixture
def expected_single_video_download():
# turn off black formatter here for readability
@ -121,9 +92,24 @@ class TestPlaylistAsKodiMusicVideo:
"""
@pytest.mark.parametrize("dry_run", [True, False])
@pytest.mark.parametrize("download_individually", [True, False])
def test_playlist_download(
self, playlist_subscription, expected_playlist_download, output_directory, dry_run
self,
music_video_config,
playlist_preset_dict,
expected_playlist_download,
output_directory,
dry_run,
download_individually,
):
playlist_preset_dict["youtube"]["download_individually"] = download_individually
playlist_subscription = Subscription.from_dict(
config=music_video_config,
preset_name="music_video_playlist_test",
preset_dict=playlist_preset_dict,
)
transaction_log = playlist_subscription.download()
assert_transaction_log_matches(
output_directory=output_directory,
@ -144,8 +130,19 @@ class TestPlaylistAsKodiMusicVideo:
@pytest.mark.parametrize("dry_run", [True, False])
def test_single_video_download(
self, single_video_subscription, expected_single_video_download, output_directory, dry_run
self,
music_video_config,
single_video_preset_dict,
expected_single_video_download,
output_directory,
dry_run,
):
single_video_subscription = Subscription.from_dict(
config=music_video_config,
preset_name="music_video_single_video_test",
preset_dict=single_video_preset_dict,
)
transaction_log = single_video_subscription.download()
assert_transaction_log_matches(
output_directory=output_directory,