mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +02:00
Fixed folder and filename sanitization to properly handle OS-specific illegal characters
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
# 1.2.0
|
# 1.2.0 (pre-release)
|
||||||
- Added `Line` option to subtitle generation modes, allowing subtitles to be generated based on line breaks in the text, by @mleg in #94.
|
- Added `Line` option to subtitle generation modes, allowing subtitles to be generated based on line breaks in the text, by @mleg in #94.
|
||||||
- Fixed `cannot access local variable 'is_narrow'` error when subtitle format `SRT` was selected, mentioned by @Kinasa0096 in #88.
|
- Fixed `cannot access local variable 'is_narrow'` error when subtitle format `SRT` was selected, mentioned by @Kinasa0096 in #88.
|
||||||
|
- Fixed folder and filename sanitization to properly handle OS-specific illegal characters (Windows, Linux, macOS), ensuring compatibility across all platforms when creating chapter folders and files.
|
||||||
- Improvements in code and documentation.
|
- Improvements in code and documentation.
|
||||||
|
|
||||||
# 1.1.9
|
# 1.1.9
|
||||||
|
|||||||
+71
-3
@@ -28,6 +28,66 @@ 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):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
sanitized = re.sub(r'[<>:"/\\|?*]', '_', name)
|
||||||
|
# Remove control characters (0-31)
|
||||||
|
sanitized = re.sub(r'[\x00-\x1f]', '_', 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
|
||||||
|
sanitized = re.sub(r'[:]', '_', name)
|
||||||
|
# Remove control characters
|
||||||
|
sanitized = re.sub(r'[\x00-\x1f]', '_', 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
|
||||||
|
sanitized = re.sub(r'[/\x00]', '_', name)
|
||||||
|
# Remove other control characters for safety
|
||||||
|
sanitized = re.sub(r'[\x01-\x1f]', '_', 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
|
||||||
|
|
||||||
|
|
||||||
class ChapterOptionsDialog(QDialog):
|
class ChapterOptionsDialog(QDialog):
|
||||||
def __init__(self, chapter_count, parent=None):
|
def __init__(self, chapter_count, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -417,6 +477,9 @@ class ConversionThread(QThread):
|
|||||||
base_path = self.display_path if self.display_path else self.file_name
|
base_path = self.display_path if self.display_path else self.file_name
|
||||||
|
|
||||||
base_name = os.path.splitext(os.path.basename(base_path))[0]
|
base_name = os.path.splitext(os.path.basename(base_path))[0]
|
||||||
|
# Sanitize base_name for folder/file creation based on OS
|
||||||
|
sanitized_base_name = sanitize_name_for_os(base_name, is_folder=True)
|
||||||
|
|
||||||
if self.save_option == "Save to Desktop":
|
if self.save_option == "Save to Desktop":
|
||||||
parent_dir = user_desktop_dir()
|
parent_dir = user_desktop_dir()
|
||||||
elif self.save_option == "Save next to input file":
|
elif self.save_option == "Save next to input file":
|
||||||
@@ -437,11 +500,11 @@ class ConversionThread(QThread):
|
|||||||
while True:
|
while True:
|
||||||
suffix = f"_{counter}" if counter > 1 else ""
|
suffix = f"_{counter}" if counter > 1 else ""
|
||||||
chapters_out_dir_candidate = os.path.join(
|
chapters_out_dir_candidate = os.path.join(
|
||||||
parent_dir, f"{base_name}{suffix}_chapters"
|
parent_dir, f"{sanitized_base_name}{suffix}_chapters"
|
||||||
)
|
)
|
||||||
# Only check for files with allowed extensions (extension without dot, case-insensitive)
|
# Only check for files with allowed extensions (extension without dot, case-insensitive)
|
||||||
clash = any(
|
clash = any(
|
||||||
os.path.splitext(fname)[0] == f"{base_name}{suffix}"
|
os.path.splitext(fname)[0] == f"{sanitized_base_name}{suffix}"
|
||||||
and os.path.splitext(fname)[1][1:].lower() in allowed_exts
|
and os.path.splitext(fname)[1][1:].lower() in allowed_exts
|
||||||
for fname in os.listdir(parent_dir)
|
for fname in os.listdir(parent_dir)
|
||||||
)
|
)
|
||||||
@@ -461,7 +524,7 @@ class ConversionThread(QThread):
|
|||||||
# Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True
|
# Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
out_dir = parent_dir
|
out_dir = parent_dir
|
||||||
base_filepath_no_ext = os.path.join(out_dir, f"{base_name}{suffix}")
|
base_filepath_no_ext = os.path.join(out_dir, f"{sanitized_base_name}{suffix}")
|
||||||
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||||
subtitle_entries = []
|
subtitle_entries = []
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
@@ -651,8 +714,13 @@ class ConversionThread(QThread):
|
|||||||
loaded_voice = self.voice
|
loaded_voice = self.voice
|
||||||
# Prepare per-chapter output file if needed
|
# Prepare per-chapter output file if needed
|
||||||
if save_chapters_separately and total_chapters > 1:
|
if save_chapters_separately and total_chapters > 1:
|
||||||
|
# First pass: keep alphanumeric, spaces, hyphens, and underscores
|
||||||
sanitized = re.sub(r"[^\w\s\-]", "", chapter_name)
|
sanitized = re.sub(r"[^\w\s\-]", "", chapter_name)
|
||||||
|
# Replace multiple spaces/hyphens with single underscore
|
||||||
sanitized = re.sub(r"[\s\-]+", "_", sanitized).strip("_")
|
sanitized = re.sub(r"[\s\-]+", "_", sanitized).strip("_")
|
||||||
|
# Apply OS-specific sanitization
|
||||||
|
sanitized = sanitize_name_for_os(sanitized, is_folder=False)
|
||||||
|
# Limit length (leaving room for the chapter number prefix)
|
||||||
MAX_LEN = 80
|
MAX_LEN = 80
|
||||||
if len(sanitized) > MAX_LEN:
|
if len(sanitized) > MAX_LEN:
|
||||||
pos = sanitized[:MAX_LEN].rfind("_")
|
pos = sanitized[:MAX_LEN].rfind("_")
|
||||||
|
|||||||
Reference in New Issue
Block a user