96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Remove JDownloader auto-extraction cruft from the romhacks library.
|
|
|
|
For each library/<game>/ 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()
|