split chapters mess
This commit is contained in:
parent
cb37f5938c
commit
276522c139
6 changed files with 122 additions and 24 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue