subtitles seem to work, need to manually verify
This commit is contained in:
parent
dae799fe12
commit
103e5aaac2
7 changed files with 180 additions and 68 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from yt_dlp.utils import DateRange
|
from yt_dlp.utils import DateRange
|
||||||
|
|
@ -74,15 +75,31 @@ class Overrides(DictFormatterValidator):
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply_formatter(
|
def apply_formatter(
|
||||||
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
|
self,
|
||||||
|
formatter: StringFormatterValidator,
|
||||||
|
entry: Optional[Entry] = None,
|
||||||
|
function_overrides: Dict[str, str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the format_string after .format has been called on it using entry (if provided) and
|
Parameters
|
||||||
override values
|
----------
|
||||||
|
formatter
|
||||||
|
Formatter to apply
|
||||||
|
entry
|
||||||
|
Optional. Entry to add source variables to the formatter
|
||||||
|
function_overrides
|
||||||
|
Optional. Explicit values to override the overrides themselves and source variables
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The format_string after .format has been called
|
||||||
"""
|
"""
|
||||||
variable_dict = self.dict_with_format_strings
|
variable_dict = self.dict_with_format_strings
|
||||||
if entry:
|
if entry:
|
||||||
variable_dict = dict(entry.to_dict(), **variable_dict)
|
variable_dict = dict(entry.to_dict(), **variable_dict)
|
||||||
|
if function_overrides:
|
||||||
|
variable_dict = dict(variable_dict, **function_overrides)
|
||||||
|
|
||||||
return formatter.apply_formatter(variable_dict)
|
return formatter.apply_formatter(variable_dict)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
@ -6,7 +7,7 @@ from typing import Set
|
||||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||||
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.validators.strict_dict_validator import StrictDictValidator
|
from ytdl_sub.plugins.plugin import PluginOptions
|
||||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||||
from ytdl_sub.validators.string_select_validator import StringSelectValidator
|
from ytdl_sub.validators.string_select_validator import StringSelectValidator
|
||||||
from ytdl_sub.validators.validators import BoolValidator
|
from ytdl_sub.validators.validators import BoolValidator
|
||||||
|
|
@ -15,12 +16,20 @@ from ytdl_sub.validators.validators import StringListValidator
|
||||||
SUBTITLE_EXTENSIONS: Set[str] = {"srt", "vtt", "ass", "lrc"}
|
SUBTITLE_EXTENSIONS: Set[str] = {"srt", "vtt", "ass", "lrc"}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_entry_subtitle_file(path: Path, entry: Entry) -> bool:
|
||||||
|
if path.is_file() and path.name.startswith(entry.uid):
|
||||||
|
for ext in SUBTITLE_EXTENSIONS:
|
||||||
|
if path.name.endswith(f".{ext}"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class SubtitlesTypeValidator(StringSelectValidator):
|
class SubtitlesTypeValidator(StringSelectValidator):
|
||||||
_expected_value_type_name = "subtitles type"
|
_expected_value_type_name = "subtitles type"
|
||||||
_select_values = SUBTITLE_EXTENSIONS
|
_select_values = SUBTITLE_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
class SubtitleOptions(StrictDictValidator):
|
class SubtitleOptions(PluginOptions):
|
||||||
"""
|
"""
|
||||||
Defines how to download and store subtitles.
|
Defines how to download and store subtitles.
|
||||||
|
|
||||||
|
|
@ -56,10 +65,10 @@ class SubtitleOptions(StrictDictValidator):
|
||||||
)
|
)
|
||||||
self._subtitles_type = self._validate_key_if_present(
|
self._subtitles_type = self._validate_key_if_present(
|
||||||
key="subtitles_type", validator=SubtitlesTypeValidator, default="srt"
|
key="subtitles_type", validator=SubtitlesTypeValidator, default="srt"
|
||||||
)
|
).value
|
||||||
self._embed_subtitles = self._validate_key_if_present(
|
self._embed_subtitles = self._validate_key_if_present(
|
||||||
key="embed_subtitles", validator=BoolValidator
|
key="embed_subtitles", validator=BoolValidator
|
||||||
)
|
).value
|
||||||
self._languages = self._validate_key_if_present(
|
self._languages = self._validate_key_if_present(
|
||||||
key="languages", validator=StringListValidator, default=["en"]
|
key="languages", validator=StringListValidator, default=["en"]
|
||||||
).list
|
).list
|
||||||
|
|
@ -86,9 +95,9 @@ class SubtitleOptions(StrictDictValidator):
|
||||||
@property
|
@property
|
||||||
def embed_subtitles(self) -> Optional[bool]:
|
def embed_subtitles(self) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
Optional. Whether to embed the subtitles into the video file.
|
Optional. Whether to embed the subtitles into the video file. Defaults to False.
|
||||||
"""
|
"""
|
||||||
return self._subtitles_type
|
return self._embed_subtitles
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def languages(self) -> Optional[List[str]]:
|
def languages(self) -> Optional[List[str]]:
|
||||||
|
|
@ -104,6 +113,14 @@ class SubtitleOptions(StrictDictValidator):
|
||||||
"""
|
"""
|
||||||
return self._allow_auto_generated_subtitles
|
return self._allow_auto_generated_subtitles
|
||||||
|
|
||||||
|
def added_source_variables(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List of new source variables created by using the subtitles plugin
|
||||||
|
"""
|
||||||
|
return ["lang", "subtitle_ext"]
|
||||||
|
|
||||||
|
|
||||||
class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
||||||
plugin_options_type = SubtitleOptions
|
plugin_options_type = SubtitleOptions
|
||||||
|
|
@ -144,6 +161,18 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
||||||
}
|
}
|
||||||
).to_dict()
|
).to_dict()
|
||||||
|
|
||||||
|
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||||
|
requested_subtitles = entry.kwargs("requested_subtitles")
|
||||||
|
languages = sorted(requested_subtitles.keys())
|
||||||
|
entry.add_variables(
|
||||||
|
variables_to_add={
|
||||||
|
"subtitle_ext": self.plugin_options.subtitles_type,
|
||||||
|
"lang": ",".join(languages),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
def post_process_entry(self, entry: Entry) -> None:
|
def post_process_entry(self, entry: Entry) -> None:
|
||||||
"""
|
"""
|
||||||
Creates an entry's NFO file using values defined in the metadata options
|
Creates an entry's NFO file using values defined in the metadata options
|
||||||
|
|
@ -151,20 +180,22 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
entry:
|
entry:
|
||||||
Entry to create an NFO file for
|
Entry to create subtitles for
|
||||||
"""
|
"""
|
||||||
|
if not self.plugin_options.subtitles_name:
|
||||||
|
return
|
||||||
|
|
||||||
# def get_ytdlp_download_subtitle_paths(self) -> List[str]:
|
requested_subtitles = entry.kwargs("requested_subtitles")
|
||||||
# possible_subtitle_exts = SUBTITLE_EXTENSIONS
|
for lang in requested_subtitles.keys():
|
||||||
# subtitle_paths: List[str] = []
|
subtitle_file_name = f"{entry.uid}.{lang}.{self.plugin_options.subtitles_type}"
|
||||||
#
|
output_subtitle_file_name = self.overrides.apply_formatter(
|
||||||
# for ext in possible_subtitle_exts:
|
formatter=self.plugin_options.subtitles_name,
|
||||||
# for path in Path(self.working_directory()).rglob("*"):
|
entry=entry,
|
||||||
# if (
|
function_overrides={"lang": lang},
|
||||||
# path.is_file()
|
)
|
||||||
# and path.name.startswith(self.uid)
|
|
||||||
# and path.name.endswith(f".{ext}")
|
self.save_file(
|
||||||
# ):
|
file_name=subtitle_file_name,
|
||||||
# subtitle_paths.append(str(path))
|
output_file_name=output_subtitle_file_name,
|
||||||
#
|
entry=entry,
|
||||||
# return subtitle_paths
|
)
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,8 @@ class SubscriptionYTDLOptions:
|
||||||
def _dry_run_options(self) -> Dict:
|
def _dry_run_options(self) -> Dict:
|
||||||
return {
|
return {
|
||||||
"skip_download": True,
|
"skip_download": True,
|
||||||
|
"simulate": True,
|
||||||
"writethumbnail": False,
|
"writethumbnail": False,
|
||||||
"writesubtitles": False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -90,30 +90,45 @@ class SubscriptionYTDLOptions:
|
||||||
# TODO: warn here
|
# TODO: warn here
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
ytdl_options: Dict = {}
|
builder = YTDLOptionsBuilder()
|
||||||
subtitle_options: SubtitleOptions = subtitle_plugin.plugin_options
|
subtitle_options: SubtitleOptions = subtitle_plugin.plugin_options
|
||||||
|
|
||||||
write_subtitle_file: bool = subtitle_options.subtitles_name is not None
|
write_subtitle_file: bool = subtitle_options.subtitles_name is not None
|
||||||
if write_subtitle_file:
|
if write_subtitle_file:
|
||||||
ytdl_options["writesubtitles"] = True
|
builder.add(
|
||||||
ytdl_options["postprocessors"] = [
|
{
|
||||||
{"key": "FFmpegSubtitlesConvertor", "format": subtitle_options.subtitles_type}
|
"writesubtitles": True,
|
||||||
]
|
"postprocessors": [
|
||||||
|
{
|
||||||
|
"key": "FFmpegSubtitlesConvertor",
|
||||||
|
"format": subtitle_options.subtitles_type,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if subtitle_options.embed_subtitles:
|
if subtitle_options.embed_subtitles:
|
||||||
ytdl_options["postprocessors"] = [
|
builder.add(
|
||||||
# already_have_subtitle=True means keep the subtitle files. False means delete
|
{
|
||||||
|
"postprocessors": [
|
||||||
|
# already_have_subtitle=True means we downloaded the subtitle files.
|
||||||
{"key": "FFmpegEmbedSubtitle", "already_have_subtitle": write_subtitle_file}
|
{"key": "FFmpegEmbedSubtitle", "already_have_subtitle": write_subtitle_file}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# If neither subtitles_name or embed_subtitles is set, do not set any other flags
|
# If neither subtitles_name or embed_subtitles is set, do not set any other flags
|
||||||
if not ytdl_options:
|
if not builder.to_dict():
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
ytdl_options["writeautomaticsub"] = subtitle_options.allow_auto_generated_subtitles
|
builder.add(
|
||||||
ytdl_options["subtitleslangs"] = subtitle_options.languages
|
{
|
||||||
|
"writeautomaticsub": subtitle_options.allow_auto_generated_subtitles,
|
||||||
|
"subtitleslangs": subtitle_options.languages,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return ytdl_options
|
return builder.to_dict()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _user_ytdl_options(self) -> Dict:
|
def _user_ytdl_options(self) -> Dict:
|
||||||
|
|
@ -128,7 +143,9 @@ class SubscriptionYTDLOptions:
|
||||||
"""
|
"""
|
||||||
ytdl_options_builder = YTDLOptionsBuilder().add(self._global_options)
|
ytdl_options_builder = YTDLOptionsBuilder().add(self._global_options)
|
||||||
if self._dry_run:
|
if self._dry_run:
|
||||||
ytdl_options_builder.add(self._dry_run_options, self._user_ytdl_options)
|
ytdl_options_builder.add(
|
||||||
|
self._subtitle_options, self._user_ytdl_options, self._dry_run_options
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ytdl_options_builder.add(
|
ytdl_options_builder.add(
|
||||||
self._output_options, self._subtitle_options, self._user_ytdl_options
|
self._output_options, self._subtitle_options, self._user_ytdl_options
|
||||||
|
|
|
||||||
56
tests/e2e/plugins/test_subtitles.py
Normal file
56
tests/e2e/plugins/test_subtitles.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import pytest
|
||||||
|
from e2e.expected_download import assert_expected_downloads
|
||||||
|
from e2e.expected_transaction_log import assert_transaction_log_matches
|
||||||
|
|
||||||
|
from ytdl_sub.subscriptions.subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def single_video_subs_preset_dict(output_directory):
|
||||||
|
return {
|
||||||
|
"preset": "yt_music_video",
|
||||||
|
"youtube": {"video_url": "https://www.youtube.com/watch?v=2lAe1cqCOXo"},
|
||||||
|
# override the output directory with our fixture-generated dir
|
||||||
|
"output_options": {"output_directory": output_directory},
|
||||||
|
"subtitles": {
|
||||||
|
"subtitles_name": "{music_video_name}.{lang}.{subtitles_ext}",
|
||||||
|
"embed_subtitles": True,
|
||||||
|
"languages": ["en", "de"],
|
||||||
|
"allow_auto_generated_subtitles": True,
|
||||||
|
},
|
||||||
|
# download the worst format so it is fast
|
||||||
|
"ytdl_options": {
|
||||||
|
"format": "worst[ext=mp4]",
|
||||||
|
},
|
||||||
|
"overrides": {"artist": "JMC"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubtitles:
|
||||||
|
@pytest.mark.parametrize("dry_run", [True, False])
|
||||||
|
def test_single_video_download_with_subtitles(
|
||||||
|
self,
|
||||||
|
music_video_config,
|
||||||
|
single_video_subs_preset_dict,
|
||||||
|
output_directory,
|
||||||
|
dry_run,
|
||||||
|
):
|
||||||
|
subscription = Subscription.from_dict(
|
||||||
|
config=music_video_config,
|
||||||
|
preset_name="subtitles_video_test",
|
||||||
|
preset_dict=single_video_subs_preset_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction_log = subscription.download(dry_run=dry_run)
|
||||||
|
assert_transaction_log_matches(
|
||||||
|
output_directory=output_directory,
|
||||||
|
transaction_log=transaction_log,
|
||||||
|
transaction_log_summary_file_name="plugins/test_subtitles_video.txt",
|
||||||
|
regenerate_transaction_log=True,
|
||||||
|
)
|
||||||
|
assert_expected_downloads(
|
||||||
|
output_directory=output_directory,
|
||||||
|
dry_run=dry_run,
|
||||||
|
expected_download_summary_file_name="plugins/test_subtitles_video.json",
|
||||||
|
regenerate_expected_download_summary=True,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind-thumb.jpg": "704246dd78074e8a0ec001dd8d03fd60",
|
||||||
|
"JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.de.srt": "b343c3bb9257b7ee7ba38f570a115b37",
|
||||||
|
"JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.en.srt": "fe8c6ee92cae6e059fd80fd61691adbe",
|
||||||
|
"JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.mp4": "383213d2809cdc2e86e3a2c4c8deb685",
|
||||||
|
"JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.nfo": "ffa10f1cbc098ace7b1c7a8fbe3097a8"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
Files created in '{output_directory}'
|
||||||
|
----------------------------------------
|
||||||
|
JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind-thumb.jpg
|
||||||
|
JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.de.srt
|
||||||
|
JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.en.srt
|
||||||
|
JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.mp4
|
||||||
|
JMC - YouTube Rewind 2019: For the Record | #YouTubeRewind.nfo
|
||||||
|
NFO tags:
|
||||||
|
musicvideo:
|
||||||
|
album: Music Videos
|
||||||
|
artist: JMC
|
||||||
|
title: YouTube Rewind 2019: For the Record | #YouTubeRewind
|
||||||
|
year: 2019
|
||||||
|
|
@ -54,35 +54,6 @@ class TestYoutubeVideo:
|
||||||
expected_download_summary_file_name="youtube/test_video.json",
|
expected_download_summary_file_name="youtube/test_video.json",
|
||||||
)
|
)
|
||||||
|
|
||||||
# @pytest.mark.parametrize("dry_run", [True, False])
|
|
||||||
# def test_single_video_download_with_subtitles(
|
|
||||||
# self,
|
|
||||||
# music_video_config,
|
|
||||||
# single_video_preset_dict,
|
|
||||||
# expected_single_video_download,
|
|
||||||
# output_directory,
|
|
||||||
# dry_run,
|
|
||||||
# ):
|
|
||||||
# single_video_preset_dict["youtube"][
|
|
||||||
# "video_url"
|
|
||||||
# ] = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
||||||
# single_video_preset_dict["subtitles"] = {}
|
|
||||||
# single_video_preset_dict["subtitles"]["subtitles_name"] = "{music_video_name}.srt"
|
|
||||||
# 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(dry_run=dry_run)
|
|
||||||
# assert_transaction_log_matches(
|
|
||||||
# output_directory=output_directory,
|
|
||||||
# transaction_log=transaction_log,
|
|
||||||
# transaction_log_summary_file_name="youtube/test_video.txt",
|
|
||||||
# )
|
|
||||||
# if not dry_run:
|
|
||||||
# expected_single_video_download.assert_files_exist(relative_directory=output_directory)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
@pytest.mark.parametrize("dry_run", [True, False])
|
||||||
def test_single_video_download_from_cli_dl(
|
def test_single_video_download_from_cli_dl(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue