Closes https://github.com/jmbannon/ytdl-sub/issues/1269 Adds the new paramter `languages_required`, which forces an entry to have this set of subtitle files, otherwise it will error. In addition, better file checking has been added to ensure they exist before attempting to move.
266 lines
9.9 KiB
Python
266 lines
9.9 KiB
Python
import os
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import Optional
|
|
from typing import Set
|
|
from typing import Tuple
|
|
|
|
from ytdl_sub.config.plugin.plugin import Plugin
|
|
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
|
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
|
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
|
from ytdl_sub.entries.entry import Entry
|
|
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
|
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
|
from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
|
|
from ytdl_sub.utils.file_handler import FileHandler
|
|
from ytdl_sub.utils.file_handler import FileMetadata
|
|
from ytdl_sub.utils.logger import Logger
|
|
from ytdl_sub.utils.subtitles import SUBTITLE_EXTENSIONS
|
|
from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
|
|
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
|
from ytdl_sub.validators.string_select_validator import StringSelectValidator
|
|
from ytdl_sub.validators.validators import BoolValidator
|
|
from ytdl_sub.validators.validators import StringListValidator
|
|
|
|
v: VariableDefinitions = VARIABLES
|
|
|
|
logger = Logger.get(name="subtitles")
|
|
|
|
|
|
class SubtitlesTypeValidator(StringSelectValidator):
|
|
_expected_value_type_name = "subtitles type"
|
|
_select_values = SUBTITLE_EXTENSIONS
|
|
|
|
|
|
class SubtitleOptions(ToggleableOptionsDictValidator):
|
|
"""
|
|
Defines how to download and store subtitles. Using this plugin creates two new variables:
|
|
``lang`` and ``subtitles_ext``. ``lang`` is dynamic since you can download multiple subtitles.
|
|
It will set the respective language to the correct subtitle file.
|
|
|
|
:Usage:
|
|
|
|
.. code-block:: yaml
|
|
|
|
subtitles:
|
|
subtitles_name: "{title_sanitized}.{lang}.{subtitles_ext}"
|
|
subtitles_type: "srt"
|
|
embed_subtitles: False
|
|
languages:
|
|
- "en" # supports multiple languages
|
|
- "de"
|
|
allow_auto_generated_subtitles: False
|
|
"""
|
|
|
|
_optional_keys = {
|
|
"enable",
|
|
"subtitles_name",
|
|
"subtitles_type",
|
|
"embed_subtitles",
|
|
"languages",
|
|
"languages_required",
|
|
"allow_auto_generated_subtitles",
|
|
}
|
|
|
|
def __init__(self, name, value):
|
|
super().__init__(name, value)
|
|
self._subtitles_name = self._validate_key_if_present(
|
|
key="subtitles_name", validator=StringFormatterFileNameValidator
|
|
)
|
|
self._subtitles_type = self._validate_key_if_present(
|
|
key="subtitles_type", validator=SubtitlesTypeValidator, default="srt"
|
|
).value
|
|
self._embed_subtitles = self._validate_key_if_present(
|
|
key="embed_subtitles",
|
|
validator=BoolValidator,
|
|
default=False,
|
|
).value
|
|
self._languages = self._validate_key_if_present(
|
|
key="languages", validator=StringListValidator, default=["en"]
|
|
).list
|
|
self._languages_required = self._validate_key_if_present(
|
|
key="languages_required", validator=StringListValidator, default=[]
|
|
).list
|
|
self._allow_auto_generated_subtitles = self._validate_key_if_present(
|
|
key="allow_auto_generated_subtitles", validator=BoolValidator, default=False
|
|
).value
|
|
|
|
@property
|
|
def subtitles_name(self) -> Optional[StringFormatterValidator]:
|
|
"""
|
|
:expected type: Optional[EntryFormatter]
|
|
:description:
|
|
The file name for the media's subtitles if they are present. This can include
|
|
directories such as ``"Season {upload_year}/{title_sanitized}.{lang}.{subtitles_ext}"``,
|
|
and will be placed in the output directory. ``lang`` is dynamic since you can download
|
|
multiple subtitles. It will set the respective language to the correct subtitle file.
|
|
"""
|
|
return self._subtitles_name
|
|
|
|
@property
|
|
def subtitles_type(self) -> Optional[str]:
|
|
"""
|
|
:expected type: Optional[String]
|
|
:description:
|
|
Defaults to "srt". One of the subtitle file types "srt", "vtt", "ass", "lrc".
|
|
"""
|
|
return self._subtitles_type
|
|
|
|
@property
|
|
def embed_subtitles(self) -> Optional[bool]:
|
|
"""
|
|
:expected type: Optional[Boolean]
|
|
:description:
|
|
Defaults to False. Whether to embed the subtitles into the video file. Note that
|
|
webm files can only embed "vtt" subtitle types.
|
|
"""
|
|
return self._embed_subtitles
|
|
|
|
@property
|
|
def languages(self) -> Optional[List[str]]:
|
|
"""
|
|
:expected type: Optional[List[String]]
|
|
:description:
|
|
Language code(s) to download for subtitles. Supports a single or list of multiple
|
|
language codes. Defaults to only "en".
|
|
"""
|
|
return [lang.value for lang in self._languages]
|
|
|
|
@property
|
|
def languages_required(self) -> Optional[List[str]]:
|
|
"""
|
|
:expected type: Optional[List[String]]
|
|
:description:
|
|
Language code(s) that are required to be present for downloads to continue. If missing,
|
|
ytdl-sub will throw an error. NOTE: currently this only checks file-based subtitles.
|
|
"""
|
|
return [lang.value for lang in self._languages_required]
|
|
|
|
@property
|
|
def allow_auto_generated_subtitles(self) -> Optional[bool]:
|
|
"""
|
|
:expected type: Optional[Boolean]
|
|
:description:
|
|
Defaults to False. Whether to allow auto generated subtitles.
|
|
"""
|
|
return self._allow_auto_generated_subtitles
|
|
|
|
def added_variables(
|
|
self,
|
|
unresolved_variables: Set[str],
|
|
) -> Dict[PluginOperation, Set[str]]:
|
|
"""
|
|
Returns
|
|
-------
|
|
List of new source variables created by using the subtitles plugin
|
|
"""
|
|
return {PluginOperation.MODIFY_ENTRY_METADATA: {"lang", "subtitles_ext"}}
|
|
|
|
|
|
class SubtitlesPlugin(Plugin[SubtitleOptions]):
|
|
plugin_options_type = SubtitleOptions
|
|
|
|
def ytdl_options(self) -> Optional[Dict]:
|
|
builder = YTDLOptionsBuilder().add(
|
|
{
|
|
"writesubtitles": True,
|
|
"sleep_interval_subtitles": 5.367, # for safe measure
|
|
}
|
|
)
|
|
|
|
if self.plugin_options.embed_subtitles:
|
|
builder.add(
|
|
{"postprocessors": [{"key": "FFmpegEmbedSubtitle", "already_have_subtitle": True}]}
|
|
)
|
|
|
|
if self.plugin_options.subtitles_name:
|
|
builder.add(
|
|
{
|
|
"postprocessors": [
|
|
{
|
|
"key": "FFmpegSubtitlesConvertor",
|
|
"format": self.plugin_options.subtitles_type,
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
# If neither subtitles_name or embed_subtitles is set, do not set any other flags
|
|
if not builder.to_dict():
|
|
return {}
|
|
|
|
builder.add(
|
|
{
|
|
"writeautomaticsub": self.plugin_options.allow_auto_generated_subtitles,
|
|
"subtitleslangs": self.plugin_options.languages,
|
|
}
|
|
)
|
|
|
|
return builder.to_dict()
|
|
|
|
def modify_entry_metadata(self, entry: Entry) -> Optional[Entry]:
|
|
entry.add({"subtitles_ext": self.plugin_options.subtitles_type, "lang": ""})
|
|
return entry
|
|
|
|
def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]:
|
|
"""
|
|
Creates an entry's NFO file using values defined in the metadata options
|
|
|
|
Parameters
|
|
----------
|
|
entry:
|
|
Entry to create subtitles for
|
|
"""
|
|
requested_subtitles = entry.get(v.requested_subtitles, expected_type=dict)
|
|
if not requested_subtitles:
|
|
logger.debug("subtitles not found for %s", entry.title)
|
|
return None
|
|
|
|
file_metadata: Optional[FileMetadata] = None
|
|
langs = list(requested_subtitles.keys())
|
|
|
|
# HACK to maintain order of languages for fixtures
|
|
if len(langs) == len(self.plugin_options.languages):
|
|
langs = self.plugin_options.languages
|
|
|
|
if self.plugin_options.embed_subtitles:
|
|
file_metadata = FileMetadata(f"Embedded subtitles with lang(s) {', '.join(langs)}")
|
|
if self.plugin_options.subtitles_name:
|
|
download_subtitle_lang_file_names: List[Tuple[str, str]] = []
|
|
|
|
for lang in langs:
|
|
download_subtitle_file_name = entry.base_filename(
|
|
ext=f"{lang}.{self.plugin_options.subtitles_type}"
|
|
)
|
|
if os.path.isfile(Path(self.working_directory) / download_subtitle_file_name):
|
|
download_subtitle_lang_file_names.append((lang, download_subtitle_file_name))
|
|
elif lang in self.plugin_options.languages_required:
|
|
raise UserThrownRuntimeError(
|
|
f"Required the subtitle lang {lang}, but the file could not be found for "
|
|
f"the entry {entry.title}."
|
|
)
|
|
|
|
for lang, file_name in download_subtitle_lang_file_names:
|
|
output_subtitle_file_name = self.overrides.apply_formatter(
|
|
formatter=self.plugin_options.subtitles_name,
|
|
entry=entry,
|
|
function_overrides={"lang": lang},
|
|
)
|
|
|
|
self.save_file(
|
|
file_name=file_name,
|
|
output_file_name=output_subtitle_file_name,
|
|
entry=entry,
|
|
)
|
|
|
|
# Delete any possible original subtitle files before conversion
|
|
# Can happen for both file and embedded subs
|
|
for lang in langs:
|
|
for possible_ext in SUBTITLE_EXTENSIONS:
|
|
possible_subs_filename = entry.base_filename(ext=f"{lang}.{possible_ext}")
|
|
possible_subs_file = Path(self.working_directory) / possible_subs_filename
|
|
FileHandler.delete(possible_subs_file)
|
|
|
|
return file_metadata
|