refactor: move sanitize_name_for_os to domain/output_paths.py

This commit is contained in:
Artem Akymenko
2026-07-24 19:17:39 +03:00
parent d1a84cfb8b
commit 654c395943
2 changed files with 46 additions and 73 deletions
+44 -1
View File
@@ -13,7 +13,6 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Callable, List, Optional, Tuple from typing import Any, Callable, List, Optional, Tuple
from abogen.subtitle_utils import sanitize_name_for_os
from abogen.text_extractor import ExtractedChapter from abogen.text_extractor import ExtractedChapter
@@ -21,6 +20,9 @@ _OUTPUT_SANITIZE_RE = re.compile(r"[^\w\-_.]+")
# OS-specific illegal characters for filenames # OS-specific illegal characters for filenames
_WINDOWS_ILLEGAL_CHARS_RE = re.compile(r'[<>:"/\\|?*\x00-\x1f]') _WINDOWS_ILLEGAL_CHARS_RE = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
_MACOS_ILLEGAL_CHARS_RE = re.compile(r"[:]")
_LINUX_ILLEGAL_CHARS_RE = re.compile(r"[/\x00]")
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f]")
_UNIX_CONTROL_CHARS_RE = re.compile(r'[\x00-\x1f]') _UNIX_CONTROL_CHARS_RE = re.compile(r'[\x00-\x1f]')
_RESERVED_NAMES = frozenset( _RESERVED_NAMES = frozenset(
{"CON", "PRN", "AUX", "NUL"} {"CON", "PRN", "AUX", "NUL"}
@@ -29,6 +31,47 @@ _RESERVED_NAMES = frozenset(
) )
def sanitize_name_for_os(name: str, is_folder: bool = True) -> str:
"""Sanitize a filename or folder name based on the operating system.
Args:
name: The name to sanitize
is_folder: Whether this is a folder name (default: True)
Returns:
Sanitized name safe for the current OS
"""
if not name:
return "audiobook"
system = platform.system()
if system == "Windows":
sanitized = _WINDOWS_ILLEGAL_CHARS_RE.sub("_", name)
sanitized = _CONTROL_CHARS_RE.sub("_", sanitized)
sanitized = sanitized.rstrip(". ")
if sanitized.upper() in _RESERVED_NAMES or sanitized.upper().split(".")[0] in _RESERVED_NAMES:
sanitized = f"_{sanitized}"
elif system == "Darwin":
sanitized = _MACOS_ILLEGAL_CHARS_RE.sub("_", name)
sanitized = _CONTROL_CHARS_RE.sub("_", sanitized)
if is_folder and sanitized.startswith("."):
sanitized = "_" + sanitized[1:]
else:
sanitized = _LINUX_ILLEGAL_CHARS_RE.sub("_", name)
sanitized = _UNIX_CONTROL_CHARS_RE.sub("_", sanitized)
if is_folder and sanitized.startswith("."):
sanitized = "_" + sanitized[1:]
if not sanitized or sanitized.strip() == "":
sanitized = "audiobook"
if len(sanitized) > 255:
sanitized = sanitized[:255].rstrip(". ")
return sanitized
def slugify(title: str, index: int) -> str: def slugify(title: str, index: int) -> str:
sanitized = re.sub(r"[^\w\-]+", "_", title.lower()).strip("_") sanitized = re.sub(r"[^\w\-]+", "_", title.lower()).strip("_")
if not sanitized: if not sanitized:
+2 -72
View File
@@ -1,5 +1,4 @@
import re import re
import platform
from abogen.utils import detect_encoding, load_config from abogen.utils import detect_encoding, load_config
from abogen.constants import SAMPLE_VOICE_TEXTS from abogen.constants import SAMPLE_VOICE_TEXTS
@@ -23,13 +22,6 @@ _VTT_NOTE_PATTERN = re.compile(r"NOTE\s*\n.*?(?=\n\n|$)", re.DOTALL)
_DOUBLE_NEWLINE_SPLIT_PATTERN = re.compile(r"\n\s*\n") _DOUBLE_NEWLINE_SPLIT_PATTERN = re.compile(r"\n\s*\n")
_VTT_TIMESTAMP_PATTERN = re.compile(r"([\d:.]+)\s*-->\s*([\d:.]+)") _VTT_TIMESTAMP_PATTERN = re.compile(r"([\d:.]+)\s*-->\s*([\d:.]+)")
_TIMESTAMP_ONLY_PATTERN = re.compile(r"^(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3})?)$") _TIMESTAMP_ONLY_PATTERN = re.compile(r"^(\d{1,2}:\d{2}:\d{2}(?:[.,]\d{1,3})?)$")
_WINDOWS_ILLEGAL_CHARS_PATTERN = re.compile(r'[<>:"/\\|?*]')
_CONTROL_CHARS_PATTERN = re.compile(r"[\x00-\x1f]")
_LINUX_CONTROL_CHARS_PATTERN = re.compile(
r"[\x01-\x1f]"
) # Linux: exclude \x00 for separate handling
_MACOS_ILLEGAL_CHARS_PATTERN = re.compile(r"[:]")
_LINUX_ILLEGAL_CHARS_PATTERN = re.compile(r"[/\x00]")
def clean_subtitle_text(text): def clean_subtitle_text(text):
@@ -400,70 +392,8 @@ def get_sample_voice_text(lang_code):
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"]) return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
def sanitize_name_for_os(name, is_folder=True): # Backward-compatible re-exports — canonical location is domain/output_paths.py
""" from abogen.domain.output_paths import sanitize_name_for_os # noqa: E402, F401
Sanitize a filename or folder name based on the operating system.
Args:
name: The name to sanitize
is_folder: Whether this is a folder name (default: True)
Returns:
Sanitized name safe for the current OS
"""
if not name:
return "audiobook"
system = platform.system()
if system == "Windows":
# Windows illegal characters: < > : " / \ | ? *
# Also can't end with space or dot
# Use pre-compiled pattern for better performance
sanitized = _WINDOWS_ILLEGAL_CHARS_PATTERN.sub("_", name)
# Remove control characters (0-31)
sanitized = _CONTROL_CHARS_PATTERN.sub("_", sanitized)
# Remove trailing spaces and dots
sanitized = sanitized.rstrip(". ")
# Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
reserved = (
["CON", "PRN", "AUX", "NUL"]
+ [f"COM{i}" for i in range(1, 10)]
+ [f"LPT{i}" for i in range(1, 10)]
)
if sanitized.upper() in reserved or sanitized.upper().split(".")[0] in reserved:
sanitized = f"_{sanitized}"
elif system == "Darwin": # macOS
# macOS illegal characters: : (colon is converted to / by the system)
# Also can't start with dot (hidden file) for folders typically
# Use pre-compiled pattern for better performance
sanitized = _MACOS_ILLEGAL_CHARS_PATTERN.sub("_", name)
# Remove control characters
sanitized = _CONTROL_CHARS_PATTERN.sub("_", sanitized)
# Avoid leading dot for folders (creates hidden folders)
if is_folder and sanitized.startswith("."):
sanitized = "_" + sanitized[1:]
else: # Linux and others
# Linux illegal characters: / and null character
# Though / is illegal, most other chars are technically allowed
# Use pre-compiled pattern for better performance
sanitized = _LINUX_ILLEGAL_CHARS_PATTERN.sub("_", name)
# Remove other control characters for safety (excluding \x00 which is already handled)
sanitized = _LINUX_CONTROL_CHARS_PATTERN.sub("_", sanitized)
# Avoid leading dot for folders (creates hidden folders)
if is_folder and sanitized.startswith("."):
sanitized = "_" + sanitized[1:]
# Ensure the name is not empty after sanitization
if not sanitized or sanitized.strip() == "":
sanitized = "audiobook"
# Limit length to 255 characters (common limit across filesystems)
if len(sanitized) > 255:
sanitized = sanitized[:255].rstrip(". ")
return sanitized
# Backward-compatible re-exports — canonical location is domain/voice_markers.py # Backward-compatible re-exports — canonical location is domain/voice_markers.py
from abogen.domain.voice_markers import ( # noqa: E402, F401 from abogen.domain.voice_markers import ( # noqa: E402, F401