Files
SymlinkModels/symlink_downloads.py
T

164 lines
5.7 KiB
Python
Raw Normal View History

2026-06-22 06:44:43 -04:00
#!/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()