working test
This commit is contained in:
parent
b1d7312a77
commit
9802eb202a
2 changed files with 84 additions and 35 deletions
|
|
@ -25,9 +25,13 @@ from ytdl_sub.validators.validators import StringValidator
|
||||||
_SPLIT_TIMESTAMP_REGEX = re.compile(r"^((?:\d\d:)?(?:\d:)?(?:\d)?\d:\d\d) (.+)$")
|
_SPLIT_TIMESTAMP_REGEX = re.compile(r"^((?:\d\d:)?(?:\d:)?(?:\d)?\d:\d\d) (.+)$")
|
||||||
|
|
||||||
|
|
||||||
def _parse_split_timestamp_file(split_timestamp_path: str) -> List[Tuple[str, str]]:
|
def _split_video_uid(source_uid: str, idx: int) -> str:
|
||||||
|
return f"{source_uid}___{idx}"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_split_timestamp_file(split_timestamp_path: str) -> Tuple[List[str], List[str]]:
|
||||||
"""
|
"""
|
||||||
Returns list of (00:00:00 title), each timestamp as HH:MM:SS
|
Returns two lists, one containing timestamps in HH:MM:SS format, and the other titles
|
||||||
"""
|
"""
|
||||||
if not os.path.isfile(split_timestamp_path):
|
if not os.path.isfile(split_timestamp_path):
|
||||||
raise ValidationException(
|
raise ValidationException(
|
||||||
|
|
@ -37,7 +41,8 @@ def _parse_split_timestamp_file(split_timestamp_path: str) -> List[Tuple[str, st
|
||||||
with open(split_timestamp_path, "r", encoding="utf-8") as file:
|
with open(split_timestamp_path, "r", encoding="utf-8") as file:
|
||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
|
|
||||||
timestamp_titles: List[Tuple[str, str]] = []
|
timestamps: List[str] = []
|
||||||
|
titles: List[str] = []
|
||||||
idx = 0
|
idx = 0
|
||||||
for idx, line in enumerate(lines):
|
for idx, line in enumerate(lines):
|
||||||
match = _SPLIT_TIMESTAMP_REGEX.match(line)
|
match = _SPLIT_TIMESTAMP_REGEX.match(line)
|
||||||
|
|
@ -57,7 +62,8 @@ def _parse_split_timestamp_file(split_timestamp_path: str) -> List[Tuple[str, st
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert len(timestamp) == 8
|
assert len(timestamp) == 8
|
||||||
timestamp_titles.append((timestamp, title))
|
timestamps.append(timestamp)
|
||||||
|
titles.append(title)
|
||||||
|
|
||||||
if idx not in (len(lines) - 1, len(lines) - 2):
|
if idx not in (len(lines) - 1, len(lines) - 2):
|
||||||
raise ValidationException(
|
raise ValidationException(
|
||||||
|
|
@ -65,7 +71,20 @@ def _parse_split_timestamp_file(split_timestamp_path: str) -> List[Tuple[str, st
|
||||||
f"Each line must be formatted as '0:00 title' - a timestamp, space, then title."
|
f"Each line must be formatted as '0:00 title' - a timestamp, space, then title."
|
||||||
)
|
)
|
||||||
|
|
||||||
return timestamp_titles
|
return timestamps, titles
|
||||||
|
|
||||||
|
|
||||||
|
def _split_video_ffmpeg_cmd(
|
||||||
|
input_file: str, output_file: str, timestamps: List[str], idx: int
|
||||||
|
) -> List[str]:
|
||||||
|
timestamp_begin = timestamps[idx]
|
||||||
|
timestamp_end = timestamps[idx + 1] if idx + 1 < len(timestamps) else ""
|
||||||
|
|
||||||
|
cmd = ["-i", input_file, "-ss", timestamp_begin]
|
||||||
|
if timestamp_end:
|
||||||
|
cmd += ["-to", timestamp_end]
|
||||||
|
cmd += ["-vcodec", "copy", "-acodec", "copy", output_file]
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
@ -151,11 +170,29 @@ class YoutubeSplitVideoDownloader(
|
||||||
**{"break_on_existing": True},
|
**{"break_on_existing": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_split_video_entry(
|
||||||
|
self, source_entry_dict: Dict, title: str, idx: int, split_video_count: int
|
||||||
|
) -> YoutubePlaylistVideo:
|
||||||
|
"""
|
||||||
|
Runs ffmpeg to create the split video
|
||||||
|
"""
|
||||||
|
entry_dict = copy.deepcopy(source_entry_dict)
|
||||||
|
entry_dict["title"] = title
|
||||||
|
entry_dict["playlist_index"] = idx + 1
|
||||||
|
entry_dict["playlist_count"] = split_video_count
|
||||||
|
entry_dict["id"] = _split_video_uid(source_uid=entry_dict["id"], idx=idx)
|
||||||
|
|
||||||
|
# Remove track and artist since its now split
|
||||||
|
del entry_dict["track"]
|
||||||
|
del entry_dict["artist"]
|
||||||
|
|
||||||
|
return YoutubePlaylistVideo(entry_dict=entry_dict, working_directory=self.working_directory)
|
||||||
|
|
||||||
def download(self) -> List[YoutubePlaylistVideo]:
|
def download(self) -> List[YoutubePlaylistVideo]:
|
||||||
"""Download a single Youtube video, then split it into multiple videos"""
|
"""Download a single Youtube video, then split it into multiple videos"""
|
||||||
split_videos: List[YoutubePlaylistVideo] = []
|
split_videos: List[YoutubePlaylistVideo] = []
|
||||||
|
|
||||||
timestamp_titles = _parse_split_timestamp_file(
|
timestamps, titles = _parse_split_timestamp_file(
|
||||||
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)
|
||||||
|
|
@ -165,36 +202,32 @@ class YoutubeSplitVideoDownloader(
|
||||||
# when copying it
|
# when copying it
|
||||||
convert_download_thumbnail(entry=entry)
|
convert_download_thumbnail(entry=entry)
|
||||||
|
|
||||||
for idx, timestamp_title in enumerate(timestamp_titles):
|
for idx, title in enumerate(titles):
|
||||||
timestamp_begin, title = timestamp_title
|
new_uid = _split_video_uid(source_uid=entry.uid, idx=idx)
|
||||||
timestamp_end = timestamp_titles[idx + 1][0] if idx + 1 < len(timestamp_titles) else ""
|
|
||||||
|
|
||||||
new_uid = f"{entry.uid}___{idx}"
|
|
||||||
|
|
||||||
|
# Get the input/output file paths
|
||||||
input_file = entry.get_download_file_path()
|
input_file = entry.get_download_file_path()
|
||||||
output_file = str(Path(self.working_directory) / f"{new_uid}.{entry.ext}")
|
output_file = str(Path(self.working_directory) / f"{new_uid}.{entry.ext}")
|
||||||
output_thumbnail_file = str(
|
output_thumbnail_file = str(
|
||||||
Path(self.working_directory) / f"{new_uid}.{entry.thumbnail_ext}"
|
Path(self.working_directory) / f"{new_uid}.{entry.thumbnail_ext}"
|
||||||
)
|
)
|
||||||
|
|
||||||
entry_dict_ = copy.deepcopy(entry_dict)
|
# Run ffmpeg to create the split the video
|
||||||
entry_dict_["title"] = title
|
FFMPEG.run(
|
||||||
entry_dict_["playlist_index"] = idx + 1
|
_split_video_ffmpeg_cmd(
|
||||||
entry_dict_["playlist_count"] = len(timestamp_titles)
|
input_file=input_file, output_file=output_file, timestamps=timestamps, idx=idx
|
||||||
entry_dict_["id"] = new_uid
|
)
|
||||||
|
)
|
||||||
cmd = ["-i", input_file, "-ss", timestamp_begin]
|
# Copy the thumbnail
|
||||||
if timestamp_end:
|
|
||||||
cmd += ["-to", timestamp_end]
|
|
||||||
cmd += ["-vcodec", "copy", "-acodec", "copy", output_file]
|
|
||||||
|
|
||||||
FFMPEG.run(cmd)
|
|
||||||
|
|
||||||
copyfile(src=entry.get_download_thumbnail_path(), dst=output_thumbnail_file)
|
copyfile(src=entry.get_download_thumbnail_path(), dst=output_thumbnail_file)
|
||||||
|
|
||||||
|
# Format the split video as a YoutubePlaylistVideo
|
||||||
split_videos.append(
|
split_videos.append(
|
||||||
YoutubePlaylistVideo(
|
self._create_split_video_entry(
|
||||||
entry_dict=entry_dict_, working_directory=self.working_directory
|
source_entry_dict=entry_dict,
|
||||||
|
title=title,
|
||||||
|
idx=idx,
|
||||||
|
split_video_count=len(timestamps),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ def config_path():
|
||||||
def split_timestamps_file_path():
|
def split_timestamps_file_path():
|
||||||
timestamps = [
|
timestamps = [
|
||||||
"0:00 Intro\n",
|
"0:00 Intro\n",
|
||||||
"00:15 Part 1\n",
|
"00:10 Part 1\n",
|
||||||
"1:01 Part 2\n",
|
"0:20 Part 2\n",
|
||||||
"01:24 Part 3\n",
|
"00:30 Part 3\n",
|
||||||
"0:02:01 Part 4\n",
|
"0:00:40 Part 4\n",
|
||||||
"00:02:33 Part 5\n",
|
"00:01:01 Part 5\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
with NamedTemporaryFile(mode="w", encoding="utf-8", suffix=".txt") as tmp:
|
with NamedTemporaryFile(mode="w", encoding="utf-8", suffix=".txt") as tmp:
|
||||||
|
|
@ -56,7 +56,8 @@ def subscription_dict(output_directory, subscription_name, split_timestamps_file
|
||||||
# override the output directory with our fixture-generated dir
|
# override the output directory with our fixture-generated dir
|
||||||
"output_options": {
|
"output_options": {
|
||||||
"output_directory": output_directory,
|
"output_directory": output_directory,
|
||||||
"file_name": "{playlist_index}.{title_sanitized}.{ext}",
|
"file_name": "{channel_sanitized} - {playlist_index}-{playlist_size}"
|
||||||
|
".{title_sanitized}.{ext}",
|
||||||
},
|
},
|
||||||
# download the worst format so it is fast
|
# download the worst format so it is fast
|
||||||
"ytdl_options": {
|
"ytdl_options": {
|
||||||
|
|
@ -89,9 +90,24 @@ def expected_single_video_download():
|
||||||
# fmt: off
|
# fmt: off
|
||||||
return ExpectedDownload(
|
return ExpectedDownload(
|
||||||
expected_md5_file_hashes={
|
expected_md5_file_hashes={
|
||||||
Path("JMC - Whale & Wasp.jpg"): "b58377dfe7c39527e1990a24b36bbd77",
|
Path('Project Zombie - 1-6.Intro.mp4'): "eaec6f50f364b13ef1a201e736ec9c05",
|
||||||
Path("JMC - Whale & Wasp.mp4"): "931a705864c57d21d6fedebed4af6bbc",
|
Path('Project Zombie - 2-6.Part 1.mp4'): "5850b19acb250cc13db36f80fa1bba5a",
|
||||||
Path("JMC - Whale & Wasp.nfo"): "6c2f085adb847c1dcc47c19514c454d8",
|
Path('Project Zombie - 3-6.Part 2.mp4'): "445d95eba437db6df284df7e1ab633e8",
|
||||||
|
Path('Project Zombie - 4-6.Part 3.mp4'): "2b6e7532d515c9e64ed2a33d850cf199",
|
||||||
|
Path('Project Zombie - 5-6.Part 4.mp4'): "842bf3c4d1fcc4c5ab110635935dac66",
|
||||||
|
Path('Project Zombie - 6-6.Part 5.mp4'): "238de99f00f829ab72f042b79da9a33a",
|
||||||
|
Path('Project Zombie - Intro.jpg'): "e87282e4115baa8b5c727fb4de15316d",
|
||||||
|
Path('Project Zombie - Intro.nfo'): "ded59ac906f579312cc3cf98a57e7ea3",
|
||||||
|
Path('Project Zombie - Part 1.jpg'): "e87282e4115baa8b5c727fb4de15316d",
|
||||||
|
Path('Project Zombie - Part 1.nfo'): "70ff5cd0092b8bc22dc4db93a824789b",
|
||||||
|
Path('Project Zombie - Part 2.jpg'): "e87282e4115baa8b5c727fb4de15316d",
|
||||||
|
Path('Project Zombie - Part 2.nfo'): "54450c18a2cbb9d6d2ee5d0a1fb3f279",
|
||||||
|
Path('Project Zombie - Part 3.jpg'): "e87282e4115baa8b5c727fb4de15316d",
|
||||||
|
Path('Project Zombie - Part 3.nfo'): "0effb13fc4039363a95969d1048dde57",
|
||||||
|
Path('Project Zombie - Part 4.jpg'): "e87282e4115baa8b5c727fb4de15316d",
|
||||||
|
Path('Project Zombie - Part 4.nfo'): "74bd0d7c12105469838768a0cc323a8c",
|
||||||
|
Path('Project Zombie - Part 5.jpg'): "e87282e4115baa8b5c727fb4de15316d",
|
||||||
|
Path('Project Zombie - Part 5.nfo'): "a8cf2e77721335ea7c18e22734e7996c",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue