split chapters mess

This commit is contained in:
jbannon 2022-08-15 22:03:35 +00:00
parent cb37f5938c
commit 276522c139
6 changed files with 122 additions and 24 deletions

View file

@ -31,19 +31,6 @@ def _split_video_uid(source_uid: str, idx: int) -> str:
return f"{source_uid}___{idx}" return f"{source_uid}___{idx}"
def _split_video_ffmpeg_cmd(
input_file: str, output_file: str, timestamps: List[Timestamp], idx: int
) -> List[str]:
timestamp_begin = timestamps[idx].standardized_str
timestamp_end = timestamps[idx + 1].standardized_str 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
class YoutubeSplitVideoDownloaderOptions(YoutubeVideoDownloaderOptions): class YoutubeSplitVideoDownloaderOptions(YoutubeVideoDownloaderOptions):
r""" r"""
Downloads a single youtube video, then splits in to separate videos using a file containing Downloads a single youtube video, then splits in to separate videos using a file containing
@ -158,7 +145,7 @@ class YoutubeSplitVideoDownloader(
"""Download a single Youtube video, then split it into multiple videos""" """Download a single Youtube video, then split it into multiple videos"""
split_videos_and_metadata: List[Tuple[YoutubePlaylistVideo, FileMetadata]] = [] split_videos_and_metadata: List[Tuple[YoutubePlaylistVideo, FileMetadata]] = []
chapters = Chapters.from_file(chapters_file_path=self.download_options.split_timestamps) chapters = Chapters.from_timestamps_file(chapters_file_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)
entry = YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory) entry = YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory)

View file

@ -103,7 +103,7 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo
return [video] return [video]
# Otherwise, add the chapters and return the video + chapter metadata # Otherwise, add the chapters and return the video + chapter metadata
chapters = Chapters.from_file(chapters_file_path=self.download_options.chapter_timestamps) chapters = Chapters.from_timestamps_file(chapters_file_path=self.download_options.chapter_timestamps)
if not self.is_dry_run: if not self.is_dry_run:
set_ffmpeg_metadata_chapters( set_ffmpeg_metadata_chapters(
file_path=video.get_download_file_path(), file_path=video.get_download_file_path(),

View file

@ -1,10 +1,32 @@
from typing import Optional import copy
from pathlib import Path
from typing import Optional, List
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import Plugin
from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.plugins.plugin import PluginOptions
from ytdl_sub.utils.chapters import Chapters, Timestamp
from ytdl_sub.utils.ffmpeg import FFMPEG
from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.validators.string_select_validator import StringSelectValidator from ytdl_sub.validators.string_select_validator import StringSelectValidator
def _split_video_ffmpeg_cmd(
input_file: str, output_file: str, timestamps: List[Timestamp], idx: int
) -> List[str]:
timestamp_begin = timestamps[idx].standardized_str
timestamp_end = timestamps[idx + 1].standardized_str 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
def _split_video_uid(source_uid: str, idx: int) -> str:
return f"{source_uid}___{idx}"
class WhenNoChaptersValidator(StringSelectValidator): class WhenNoChaptersValidator(StringSelectValidator):
_expected_value_type = "when no chapters option" _expected_value_type = "when no chapters option"
@ -46,8 +68,77 @@ class SplitByChaptersOptions(PluginOptions):
class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]): class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
plugin_options_type = SplitByChaptersOptions plugin_options_type = SplitByChaptersOptions
def _create_split_entry(
self, source_entry: Entry, title: str, idx: int, chapters: Chapters
) -> Entry:
"""
Runs ffmpeg to create the split video
"""
entry_dict = copy.deepcopy(source_entry)
entry_dict["title"] = title
entry_dict["playlist_index"] = idx + 1
entry_dict["playlist_count"] = len(chapters.timestamps)
entry_dict["id"] = _split_video_uid(source_uid=entry_dict["id"], idx=idx)
# Remove track and artist since its now split
if "track" in entry_dict:
del entry_dict["track"]
if "artist" in entry_dict:
del entry_dict["artist"]
timestamp_begin = chapters.timestamps[idx].readable_str
timestamp_end = Timestamp(source_entry_dict["duration"]).readable_str
if idx + 1 < len(chapters.timestamps):
timestamp_end = chapters.timestamps[idx + 1].readable_str
metadata = FileMetadata(metadata=f"{timestamp_begin} - {timestamp_end}")
return (
YoutubePlaylistVideo(entry_dict=entry_dict, working_directory=self.working_directory),
metadata,
)
def modify_entry(self, entry: Entry) -> Optional[Entry]: def modify_entry(self, entry: Entry) -> Optional[Entry]:
""" """
Tags the entry's audio file using values defined in the metadata options Tags the entry's audio file using values defined in the metadata options
""" """
return entry split_entries: List[Entry] = []
chapters = Chapters.from_embedded_chapters(file_path=entry.get_download_file_path())
# convert the entry thumbnail early so we do not have to guess the thumbnail extension
# when copying it
if not self.is_dry_run:
convert_download_thumbnail(entry=entry)
for idx, title in enumerate(chapters.titles):
new_uid = _split_video_uid(source_uid=entry.uid, idx=idx)
if not self.is_dry_run:
# Get the input/output file paths
input_file = entry.get_download_file_path()
output_file = str(Path(self.working_directory) / f"{new_uid}.{entry.ext}")
# Run ffmpeg to create the split the video
FFMPEG.run(
_split_video_ffmpeg_cmd(
input_file=input_file,
output_file=output_file,
timestamps=chapters.timestamps,
idx=idx,
)
)
# Copy the original vid thumbnail to the working directory with the new uid. This so
# downstream logic thinks this split video has its own thumbnail
FileHandler.copy(
src_file_path=entry.get_download_thumbnail_path(),
dst_file_path=Path(self.working_directory) / f"{new_uid}.{entry.thumbnail_ext}",
)
# Format the split video as a YoutubePlaylistVideo
split_videos_and_metadata.append(
self._create_split_video_entry(
source_entry_dict=entry_dict, title=title, idx=idx, chapters=chapters
)
)
return split_videos_and_metadata

View file

@ -1,10 +1,14 @@
import json
import os import os
import re import re
import subprocess
from io import BytesIO
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
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.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
@ -165,7 +169,7 @@ class Chapters:
) )
@classmethod @classmethod
def from_file(cls, chapters_file_path: str) -> "Chapters": def from_timestamps_file(cls, chapters_file_path: str) -> "Chapters":
""" """
Parameters Parameters
---------- ----------
@ -207,3 +211,20 @@ class Chapters:
titles.append(title) titles.append(title)
return cls(timestamps=timestamps, titles=titles) return cls(timestamps=timestamps, titles=titles)
@classmethod
def from_embedded_chapters(cls, file_path: str) -> "Chapters":
with BytesIO() as bytes_io:
subprocess.run([
"-loglevel", "quiet", "-print_format", "json", "-show_chapters", "--", file_path
], check=True, stdout=bytes_io)
embedded_chapters = json.load(bytes_io)
timestamps: List[Timestamp] = []
titles: List[str] = []
for chapter in embedded_chapters['chapters']:
timestamps.append(Timestamp.from_seconds(int(chapter['start_time'])))
titles.append(chapter['tags']['title'])
return Chapters(timestamps=timestamps, titles=titles)

View file

@ -17,6 +17,10 @@ def single_video_sponsorblock_and_embedded_subs_preset_dict(output_directory):
"languages": ["en", "de"], "languages": ["en", "de"],
"allow_auto_generated_subtitles": True, "allow_auto_generated_subtitles": True,
}, },
"audio_extract": {
"codec": "mp3",
"quality": 128,
},
"chapters": { "chapters": {
"sponsorblock_categories": [ "sponsorblock_categories": [
"outro", "outro",

View file

@ -1,14 +1,9 @@
Files created in '{output_directory}' Files created in '{output_directory}'
---------------------------------------- ----------------------------------------
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case-thumb.jpg JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case-thumb.jpg
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4 JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp3
Embedded Chapters Embedded Chapters
Removed Chapter(s): Intro, Outro Removed Chapter(s): Intro, Outro
Removed SponsorBlock Category Count(s):
Sponsor: 2
Endcards/Credits: 1
Intermission/Intro Animation: 1
Unpaid/Self Promotion: 1
Embedded subtitles with lang(s) en, de Embedded subtitles with lang(s) en, de
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.nfo JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.nfo
NFO tags: NFO tags: