[FEATURE] download_individually for youtube playlists (#102)
This commit is contained in:
parent
8e35c1674b
commit
605de74697
2 changed files with 76 additions and 45 deletions
|
|
@ -1,10 +1,12 @@
|
||||||
from typing import Dict
|
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 YoutubeDownloader
|
||||||
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloaderOptions
|
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloaderOptions
|
||||||
from ytdl_sub.entries.youtube import YoutubePlaylistVideo
|
from ytdl_sub.entries.youtube import YoutubePlaylistVideo
|
||||||
from ytdl_sub.validators.url_validator import YoutubePlaylistUrlValidator
|
from ytdl_sub.validators.url_validator import YoutubePlaylistUrlValidator
|
||||||
|
from ytdl_sub.validators.validators import BoolValidator
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
|
class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
|
||||||
|
|
@ -21,15 +23,21 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
|
||||||
# required
|
# required
|
||||||
download_strategy: "playlist"
|
download_strategy: "playlist"
|
||||||
playlist_url: "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
|
playlist_url: "https://www.youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
|
||||||
|
# optional
|
||||||
|
download_individually: True
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_required_keys = {"playlist_url"}
|
_required_keys = {"playlist_url"}
|
||||||
|
_optional_keys = {"download_individually"}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self._playlist_url = self._validate_key(
|
self._playlist_url = self._validate_key(
|
||||||
"playlist_url", YoutubePlaylistUrlValidator
|
"playlist_url", YoutubePlaylistUrlValidator
|
||||||
).playlist_url
|
).playlist_url
|
||||||
|
self._download_individually = self._validate_key_if_present(
|
||||||
|
"download_individually", BoolValidator, default=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def playlist_url(self) -> str:
|
def playlist_url(self) -> str:
|
||||||
|
|
@ -39,6 +47,15 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
|
||||||
"""
|
"""
|
||||||
return self._playlist_url
|
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(
|
class YoutubePlaylistDownloader(
|
||||||
YoutubeDownloader[YoutubePlaylistDownloaderOptions, YoutubePlaylistVideo]
|
YoutubeDownloader[YoutubePlaylistDownloaderOptions, YoutubePlaylistVideo]
|
||||||
|
|
@ -65,19 +82,36 @@ class YoutubePlaylistDownloader(
|
||||||
|
|
||||||
# pylint: enable=line-too-long
|
# pylint: enable=line-too-long
|
||||||
|
|
||||||
def download(self) -> List[YoutubePlaylistVideo]:
|
def download(self) -> Generator[YoutubePlaylistVideo, None, None]:
|
||||||
"""
|
"""
|
||||||
Downloads all videos in a Youtube playlist
|
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:
|
for entry_dict in entry_dicts:
|
||||||
if entry_dict.get("extractor") == "youtube":
|
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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,11 @@ from e2e.expected_download import ExpectedDownloads
|
||||||
from e2e.expected_transaction_log import assert_transaction_log_matches
|
from e2e.expected_transaction_log import assert_transaction_log_matches
|
||||||
|
|
||||||
import ytdl_sub.downloaders.downloader
|
import ytdl_sub.downloaders.downloader
|
||||||
from ytdl_sub.config.preset import Preset
|
|
||||||
from ytdl_sub.subscriptions.subscription import Subscription
|
from ytdl_sub.subscriptions.subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def subscription_dict(output_directory):
|
def playlist_preset_dict(output_directory):
|
||||||
return {
|
return {
|
||||||
"preset": "yt_music_video_playlist",
|
"preset": "yt_music_video_playlist",
|
||||||
"youtube": {"playlist_url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35"},
|
"youtube": {"playlist_url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35"},
|
||||||
|
|
@ -31,20 +30,6 @@ def subscription_dict(output_directory):
|
||||||
# PLAYLIST FIXTURES
|
# 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
|
@pytest.fixture
|
||||||
def expected_playlist_download():
|
def expected_playlist_download():
|
||||||
# turn off black formatter here for readability
|
# turn off black formatter here for readability
|
||||||
|
|
@ -74,11 +59,11 @@ def expected_playlist_download():
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
# SINGLE VIDEO FIXTURES
|
# SINGLE VIDEO FIXTURES
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def single_video_subscription_dict(subscription_dict):
|
def single_video_preset_dict(playlist_preset_dict):
|
||||||
del subscription_dict["youtube"]
|
del playlist_preset_dict["youtube"]
|
||||||
|
|
||||||
return mergedeep.merge(
|
return mergedeep.merge(
|
||||||
subscription_dict,
|
playlist_preset_dict,
|
||||||
{
|
{
|
||||||
"preset": "yt_music_video",
|
"preset": "yt_music_video",
|
||||||
"youtube": {"video_url": "https://youtube.com/watch?v=HKTNxEqsN3Q"},
|
"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
|
@pytest.fixture
|
||||||
def expected_single_video_download():
|
def expected_single_video_download():
|
||||||
# turn off black formatter here for readability
|
# turn off black formatter here for readability
|
||||||
|
|
@ -121,9 +92,24 @@ class TestPlaylistAsKodiMusicVideo:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
@pytest.mark.parametrize("dry_run", [True, False])
|
||||||
|
@pytest.mark.parametrize("download_individually", [True, False])
|
||||||
def test_playlist_download(
|
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()
|
transaction_log = playlist_subscription.download()
|
||||||
assert_transaction_log_matches(
|
assert_transaction_log_matches(
|
||||||
output_directory=output_directory,
|
output_directory=output_directory,
|
||||||
|
|
@ -144,8 +130,19 @@ class TestPlaylistAsKodiMusicVideo:
|
||||||
|
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
@pytest.mark.parametrize("dry_run", [True, False])
|
||||||
def test_single_video_download(
|
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()
|
transaction_log = single_video_subscription.download()
|
||||||
assert_transaction_log_matches(
|
assert_transaction_log_matches(
|
||||||
output_directory=output_directory,
|
output_directory=output_directory,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue