Files
HS-Rename/engine/file_scan.py
T
Bulk RenamerandCursor 85dc873e16 Release v1.0.8: recursive folder scan and file type filters.
Adds subfolder inclusion and extension presets so the preview list can target nested media without loading everything.

Co-authored-by: Cursor <[email protected]>
2026-07-03 17:46:58 -05:00

87 lines
2.4 KiB
Python

"""Scan folders for rename candidates with optional recursion and extension filters."""
from __future__ import annotations
import os
import re
from pathlib import Path
from .pipeline import UNDO_FILENAME
FILE_TYPE_PRESETS: dict[str, str] = {
"All files": "",
"Video": ".mkv,.mp4,.avi,.webm,.m4v,.mov",
"Audio": ".mp3,.flac,.wav,.ogg,.m4a,.aac",
"Images": ".jpg,.jpeg,.png,.gif,.webp,.bmp",
}
def parse_extension_filter(spec: str) -> frozenset[str] | None:
"""
Parse a user extension filter into a set of lowercase extensions with a leading dot.
Returns None when empty (match all files).
"""
spec = spec.strip()
if not spec:
return None
exts: set[str] = set()
for part in re.split(r"[,;\s]+", spec):
part = part.strip().lower()
if not part:
continue
if part.startswith("*."):
part = part[1:]
if not part.startswith("."):
part = "." + part
exts.add(part)
return frozenset(exts) if exts else None
def file_extension(rel_path: str) -> str:
"""Return the lowercase extension (with dot) of the basename, or '' if none."""
name = Path(rel_path).name
if "." in name and not name.startswith("."):
return "." + name.rsplit(".", 1)[-1].lower()
return ""
def matches_extension(rel_path: str, extensions: frozenset[str] | None) -> bool:
if extensions is None:
return True
return file_extension(rel_path) in extensions
def list_files(
base_dir: str,
*,
recursive: bool = False,
extensions: frozenset[str] | None = None,
) -> list[str]:
"""Return sorted paths relative to base_dir."""
base = Path(base_dir)
if not base.is_dir():
return []
found: list[str] = []
def consider(path: Path) -> None:
if not path.is_file():
return
if path.name.startswith("."):
return
if path.name == UNDO_FILENAME:
return
rel = path.relative_to(base).as_posix()
if matches_extension(rel, extensions):
found.append(rel)
if recursive:
for dirpath, dirnames, filenames in os.walk(base):
dirnames[:] = sorted(d for d in dirnames if not d.startswith("."))
for fn in sorted(filenames):
consider(Path(dirpath) / fn)
else:
for entry in sorted(base.iterdir(), key=lambda p: p.name.lower()):
consider(entry)
return sorted(found, key=str.lower)