#!/usr/bin/env python3 """Remove JDownloader auto-extraction cruft from the romhacks library. For each library// folder, list every archive's contents (bsdtar) and delete only the loose files whose path exactly matches an archive entry — i.e. files that are reproducible by re-extracting an archive that is STILL PRESENT. Archives themselves, art, guides/spreadsheets, and anything NOT contained in an archive are always kept. If a folder has no archive, nothing is deleted (the loose files are the only copy). Also audits for possible data loss: flags any folder that ends up with neither a source archive nor a rom file. CLEAN_DRY=true (default) previews only. """ import os import pathlib import subprocess import sys LIBRARY = pathlib.Path(os.environ.get("LIBRARY_DIR", "/library")) DRY = os.environ.get("CLEAN_DRY", "true").lower() != "false" ARCHIVE_EXTS = {".zip", ".rar", ".7z"} ROM_EXTS = {".gba", ".gb", ".gbc", ".nds", ".3ds", ".cia", ".nsp", ".xci"} def listing(path): try: out = subprocess.run(["bsdtar", "-tf", str(path)], capture_output=True, text=True, timeout=300) return [l for l in out.stdout.splitlines() if l.strip()] except Exception: return [] def main(): total_files = 0 total_bytes = 0 flagged = [] for folder in sorted(p for p in LIBRARY.iterdir() if p.is_dir()): archives = [p for p in folder.rglob("*") if p.is_file() and p.suffix.lower() in ARCHIVE_EXTS] # Resolve the set of paths each archive would extract to (relative to the # archive's own directory) — these are the reproducible files. extracted = set() for a in archives: for entry in listing(a): extracted.add((a.parent / entry).resolve()) del_files = [p for p in folder.rglob("*") if p.is_file() and p.suffix.lower() not in ARCHIVE_EXTS and p.resolve() in extracted] nbytes = sum(p.stat().st_size for p in del_files) # Audit: will the folder still hold a recoverable rom (an archive, or a # loose rom we are NOT deleting)? keeps = bool(archives) or any( p.suffix.lower() in ROM_EXTS and p not in del_files for p in folder.rglob("*") if p.is_file()) if not keeps: flagged.append(folder.name) if del_files: verb = "would delete" if DRY else "deleting" print(f"[clean] {folder.name}: {verb} {len(del_files)} file(s) " f"{nbytes/1048576:.0f}MB (keeping {len(archives)} archive(s))") total_files += len(del_files) total_bytes += nbytes if not DRY: for p in del_files: try: p.unlink() except Exception as e: print(f" unlink fail {p}: {e}", file=sys.stderr) if not DRY: for d in sorted((p for p in folder.rglob("*") if p.is_dir()), key=lambda x: len(x.parts), reverse=True): try: d.rmdir() except OSError: pass print(f"\n[clean] {'WOULD reclaim' if DRY else 'reclaimed'} " f"{total_bytes/1073741824:.2f} GB across {total_files} files; DRY={DRY}") if flagged: print(f"[clean] WARNING — {len(flagged)} folder(s) with NO archive and " f"NO rom (possible loss / check these):") for f in flagged: print(" ", f) else: print("[clean] audit OK — every folder retains an archive or rom.") if __name__ == "__main__": main()