Initial Commit
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
scanner.py — Recursive EXR sequence discovery.
|
||||
|
||||
Uses a background QThread so the GUI stays responsive during scanning.
|
||||
The scan logic itself (_scan_folder) is kept free of Qt types so it can
|
||||
be tested independently.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
from parser import SEQ_FILE_RE, Shot, build_shot, parse_basename
|
||||
|
||||
|
||||
class ScanWorker(QThread):
|
||||
"""Scans one or more folders for EXR sequences in a background thread.
|
||||
|
||||
Signals
|
||||
-------
|
||||
log_message(str)
|
||||
Informational text suitable for display in a log window.
|
||||
scan_complete(list[Shot])
|
||||
Emitted when scanning finishes (always, even on error).
|
||||
"""
|
||||
|
||||
log_message: Signal = Signal(str)
|
||||
scan_complete: Signal = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
folders: list[str],
|
||||
output_root: str,
|
||||
parent=None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.folders = folders
|
||||
self.output_root = output_root
|
||||
|
||||
def run(self) -> None:
|
||||
shots: list[Shot] = []
|
||||
try:
|
||||
for folder in self.folders:
|
||||
self.log_message.emit(f"Scanning: {folder}")
|
||||
found = scan_folder(folder, self.output_root, self.log_message.emit)
|
||||
shots.extend(found)
|
||||
except Exception as exc:
|
||||
self.log_message.emit(f"ERROR during scan: {exc}")
|
||||
finally:
|
||||
self.log_message.emit(f"Scan complete — {len(shots)} sequence(s) found.")
|
||||
self.scan_complete.emit(shots)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Core scan logic (Qt-free, testable standalone)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def scan_folder(
|
||||
root: str,
|
||||
output_root: str,
|
||||
log: Optional[Callable[[str], None]] = None,
|
||||
) -> list[Shot]:
|
||||
"""Recursively scan *root* for EXR sequences.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
root:
|
||||
Top-level directory to walk.
|
||||
output_root:
|
||||
Base path used to derive output folder/movie/exr paths on each Shot.
|
||||
log:
|
||||
Optional callable that receives progress strings (e.g. ``print``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Shot]
|
||||
One Shot per discovered sequence, sorted by basename.
|
||||
"""
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
if log:
|
||||
log(msg)
|
||||
|
||||
# Collect files: dir → basename → [(frame_int, frame_str)]
|
||||
dir_seqs: dict[str, dict[str, list[tuple[int, str]]]] = defaultdict(
|
||||
lambda: defaultdict(list)
|
||||
)
|
||||
|
||||
for dirpath, _dirs, files in os.walk(root):
|
||||
for fname in files:
|
||||
m = SEQ_FILE_RE.match(fname)
|
||||
if not m:
|
||||
continue
|
||||
basename, frame_str = m.group(1), m.group(2)
|
||||
dir_seqs[dirpath][basename].append((int(frame_str), frame_str))
|
||||
|
||||
shots: list[Shot] = []
|
||||
|
||||
for dirpath in sorted(dir_seqs):
|
||||
for basename in sorted(dir_seqs[dirpath]):
|
||||
parsed = parse_basename(basename)
|
||||
if parsed is None:
|
||||
_log(f" Skipping (unrecognised name): {basename}")
|
||||
continue
|
||||
|
||||
frame_list = dir_seqs[dirpath][basename]
|
||||
frames = [f for f, _ in frame_list]
|
||||
# Detect padding length from the first filename encountered
|
||||
frame_padding = len(frame_list[0][1])
|
||||
|
||||
_log(
|
||||
f" Found: {basename} "
|
||||
f"({len(frames)} frames, padding={frame_padding})"
|
||||
)
|
||||
|
||||
shot = build_shot(
|
||||
basename=basename,
|
||||
source_dir=dirpath,
|
||||
frames=frames,
|
||||
output_root=output_root,
|
||||
parsed=parsed,
|
||||
frame_padding=frame_padding,
|
||||
)
|
||||
shots.append(shot)
|
||||
|
||||
return shots
|
||||
Reference in New Issue
Block a user