initial commit
This commit is contained in:
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
symlink_downloads.py — place in the "Sven Co-op" root folder alongside tags.json.
|
||||
|
||||
Workflow:
|
||||
1. Scan svencoop_downloads/models/player/ for .mdl files whose stem matches a
|
||||
model ID in tags.json (case-insensitive).
|
||||
2. For each match, create a relative symlink in svencoop_addon/models/player/:
|
||||
<id>/<id>.mdl -> ../../../../svencoop/models/player/helmet/helmet.mdl
|
||||
3. Show matched folders, then prompt to delete them from svencoop_downloads.
|
||||
The player/ root itself is never deleted.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
HELMET_REL = Path("svencoop") / "models" / "player" / "helmet" / "helmet.mdl"
|
||||
ADDON_PLAYERS = Path("svencoop_addon") / "models" / "player"
|
||||
DL_PLAYERS = Path("svencoop_downloads") / "models" / "player"
|
||||
SYMLINK_TARGET = Path("..") / ".." / ".." / ".." / HELMET_REL
|
||||
|
||||
|
||||
def write_symlink(mdl_path: Path) -> str:
|
||||
"""Create or update the symlink. Returns 'created', 'replaced', or 'ok'."""
|
||||
target_str = str(SYMLINK_TARGET)
|
||||
if mdl_path.is_symlink():
|
||||
if os.readlink(mdl_path) == target_str:
|
||||
return "ok"
|
||||
mdl_path.unlink()
|
||||
elif mdl_path.exists():
|
||||
mdl_path.unlink()
|
||||
mdl_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
os.symlink(target_str, mdl_path)
|
||||
return "created"
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
tags_path = script_dir / "tags.json"
|
||||
|
||||
if not tags_path.exists():
|
||||
sys.exit(f"tags.json not found next to script ({tags_path})")
|
||||
|
||||
helmet_abs = script_dir / HELMET_REL
|
||||
if not helmet_abs.exists():
|
||||
sys.exit(f"helmet.mdl not found: {helmet_abs}\nRun from the Sven Co-op root folder.")
|
||||
|
||||
dl_root = script_dir / DL_PLAYERS
|
||||
if not dl_root.is_dir():
|
||||
sys.exit(f"Downloads folder not found: {dl_root}")
|
||||
|
||||
addon_root = script_dir / ADDON_PLAYERS
|
||||
|
||||
with open(tags_path, encoding="utf-8") as f:
|
||||
tags: dict[str, list[str]] = json.load(f)
|
||||
|
||||
# Build case-insensitive lookup: lowercase stem -> canonical tags.json ID
|
||||
tag_lookup: dict[str, str] = {
|
||||
model_id.lower(): model_id
|
||||
for model_ids in tags.values()
|
||||
for model_id in model_ids
|
||||
}
|
||||
|
||||
# Scan downloads: collect all .mdl paths and identify matches
|
||||
# folder_mdls: folder -> all .mdl files in that folder
|
||||
# matched: list of (dl_mdl_path, canonical_tags_id)
|
||||
folder_mdls: dict[Path, list[Path]] = defaultdict(list)
|
||||
matched: list[tuple[Path, str]] = []
|
||||
|
||||
for mdl_path in sorted(dl_root.rglob("*.mdl")):
|
||||
parent = mdl_path.parent
|
||||
if parent == dl_root:
|
||||
continue # skip .mdl files sitting directly in player/ (no subfolder to delete)
|
||||
folder_mdls[parent].append(mdl_path)
|
||||
tags_id = tag_lookup.get(mdl_path.stem.lower())
|
||||
if tags_id:
|
||||
matched.append((mdl_path, tags_id))
|
||||
|
||||
if not matched:
|
||||
print("No downloaded .mdl files matched any model ID in tags.json.")
|
||||
return
|
||||
|
||||
# --- Symlink pass ---
|
||||
print(f"{'[DRY RUN] ' if dry_run else ''}Symlinking {len(matched)} matched model(s):\n")
|
||||
counts = {"created": 0, "replaced": 0, "ok": 0, "error": 0}
|
||||
|
||||
for dl_mdl, tags_id in matched:
|
||||
addon_mdl = addon_root / tags_id / f"{tags_id}.mdl"
|
||||
if dry_run:
|
||||
existing = "ok" if (addon_mdl.is_symlink() and os.readlink(addon_mdl) == str(SYMLINK_TARGET)) else "would link"
|
||||
print(f" {existing:<12} {tags_id} (from {dl_mdl.relative_to(dl_root)})")
|
||||
counts["created"] += 1
|
||||
else:
|
||||
try:
|
||||
result = write_symlink(addon_mdl)
|
||||
counts[result] += 1
|
||||
label = {"created": "linked ", "replaced": "replaced ", "ok": "already ok"}.get(result, result)
|
||||
print(f" {label} {tags_id}")
|
||||
except OSError as exc:
|
||||
counts["error"] += 1
|
||||
print(f" ERROR {tags_id}: {exc}")
|
||||
|
||||
if not dry_run:
|
||||
print(
|
||||
f"\n {counts['created']} created, {counts['replaced']} replaced, "
|
||||
f"{counts['ok']} already correct, {counts['error']} errors"
|
||||
)
|
||||
|
||||
# --- Cleanup prompt ---
|
||||
# Any folder containing at least one tagged .mdl is deleted entirely.
|
||||
# The player/ root itself is never deleted.
|
||||
folders_to_clean: dict[Path, list[Path]] = defaultdict(list)
|
||||
for dl_mdl, _ in matched:
|
||||
folders_to_clean[dl_mdl.parent].append(dl_mdl)
|
||||
|
||||
print(f"\nFolders to delete from svencoop_downloads ({len(folders_to_clean)}):\n")
|
||||
for folder in sorted(folders_to_clean):
|
||||
matched_here = folders_to_clean[folder]
|
||||
other = [p for p in folder_mdls[folder] if p not in matched_here]
|
||||
print(f" {folder.relative_to(script_dir)}/")
|
||||
for m in matched_here:
|
||||
print(f" [tagged] {m.name}")
|
||||
for o in other:
|
||||
print(f" [extra ] {o.name}")
|
||||
|
||||
print()
|
||||
if dry_run:
|
||||
print("[DRY RUN] Skipping delete prompt.")
|
||||
return
|
||||
|
||||
try:
|
||||
answer = input("Delete these folders from svencoop_downloads? [y/N] ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nAborted.")
|
||||
return
|
||||
|
||||
if answer != "y":
|
||||
print("Skipped cleanup.")
|
||||
return
|
||||
|
||||
deleted = 0
|
||||
for folder in sorted(folders_to_clean):
|
||||
if folder == dl_root:
|
||||
print(f" SKIP (is player/ root): {folder}")
|
||||
continue
|
||||
try:
|
||||
shutil.rmtree(folder)
|
||||
print(f" deleted {folder.relative_to(script_dir)}/")
|
||||
deleted += 1
|
||||
except OSError as exc:
|
||||
print(f" ERROR {folder.name}: {exc}")
|
||||
|
||||
print(f"\nDeleted {deleted} folder(s).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user