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