almost working, need to deal with track_title and other overwrites
This commit is contained in:
parent
8045c420a2
commit
b1d7312a77
4 changed files with 147 additions and 19 deletions
|
|
@ -4,6 +4,7 @@ from typing import Type
|
||||||
|
|
||||||
from ytdl_sub.downloaders.downloader import Downloader
|
from ytdl_sub.downloaders.downloader import Downloader
|
||||||
from ytdl_sub.downloaders.soundcloud_downloader import SoundcloudAlbumsAndSinglesDownloader
|
from ytdl_sub.downloaders.soundcloud_downloader import SoundcloudAlbumsAndSinglesDownloader
|
||||||
|
from ytdl_sub.downloaders.youtube.split_video import YoutubeSplitVideoDownloader
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubeChannelDownloader
|
from ytdl_sub.downloaders.youtube_downloader import YoutubeChannelDownloader
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubePlaylistDownloader
|
from ytdl_sub.downloaders.youtube_downloader import YoutubePlaylistDownloader
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloader
|
from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloader
|
||||||
|
|
@ -23,6 +24,7 @@ class DownloadStrategyMapping:
|
||||||
"video": YoutubeVideoDownloader,
|
"video": YoutubeVideoDownloader,
|
||||||
"playlist": YoutubePlaylistDownloader,
|
"playlist": YoutubePlaylistDownloader,
|
||||||
"channel": YoutubeChannelDownloader,
|
"channel": YoutubeChannelDownloader,
|
||||||
|
"split_video": YoutubeSplitVideoDownloader,
|
||||||
},
|
},
|
||||||
"soundcloud": {
|
"soundcloud": {
|
||||||
"albums_and_singles": SoundcloudAlbumsAndSinglesDownloader,
|
"albums_and_singles": SoundcloudAlbumsAndSinglesDownloader,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import copy
|
import copy
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import copyfile
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
@ -8,8 +10,10 @@ from typing import Tuple
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubeDownloader
|
from ytdl_sub.downloaders.youtube_downloader import YoutubeDownloader
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloaderOptions
|
from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloaderOptions
|
||||||
from ytdl_sub.entries.youtube import YoutubePlaylistVideo
|
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.exceptions import ValidationException
|
||||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||||
|
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
|
||||||
from ytdl_sub.validators.validators import StringValidator
|
from ytdl_sub.validators.validators import StringValidator
|
||||||
|
|
||||||
# Captures the following formats:
|
# Captures the following formats:
|
||||||
|
|
@ -112,7 +116,7 @@ class YoutubeSplitVideoDownloaderOptions(YoutubeVideoDownloaderOptions):
|
||||||
end at Youtube video's ending.
|
end at Youtube video's ending.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_required_keys = super()._required_keys.union("split_timestamps")
|
_required_keys = {"video_url", "split_timestamps"}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
@ -155,32 +159,42 @@ class YoutubeSplitVideoDownloader(
|
||||||
split_timestamp_path=self.download_options.split_timestamps
|
split_timestamp_path=self.download_options.split_timestamps
|
||||||
)
|
)
|
||||||
entry_dict = self.extract_info(url=self.download_options.video_url)
|
entry_dict = self.extract_info(url=self.download_options.video_url)
|
||||||
uid = entry_dict["id"]
|
|
||||||
ext = entry_dict["ext"]
|
|
||||||
|
|
||||||
idx = 0
|
entry = YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory)
|
||||||
for timestamp, title in timestamp_titles:
|
# convert the entry thumbnail early so we do not have to guess the thumbnail extension
|
||||||
|
# when copying it
|
||||||
|
convert_download_thumbnail(entry=entry)
|
||||||
|
|
||||||
|
for idx, timestamp_title in enumerate(timestamp_titles):
|
||||||
|
timestamp_begin, title = timestamp_title
|
||||||
|
timestamp_end = timestamp_titles[idx + 1][0] if idx + 1 < len(timestamp_titles) else ""
|
||||||
|
|
||||||
|
new_uid = f"{entry.uid}___{idx}"
|
||||||
|
|
||||||
|
input_file = entry.get_download_file_path()
|
||||||
|
output_file = str(Path(self.working_directory) / f"{new_uid}.{entry.ext}")
|
||||||
|
output_thumbnail_file = str(
|
||||||
|
Path(self.working_directory) / f"{new_uid}.{entry.thumbnail_ext}"
|
||||||
|
)
|
||||||
|
|
||||||
entry_dict_ = copy.deepcopy(entry_dict)
|
entry_dict_ = copy.deepcopy(entry_dict)
|
||||||
new_uid = f"{uid}___{idx}"
|
|
||||||
entry_dict_["title"] = title
|
entry_dict_["title"] = title
|
||||||
entry_dict_["playlist_index"] = idx + 1
|
entry_dict_["playlist_index"] = idx + 1
|
||||||
entry_dict_["playlist_count"] = len(timestamp_titles)
|
entry_dict_["playlist_count"] = len(timestamp_titles)
|
||||||
entry_dict_["id"] = new_uid
|
entry_dict_["id"] = new_uid
|
||||||
|
|
||||||
timestamp_begin_arg = f"-ss {timestamp}"
|
cmd = ["-i", input_file, "-ss", timestamp_begin]
|
||||||
timestamp_end_arg = (
|
if timestamp_end:
|
||||||
f"-to {timestamp_titles[idx + 1][0]}" if idx + 1 < len(timestamp_titles) else ""
|
cmd += ["-to", timestamp_end]
|
||||||
)
|
cmd += ["-vcodec", "copy", "-acodec", "copy", output_file]
|
||||||
|
|
||||||
FFMPEG.run(
|
FFMPEG.run(cmd)
|
||||||
f"-i {uid}.{ext} "
|
|
||||||
f"{timestamp_begin_arg} {timestamp_end_arg} "
|
copyfile(src=entry.get_download_thumbnail_path(), dst=output_thumbnail_file)
|
||||||
f"-vcodec copy -acodec copy {new_uid}.{ext}"
|
|
||||||
)
|
|
||||||
|
|
||||||
split_videos.append(
|
split_videos.append(
|
||||||
YoutubePlaylistVideo(
|
YoutubePlaylistVideo(
|
||||||
entry_dict=entry_dict, working_directory=self.working_directory
|
entry_dict=entry_dict_, working_directory=self.working_directory
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from ytdl_sub.utils.exceptions import ValidationException
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
|
|
@ -17,7 +18,7 @@ class FFMPEG:
|
||||||
) from subprocess_error
|
) from subprocess_error
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls, ffmpeg_args: str) -> None:
|
def run(cls, ffmpeg_args: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Runs an ffmpeg command. Should not include 'ffmpeg' as the beginning argument.
|
Runs an ffmpeg command. Should not include 'ffmpeg' as the beginning argument.
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ class FFMPEG:
|
||||||
"""
|
"""
|
||||||
cls._ensure_installed()
|
cls._ensure_installed()
|
||||||
|
|
||||||
cmd = f"ffmpeg {ffmpeg_args}"
|
cmd = ["ffmpeg"]
|
||||||
logger.debug("Running %s", cmd)
|
cmd.extend(ffmpeg_args)
|
||||||
|
logger.debug("Running %s", " ".join(cmd))
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
|
|
|
||||||
110
tests/e2e/youtube/test_split_video.py
Normal file
110
tests/e2e/youtube/test_split_video.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
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 split_timestamps_file_path():
|
||||||
|
timestamps = [
|
||||||
|
"0:00 Intro\n",
|
||||||
|
"00:15 Part 1\n",
|
||||||
|
"1:01 Part 2\n",
|
||||||
|
"01:24 Part 3\n",
|
||||||
|
"0:02:01 Part 4\n",
|
||||||
|
"00:02:33 Part 5\n",
|
||||||
|
]
|
||||||
|
|
||||||
|
with NamedTemporaryFile(mode="w", encoding="utf-8", suffix=".txt") as tmp:
|
||||||
|
tmp.writelines(timestamps)
|
||||||
|
tmp.seek(0)
|
||||||
|
yield tmp.name
|
||||||
|
|
||||||
|
|
||||||
|
@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, split_timestamps_file_path):
|
||||||
|
return {
|
||||||
|
"preset": "yt_music_video",
|
||||||
|
"youtube": {
|
||||||
|
"download_strategy": "split_video",
|
||||||
|
"video_url": "https://youtube.com/watch?v=HKTNxEqsN3Q",
|
||||||
|
"split_timestamps": split_timestamps_file_path,
|
||||||
|
},
|
||||||
|
# override the output directory with our fixture-generated dir
|
||||||
|
"output_options": {
|
||||||
|
"output_directory": output_directory,
|
||||||
|
"file_name": "{playlist_index}.{title_sanitized}.{ext}",
|
||||||
|
},
|
||||||
|
# download the worst format so it is fast
|
||||||
|
"ytdl_options": {
|
||||||
|
"format": "worst[ext=mp4]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
# SINGLE VIDEO FIXTURES
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def single_video_subscription(config, subscription_name, subscription_dict):
|
||||||
|
single_video_preset = Preset.from_dict(
|
||||||
|
config=config,
|
||||||
|
preset_name=subscription_name,
|
||||||
|
preset_dict=subscription_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Subscription.from_preset(
|
||||||
|
preset=single_video_preset,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def expected_single_video_download():
|
||||||
|
# turn off black formatter here for readability
|
||||||
|
# fmt: off
|
||||||
|
return ExpectedDownload(
|
||||||
|
expected_md5_file_hashes={
|
||||||
|
Path("JMC - Whale & Wasp.jpg"): "b58377dfe7c39527e1990a24b36bbd77",
|
||||||
|
Path("JMC - Whale & Wasp.mp4"): "931a705864c57d21d6fedebed4af6bbc",
|
||||||
|
Path("JMC - Whale & Wasp.nfo"): "6c2f085adb847c1dcc47c19514c454d8",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlaylistAsKodiMusicVideo:
|
||||||
|
"""
|
||||||
|
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_single_video_download(
|
||||||
|
self, single_video_subscription, expected_single_video_download, output_directory
|
||||||
|
):
|
||||||
|
single_video_subscription.download()
|
||||||
|
expected_single_video_download.assert_files_exist(relative_directory=output_directory)
|
||||||
Loading…
Reference in a new issue