mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: move sanitize_name_for_os to domain/output_paths.py
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user