172 lines
5.2 KiB
Python
172 lines
5.2 KiB
Python
"""General‑purpose helper functions."""
|
||
|
||
import re
|
||
|
||
|
||
def parse_timestamp(timestamp: str) -> int:
|
||
"""
|
||
Convert a timestamp in 'mm:ss' or 'HH:MM:SS' format to total seconds.
|
||
|
||
Args:
|
||
timestamp: String in the format 'mm:ss' or 'HH:MM:SS'.
|
||
|
||
Returns:
|
||
Total number of seconds.
|
||
|
||
Raises:
|
||
ValueError: If the format is unrecognized.
|
||
"""
|
||
parts = timestamp.strip().split(':')
|
||
if len(parts) == 2:
|
||
minutes, seconds = map(int, parts)
|
||
return minutes * 60 + seconds
|
||
elif len(parts) == 3:
|
||
hours, minutes, seconds = map(int, parts)
|
||
return hours * 3600 + minutes * 60 + seconds
|
||
else:
|
||
raise ValueError(f"Invalid timestamp format: {timestamp}")
|
||
|
||
|
||
def format_time(seconds: int) -> str:
|
||
"""
|
||
Convert seconds to 'HH:MM:SS' format for FFmpeg.
|
||
|
||
Args:
|
||
seconds: Total number of seconds.
|
||
|
||
Returns:
|
||
Time string in 'HH:MM:SS' format.
|
||
"""
|
||
hours = seconds // 3600
|
||
minutes = (seconds % 3600) // 60
|
||
secs = seconds % 60
|
||
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
||
|
||
|
||
def sanitize_filename(name: str) -> str:
|
||
"""
|
||
Remove characters that are problematic on common filesystems.
|
||
|
||
This is a fallback sanitizer; the main replacement is handled by
|
||
apply_replacement() when --replace-bad-chars is used.
|
||
|
||
Args:
|
||
name: Original filename candidate.
|
||
|
||
Returns:
|
||
Sanitized string with dangerous characters replaced by '_'.
|
||
"""
|
||
# Replace characters that are forbidden on Windows/Linux/macOS.
|
||
return re.sub(r'[<>:"/\\|?*]', '_', name).strip()
|
||
|
||
|
||
def apply_replacement(name: str, bad_chars: str, replacement_char: str) -> str:
|
||
"""
|
||
Replace every occurrence of any character in bad_chars with replacement_char.
|
||
|
||
Args:
|
||
name: The original string.
|
||
bad_chars: String containing all characters to replace.
|
||
replacement_char: The character to insert.
|
||
|
||
Returns:
|
||
The transformed string.
|
||
"""
|
||
if not bad_chars:
|
||
return name
|
||
# Build a translation table mapping each bad character to the replacement.
|
||
translation_table = str.maketrans({c: replacement_char for c in set(bad_chars)})
|
||
return name.translate(translation_table)
|
||
|
||
|
||
def cleanup_good_chars(name: str, good_char: str) -> str:
|
||
"""
|
||
Clean up the filename after replacement to avoid awkward sequences.
|
||
|
||
Steps performed:
|
||
1. Strip leading/trailing good_char characters.
|
||
2. Collapse consecutive good_char characters into a single one.
|
||
3. Remove good_char characters that are adjacent to a single non‑good_char
|
||
(i.e., pattern good_char + X + good_char becomes just X),
|
||
and repeat this step until no more such patterns exist.
|
||
4. After each iteration, re‑apply stripping and collapsing.
|
||
|
||
This makes filenames more human‑readable, e.g.:
|
||
"author_-_song" -> "author-song"
|
||
"hello__world" -> "hello_world"
|
||
"_hello_" -> "hello"
|
||
|
||
Args:
|
||
name: The string to clean (after replacement).
|
||
good_char: The character used as replacement.
|
||
|
||
Returns:
|
||
The cleaned string.
|
||
"""
|
||
# Escape the good_char for regex usage.
|
||
escaped = re.escape(good_char)
|
||
|
||
# Helper to collapse consecutive good chars and strip edges.
|
||
def collapse_and_strip(s: str) -> str:
|
||
# Collapse multiple consecutive good chars into one.
|
||
s = re.sub(rf'{escaped}+', good_char, s)
|
||
# Remove leading/trailing good chars.
|
||
s = s.strip(good_char)
|
||
return s
|
||
|
||
# Apply stripping and collapsing initially.
|
||
name = collapse_and_strip(name)
|
||
|
||
# Remove patterns: good_char + X + good_char (where X is any char != good_char).
|
||
# Repeat until no further changes.
|
||
while True:
|
||
# Replace pattern: good_char (non-good-char) good_char -> just the non-good-char.
|
||
# The negative lookahead ensures X is not the good_char.
|
||
pattern = rf'{escaped}([^{escaped}]){escaped}'
|
||
new_name = re.sub(pattern, r'\1', name)
|
||
if new_name == name:
|
||
break
|
||
name = new_name
|
||
# Re‑apply stripping and collapsing after removal.
|
||
name = collapse_and_strip(name)
|
||
|
||
return name
|
||
|
||
def apply_template(template: str, placeholders: dict) -> str:
|
||
"""
|
||
Replace placeholders in a template string with values from a dict.
|
||
|
||
Placeholders are of the form %key (e.g., %tn, %an).
|
||
Use %% to escape a literal percent sign.
|
||
|
||
Args:
|
||
template: The template string.
|
||
placeholders: Dict mapping placeholder names to values.
|
||
|
||
Returns:
|
||
The rendered string.
|
||
"""
|
||
import re
|
||
result = []
|
||
i = 0
|
||
while i < len(template):
|
||
ch = template[i]
|
||
if ch == '%':
|
||
if i + 1 < len(template) and template[i + 1] == '%':
|
||
result.append('%')
|
||
i += 2
|
||
continue
|
||
match = re.match(r'%([a-zA-Z]+)', template[i:])
|
||
if match:
|
||
key = match.group(1)
|
||
value = placeholders.get(key, '')
|
||
result.append(value)
|
||
i += len(match.group(0))
|
||
else:
|
||
result.append(ch)
|
||
i += 1
|
||
else:
|
||
result.append(ch)
|
||
i += 1
|
||
return ''.join(result)
|