diff --git a/src/ytdl_sub/plugins/subtitles.py b/src/ytdl_sub/plugins/subtitles.py index c63d87b0..2b96ab01 100644 --- a/src/ytdl_sub/plugins/subtitles.py +++ b/src/ytdl_sub/plugins/subtitles.py @@ -2,7 +2,6 @@ from pathlib import Path from typing import Dict from typing import List from typing import Optional -from typing import Set from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.entries.entry import Entry @@ -11,15 +10,13 @@ from ytdl_sub.plugins.plugin import PluginOptions 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.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 -SUBTITLE_EXTENSIONS: Set[str] = {"srt", "vtt", "ass", "lrc"} - - logger = Logger.get(name="subtitles") diff --git a/src/ytdl_sub/utils/subtitles.py b/src/ytdl_sub/utils/subtitles.py new file mode 100644 index 00000000..9330ccbe --- /dev/null +++ b/src/ytdl_sub/utils/subtitles.py @@ -0,0 +1,3 @@ +from typing import Set + +SUBTITLE_EXTENSIONS: Set[str] = {"srt", "vtt", "ass", "lrc"} diff --git a/src/ytdl_sub/validators/file_path_validators.py b/src/ytdl_sub/validators/file_path_validators.py index 0a9cc41f..b5773bec 100644 --- a/src/ytdl_sub/validators/file_path_validators.py +++ b/src/ytdl_sub/validators/file_path_validators.py @@ -1,12 +1,25 @@ -import os from pathlib import Path from typing import Any from typing import Dict +from typing import Tuple +from ytdl_sub.utils.subtitles import SUBTITLE_EXTENSIONS +from ytdl_sub.utils.system import IS_WINDOWS from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.validators import StringValidator +_MAX_FILE_NAME_LEN: int = 0 +_MAX_FILE_NAME_BYTES: int = 0 +if IS_WINDOWS: + from ctypes.wintypes import MAX_PATH + + _MAX_FILE_NAME_LEN = MAX_PATH +else: + import os + + _MAX_FILE_NAME_BYTES = os.pathconf("/", "PC_NAME_MAX") + class FFmpegFileValidator(StringValidator): _expected_value_type_name = "ffmpeg dependency" @@ -34,9 +47,50 @@ class FFprobeFileValidator(FFmpegFileValidator): class StringFormatterFilePathValidator(StringFormatterValidator): _expected_value_type_name = "filepath" + @classmethod + def _is_file_name_too_long(cls, file_name: str) -> bool: + return (_MAX_FILE_NAME_LEN and len(file_name) > _MAX_FILE_NAME_LEN) or ( + _MAX_FILE_NAME_BYTES and 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) + if _MAX_FILE_NAME_LEN: + to_trim = len(file_name) - _MAX_FILE_NAME_LEN + 1 + file_sub_name = file_sub_name[:-to_trim] + elif _MAX_FILE_NAME_BYTES: + 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}" + def apply_formatter(self, variable_dict: Dict[str, str]) -> str: """Turn into a Path, then a string, to get correct directory separators""" - return str(Path(super().apply_formatter(variable_dict))) + file_path = str(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 file_path class OverridesStringFormatterValidatorFilePathValidator(OverridesStringFormatterValidator): diff --git a/tests/unit/validators/test_file_path_validators.py b/tests/unit/validators/test_file_path_validators.py new file mode 100644 index 00000000..7fe057de --- /dev/null +++ b/tests/unit/validators/test_file_path_validators.py @@ -0,0 +1,34 @@ +import tempfile +from pathlib import Path + +import pytest + +from ytdl_sub.utils.subtitles import SUBTITLE_EXTENSIONS +from ytdl_sub.validators.file_path_validators import StringFormatterFilePathValidator + + +class TestStringFormatterFilePathValidator: + @pytest.mark.parametrize( + "ext", + [ + ".mp4", + ".info.json", + ] + + [f".en.{ext}" for ext in SUBTITLE_EXTENSIONS], + ) + def test_truncates_file_name_successfully(self, ext: str): + ext = ".info.json" + with tempfile.TemporaryDirectory() as temp_dir: + file_name = ("a" * 10000) + ext + file_path = str(Path(temp_dir) / file_name) + + formatter = StringFormatterFilePathValidator(name="test", value=str(file_path)) + truncated_file_path = formatter.apply_formatter({}) + + assert truncated_file_path.count(".") == ext.count(".") + assert str(Path(temp_dir)) in truncated_file_path + assert ext in truncated_file_path + + # Ensure it can actually open the file + with open(truncated_file_path, "w", encoding="utf-8"): + pass