From cec1328684c84b6af6346211f943d3548155a606 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Fri, 10 Mar 2023 09:50:12 -0800 Subject: [PATCH] [BACKEND] Use title for TV show preset file names --- docs/config.rst | 2 + src/ytdl_sub/config/preset_options.py | 14 ++--- src/ytdl_sub/plugins/nfo_tags.py | 6 +- src/ytdl_sub/plugins/subtitles.py | 4 +- .../validators/file_path_validators.py | 60 ++++++++++++++++--- .../validators/test_file_path_validators.py | 4 +- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 6862ba8b..3b0be2ee 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -328,6 +328,8 @@ The `config.yaml`_ uses various types for its configurable fields. Below is a de .. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator() +.. autoclass:: ytdl_sub.validators.file_path_validators.StringFormatterFileNameValidator() + .. autoclass:: ytdl_sub.validators.string_datetime.StringDatetimeValidator() .. autoclass:: ytdl_sub.validators.string_formatter_validators.DictFormatterValidator() diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index e90b4629..561d8634 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -7,10 +7,8 @@ from typing import Optional from yt_dlp.utils import sanitize_filename from ytdl_sub.entries.entry import Entry -from ytdl_sub.validators.file_path_validators import ( - OverridesStringFormatterValidatorFilePathValidator, -) -from ytdl_sub.validators.file_path_validators import StringFormatterFilePathValidator +from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator +from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.string_datetime import StringDatetimeValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator @@ -226,18 +224,18 @@ class OutputOptions(StrictDictValidator): # Output directory should resolve without any entry variables. # This is to check the directory for any download-archives before any downloads begin self._output_directory = self._validate_key( - key="output_directory", validator=OverridesStringFormatterValidatorFilePathValidator + key="output_directory", validator=OverridesStringFormatterFilePathValidator ) # file name and thumbnails however can use entry variables self._file_name = self._validate_key( - key="file_name", validator=StringFormatterFilePathValidator + key="file_name", validator=StringFormatterFileNameValidator ) self._thumbnail_name = self._validate_key_if_present( - key="thumbnail_name", validator=StringFormatterFilePathValidator + key="thumbnail_name", validator=StringFormatterFileNameValidator ) self._info_json_name = self._validate_key_if_present( - key="info_json_name", validator=StringFormatterFilePathValidator + key="info_json_name", validator=StringFormatterFileNameValidator ) self._maintain_download_archive = self._validate_key_if_present( diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index cb6df5a9..395cc3f2 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -16,7 +16,7 @@ from ytdl_sub.utils.xml import XmlElement from ytdl_sub.utils.xml import to_max_3_byte_utf8_dict from ytdl_sub.utils.xml import to_max_3_byte_utf8_string from ytdl_sub.utils.xml import to_xml -from ytdl_sub.validators.file_path_validators import StringFormatterFilePathValidator +from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator from ytdl_sub.validators.nfo_validators import NfoTagsValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator @@ -47,7 +47,7 @@ class SharedNfoTagsOptions(PluginOptions): super().__init__(name, value) self._nfo_name = self._validate_key_if_present( - key="nfo_name", validator=StringFormatterFilePathValidator + key="nfo_name", validator=StringFormatterFileNameValidator ) self._nfo_root = self._validate_key_if_present( key="nfo_root", validator=StringFormatterValidator @@ -58,7 +58,7 @@ class SharedNfoTagsOptions(PluginOptions): ).value @property - def nfo_name(self) -> StringFormatterFilePathValidator: + def nfo_name(self) -> StringFormatterFileNameValidator: """ The NFO file name. """ diff --git a/src/ytdl_sub/plugins/subtitles.py b/src/ytdl_sub/plugins/subtitles.py index 2b96ab01..99780a3c 100644 --- a/src/ytdl_sub/plugins/subtitles.py +++ b/src/ytdl_sub/plugins/subtitles.py @@ -11,7 +11,7 @@ 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 StringFormatterFilePathValidator +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 @@ -56,7 +56,7 @@ class SubtitleOptions(PluginOptions): def __init__(self, name, value): super().__init__(name, value) self._subtitles_name = self._validate_key_if_present( - key="subtitles_name", validator=StringFormatterFilePathValidator + key="subtitles_name", validator=StringFormatterFileNameValidator ) self._subtitles_type = self._validate_key_if_present( key="subtitles_type", validator=SubtitlesTypeValidator, default="srt" diff --git a/src/ytdl_sub/validators/file_path_validators.py b/src/ytdl_sub/validators/file_path_validators.py index 592b52b0..e17bc1a7 100644 --- a/src/ytdl_sub/validators/file_path_validators.py +++ b/src/ytdl_sub/validators/file_path_validators.py @@ -39,7 +39,56 @@ class FFprobeFileValidator(FFmpegFileValidator): _ffmpeg_dependency = "ffprobe" -class StringFormatterFilePathValidator(StringFormatterValidator): +class FilePathValidatorMixin: + @classmethod + def _is_file_name_too_long(cls, file_name: str) -> bool: + return len(file_name.encode("utf-8")) > _MAX_FILE_NAME_BYTES + + @classmethod + def _get_extension_split(cls, file_name: str) -> Tuple[str, str]: + if file_name.endswith(".info.json"): + ext = "info.json" + elif any(file_name.endswith(f".{subtitle_ext}") for subtitle_ext in SUBTITLE_EXTENSIONS): + file_name_split = file_name.split(".") + ext = file_name_split[-1] + + # Try to capture .lang.ext + if len(file_name_split) > 2 and len(file_name_split[-2]) < 6: + ext = f"{file_name_split[-2]}.{file_name_split[-1]}" + else: + ext = file_name.rsplit(".", maxsplit=1)[-1] + + return file_name[: -len(ext)], ext + + @classmethod + def _truncate_file_name(cls, file_name: str) -> str: + file_sub_name, file_ext = cls._get_extension_split(file_name) + + desired_size = _MAX_FILE_NAME_BYTES - len(file_ext.encode("utf-8")) - 1 + while len(file_sub_name.encode("utf-8")) > desired_size: + file_sub_name = file_sub_name[:-1] + + return f"{file_sub_name}.{file_ext}" + + @classmethod + def _maybe_truncate_file_path(cls, file_path: Path) -> str: + """Turn into a Path, then a string, to get correct directory separators""" + file_directory, file_name = os.path.split(Path(file_path)) + + if cls._is_file_name_too_long(file_name): + return str(Path(file_directory) / cls._truncate_file_name(file_name)) + + return str(file_path) + + +class StringFormatterFileNameValidator(StringFormatterValidator, FilePathValidatorMixin): + """ + Same as a + :class:`StringFormatterValidator ` + but ensures the file name does not exceed the OS limit (typically 255 bytes). If it does exceed, + it will preserve the extension and truncate the end of the file name. + """ + _expected_value_type_name = "filepath" @classmethod @@ -75,15 +124,10 @@ class StringFormatterFilePathValidator(StringFormatterValidator): def apply_formatter(self, variable_dict: Dict[str, str]) -> str: """Turn into a Path, then a string, to get correct directory separators""" file_path = Path(super().apply_formatter(variable_dict)) - file_directory, file_name = os.path.split(Path(file_path)) - - if self._is_file_name_too_long(file_name): - return str(Path(file_directory) / self._truncate_file_name(file_name)) - - return str(file_path) + return self._maybe_truncate_file_path(file_path) -class OverridesStringFormatterValidatorFilePathValidator(OverridesStringFormatterValidator): +class OverridesStringFormatterFilePathValidator(OverridesStringFormatterValidator): _expected_value_type_name = "static filepath" def apply_formatter(self, variable_dict: Dict[str, str]) -> str: diff --git a/tests/unit/validators/test_file_path_validators.py b/tests/unit/validators/test_file_path_validators.py index 4a667c79..de0061f4 100644 --- a/tests/unit/validators/test_file_path_validators.py +++ b/tests/unit/validators/test_file_path_validators.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest from ytdl_sub.utils.subtitles import SUBTITLE_EXTENSIONS -from ytdl_sub.validators.file_path_validators import StringFormatterFilePathValidator +from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator class TestStringFormatterFilePathValidator: @@ -26,7 +26,7 @@ class TestStringFormatterFilePathValidator: file_name = (file_name_char * file_name_len) + ext file_path = str(Path(temp_dir) / file_name) - formatter = StringFormatterFilePathValidator(name="test", value=str(file_path)) + formatter = StringFormatterFileNameValidator(name="test", value=str(file_path)) truncated_file_path = formatter.apply_formatter({}) assert truncated_file_path.count(".") == ext.count(".")