rename: recurse subdirs, move already-renamed files to correct folder

When races_dir is set:
- os.walk scans all subdirectories of the source
- already-renamed files (both tokens present) are also moved if not in
  the right races/<track>/<session>/ folder
- files already in the correct place are skipped with 'already in place'

GUI remembers last rename source folder separately from other modes.
This commit is contained in:
Andrew Pimenov 2026-06-18 01:58:21 +03:00
parent 818bda26cb
commit 351e9cdf69
3 changed files with 83 additions and 23 deletions

View file

@ -196,9 +196,11 @@ class _App:
self._log_queue.put(None) self._log_queue.put(None)
def _on_rename(self) -> None: def _on_rename(self) -> None:
d = filedialog.askdirectory(title='Select folder to rename', initialdir=_RACES_DIR) init = self._last_dir.get('rename', os.path.expanduser('~'))
d = filedialog.askdirectory(title='Select folder with CSV files', initialdir=init)
if not d: if not d:
return return
self._last_dir['rename'] = d
self._set_busy(True) self._set_busy(True)
threading.Thread(target=self._worker_rename, args=(d,), daemon=True).start() threading.Thread(target=self._worker_rename, args=(d,), daemon=True).start()
@ -209,7 +211,8 @@ class _App:
self._push('No CSV files found') self._push('No CSV files found')
for r in results: for r in results:
if r.status == 'renamed': if r.status == 'renamed':
self._push(f'renamed: {r.old} -> {r.new}') new_display = os.path.relpath(r.new, _RACES_DIR) if r.new and r.new.startswith(_RACES_DIR) else r.new
self._push(f'renamed: {r.old} -> {new_display}')
elif r.status == 'skipped': elif r.status == 'skipped':
self._push(f'skipped: {r.old} ({r.detail})') self._push(f'skipped: {r.old} ({r.detail})')
else: else:

View file

@ -115,9 +115,15 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
for t in targets: for t in targets:
t = os.path.abspath(t) t = os.path.abspath(t)
if os.path.isdir(t): if os.path.isdir(t):
for name in os.listdir(t): if races_dir is not None:
if name.lower().endswith('.csv'): for root, _, files in os.walk(t):
paths.append(os.path.join(t, name)) for fname in sorted(files):
if fname.lower().endswith('.csv'):
paths.append(os.path.join(root, fname))
else:
for fname in os.listdir(t):
if fname.lower().endswith('.csv'):
paths.append(os.path.join(t, fname))
else: else:
paths.append(t) paths.append(t)
@ -131,9 +137,22 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
results.append(RenameResult(name, None, 'error', 'not a CSV file')) results.append(RenameResult(name, None, 'error', 'not a CSV file'))
continue continue
stem = os.path.splitext(name)[0] stem = os.path.splitext(name)[0]
if races_dir is None:
if _is_processed(stem): if _is_processed(stem):
results.append(RenameResult(name, None, 'skipped', 'already processed')) results.append(RenameResult(name, None, 'skipped', 'already processed'))
continue continue
try:
lap, session_type, _ = read_metadata(p)
except Exception as e:
results.append(RenameResult(name, None, 'error', str(e)))
continue
if not session_type:
results.append(RenameResult(name, None, 'error', 'session type not found in CSV'))
continue
new_name = insert_tokens(name, session_type, lap)
dest = os.path.join(os.path.dirname(p), new_name)
else:
try: try:
lap, session_type, track_name = read_metadata(p) lap, session_type, track_name = read_metadata(p)
except Exception as e: except Exception as e:
@ -142,14 +161,15 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
if not session_type: if not session_type:
results.append(RenameResult(name, None, 'error', 'session type not found in CSV')) results.append(RenameResult(name, None, 'error', 'session type not found in CSV'))
continue continue
new_name = insert_tokens(name, session_type, lap) new_name = name if _is_processed(stem) else insert_tokens(name, session_type, lap)
if races_dir is not None:
dest_dir = os.path.join(races_dir, _safe_dirname(track_name), _session_folder(session_type)) dest_dir = os.path.join(races_dir, _safe_dirname(track_name), _session_folder(session_type))
os.makedirs(dest_dir, exist_ok=True)
else:
dest_dir = os.path.dirname(p)
dest = os.path.join(dest_dir, new_name) dest = os.path.join(dest_dir, new_name)
if os.path.exists(dest): if os.path.abspath(p) == os.path.abspath(dest):
results.append(RenameResult(name, None, 'skipped', 'already in place'))
continue
os.makedirs(dest_dir, exist_ok=True)
if os.path.exists(dest) and os.path.abspath(dest) != os.path.abspath(p):
results.append(RenameResult(name, dest, 'error', 'target exists')) results.append(RenameResult(name, dest, 'error', 'target exists'))
continue continue
try: try:

View file

@ -139,3 +139,40 @@ def test_rename_unprocessed_moves_to_races_dir(tmp_path, sample_path):
assert r.new.startswith(races_dir) assert r.new.startswith(races_dir)
assert 'Practice' in r.new assert 'Practice' in r.new
assert not os.path.exists(src) assert not os.path.exists(src)
def test_rename_unprocessed_moves_already_renamed(tmp_path, sample_path):
"""Already-renamed file in wrong folder gets moved to correct folder."""
src = str(tmp_path / "zandvoort_P1_L7_74.074_ferrari.csv")
shutil.copy(sample_path, src)
races_dir = str(tmp_path / 'races')
results = rename_unprocessed([src], races_dir=races_dir)
assert len(results) == 1
r = results[0]
assert r.status == 'renamed'
assert 'Practice' in r.new
assert os.path.exists(r.new)
assert not os.path.exists(src)
def test_rename_unprocessed_skips_already_in_place(tmp_path, sample_path):
"""File already in correct folder is skipped on second run."""
src = str(tmp_path / "zandvoort_74.074_ferrari.csv")
shutil.copy(sample_path, src)
races_dir = str(tmp_path / 'races')
rename_unprocessed([src], races_dir=races_dir)
results = rename_unprocessed([races_dir], races_dir=races_dir)
assert all(r.status == 'skipped' for r in results)
def test_rename_unprocessed_walks_subdirs(tmp_path, sample_path):
"""With races_dir, scans subdirectories recursively."""
sub = tmp_path / 'sub' / 'nested'
sub.mkdir(parents=True)
src = str(sub / "zandvoort_74.074_ferrari.csv")
shutil.copy(sample_path, src)
races_dir = str(tmp_path / 'races')
results = rename_unprocessed([str(tmp_path)], races_dir=races_dir)
assert len(results) == 1
assert results[0].status == 'renamed'
assert 'Practice' in results[0].new