Add wildcard and regex modes to Bulk Rename
Plain-text substring matching couldn't handle a batch of slightly-different patterns (e.g. several release-group suffixes) in one pass. Add a match-type selector: wildcard (shell-style */?) for the common case, and full regex (with backreferences in the replacement) for anything more precise. Invalid regex is caught and reported inline instead of failing the request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+62
-12
@@ -3433,18 +3433,62 @@ def _iter_all_directories(directory: Path, hidden_set: set) -> Iterator[Path]:
|
||||
yield from _iter_all_directories(entry, hidden_set)
|
||||
|
||||
|
||||
def find_bulk_rename_matches(library_root: str, pattern: str, replacement: str) -> List[Dict[str, str]]:
|
||||
"""Every directory (course or folder) in the library whose name contains `pattern`."""
|
||||
def _wildcard_to_regex(pattern: str) -> str:
|
||||
"""
|
||||
Translate a shell-style wildcard pattern into a regex fragment for
|
||||
*substring* matching (not a full-name match, unlike fnmatch) - '*'
|
||||
matches any run of characters, '?' matches exactly one, everything
|
||||
else is matched literally. Lets ".BOOKWARE*" catch
|
||||
".BOOKWARE-GETH", ".BOOKWARE-BOOKTIME", ".BOOKWARE-BLZiSO", etc. in
|
||||
one pattern instead of one bulk rename per exact suffix.
|
||||
"""
|
||||
parts = []
|
||||
for ch in pattern:
|
||||
if ch == '*':
|
||||
parts.append('.*')
|
||||
elif ch == '?':
|
||||
parts.append('.')
|
||||
else:
|
||||
parts.append(re.escape(ch))
|
||||
return ''.join(parts)
|
||||
|
||||
|
||||
def find_bulk_rename_matches(library_root: str, pattern: str, replacement: str,
|
||||
mode: str = 'plain') -> List[Dict[str, str]]:
|
||||
"""
|
||||
Every directory (course or folder) in the library whose name matches
|
||||
`pattern`, paired with what it would become. Three modes:
|
||||
- 'plain' (default): `pattern` is matched/replaced as a literal
|
||||
substring, same as before.
|
||||
- 'wildcard': `pattern` uses shell-style '*'/'?' wildcards (see
|
||||
_wildcard_to_regex); `replacement` is literal text.
|
||||
- 'regex': `pattern` is a Python regex, matched with re.search and
|
||||
replaced with re.sub - `replacement` may use backreferences like
|
||||
\\1. Raises re.error if `pattern` isn't valid.
|
||||
All three replace every match in the name, not just the first, mirroring
|
||||
how plain mode's str.replace() already behaved.
|
||||
"""
|
||||
hidden_set = set(get_hidden_paths())
|
||||
return [
|
||||
{
|
||||
'path': str(directory),
|
||||
'old_name': directory.name,
|
||||
'new_name': directory.name.replace(pattern, replacement)
|
||||
}
|
||||
for directory in _iter_all_directories(Path(library_root), hidden_set)
|
||||
if pattern in directory.name
|
||||
]
|
||||
directories = _iter_all_directories(Path(library_root), hidden_set)
|
||||
|
||||
if mode == 'plain':
|
||||
return [
|
||||
{'path': str(directory), 'old_name': directory.name,
|
||||
'new_name': directory.name.replace(pattern, replacement)}
|
||||
for directory in directories
|
||||
if pattern in directory.name
|
||||
]
|
||||
|
||||
regex = re.compile(pattern if mode == 'regex' else _wildcard_to_regex(pattern))
|
||||
matches = []
|
||||
for directory in directories:
|
||||
if regex.search(directory.name):
|
||||
matches.append({
|
||||
'path': str(directory),
|
||||
'old_name': directory.name,
|
||||
'new_name': regex.sub(replacement, directory.name),
|
||||
})
|
||||
return matches
|
||||
|
||||
|
||||
@app.route('/api/bulk-rename/preview')
|
||||
@@ -3452,11 +3496,17 @@ def bulk_rename_preview_api():
|
||||
"""Preview a find/replace rename across the whole library, without touching disk."""
|
||||
pattern = request.args.get('pattern', '')
|
||||
replacement = request.args.get('replacement', '')
|
||||
mode = request.args.get('mode', 'plain')
|
||||
if not pattern:
|
||||
return jsonify({'error': 'pattern is required'}), 400
|
||||
if mode not in ('plain', 'wildcard', 'regex'):
|
||||
return jsonify({'error': 'Invalid mode'}), 400
|
||||
|
||||
library_root = os.path.abspath(get_library_root())
|
||||
matches = find_bulk_rename_matches(library_root, pattern, replacement)
|
||||
try:
|
||||
matches = find_bulk_rename_matches(library_root, pattern, replacement, mode)
|
||||
except re.error as e:
|
||||
return jsonify({'error': f'Invalid pattern: {e}'}), 400
|
||||
return jsonify({'matches': matches})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user