Initial commit. Basic featues are implemented
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user