add better dashboard with full snake game board
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import urlopen
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
BASE_URL = "https://media.battlesnake.com/"
|
||||
S3_NS = {"s3": "http://doc.s3.amazonaws.com/2006-03-01"}
|
||||
DEFAULT_PREFIXES = ("snakes/heads/", "snakes/tails/")
|
||||
ALLOWED_EXTENSIONS = {".svg", ".png", ".webp"}
|
||||
|
||||
def build_list_url(prefix:str, marker:str|None) -> str:
|
||||
query = {"prefix": prefix}
|
||||
if marker:
|
||||
query["marker"] = marker
|
||||
return f"{BASE_URL}?{urlencode(query)}"
|
||||
|
||||
def list_keys_for_prefix(prefix:str) -> list[str]:
|
||||
keys: list[str] = []
|
||||
marker: str | None = None
|
||||
|
||||
while True:
|
||||
url = build_list_url(prefix=prefix, marker=marker)
|
||||
with urlopen(url) as response:
|
||||
xml_bytes = response.read()
|
||||
|
||||
root = ET.fromstring(xml_bytes)
|
||||
for key_node in root.findall("s3:Contents/s3:Key", S3_NS):
|
||||
key = (key_node.text or "").strip()
|
||||
if key and not key.endswith("/"):
|
||||
keys.append(key)
|
||||
|
||||
truncated_text = (
|
||||
root.findtext("s3:IsTruncated", default="false", namespaces=S3_NS)
|
||||
or "false"
|
||||
).lower()
|
||||
is_truncated = truncated_text == "true"
|
||||
if not is_truncated:
|
||||
break
|
||||
|
||||
next_marker = (
|
||||
root.findtext("s3:NextMarker", default="", namespaces=S3_NS) or ""
|
||||
).strip()
|
||||
if next_marker:
|
||||
marker = next_marker
|
||||
continue
|
||||
|
||||
last_key = keys[-1] if keys else None
|
||||
if not last_key:
|
||||
break
|
||||
marker = last_key
|
||||
|
||||
return keys
|
||||
|
||||
def keep_customization_key(key:str) -> bool:
|
||||
if not key.startswith(DEFAULT_PREFIXES):
|
||||
return False
|
||||
suffix = Path(key).suffix.lower()
|
||||
return suffix in ALLOWED_EXTENSIONS
|
||||
|
||||
def to_output_path(output_root:Path, key:str) -> Path:
|
||||
if key.startswith("snakes/heads/"):
|
||||
relative = key.removeprefix("snakes/")
|
||||
elif key.startswith("snakes/tails/"):
|
||||
relative = key.removeprefix("snakes/")
|
||||
else:
|
||||
relative = key
|
||||
return output_root / relative
|
||||
|
||||
def download_file(url:str, output_file:Path) -> None:
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with urlopen(url) as response, output_file.open("wb") as target:
|
||||
shutil.copyfileobj(response, target)
|
||||
|
||||
def prune_output(output_root:Path, wanted_files:set[Path]) -> int:
|
||||
removed = 0
|
||||
if not output_root.exists():
|
||||
return 0
|
||||
|
||||
for file_path in output_root.rglob("*"):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
if file_path not in wanted_files:
|
||||
file_path.unlink()
|
||||
removed += 1
|
||||
|
||||
for directory in sorted(
|
||||
(p for p in output_root.rglob("*") if p.is_dir()), reverse=True
|
||||
):
|
||||
if any(directory.iterdir()):
|
||||
continue
|
||||
directory.rmdir()
|
||||
|
||||
return removed
|
||||
|
||||
def collect_customization_keys(prefixes:Iterable[str]) -> list[str]:
|
||||
all_keys: list[str] = []
|
||||
for prefix in prefixes:
|
||||
all_keys.extend(list_keys_for_prefix(prefix))
|
||||
return [key for key in sorted(set(all_keys)) if keep_customization_key(key)]
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Download Battlesnake snake customization assets (heads/tails) from media.battlesnake.com",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="data/battlesnake-customizations",
|
||||
help="Output directory (default: data/battlesnake-customizations)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite files that already exist",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prune",
|
||||
action="store_true",
|
||||
help="Delete files in output directory that are not snake customizations",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
output_root = Path(args.output).resolve()
|
||||
|
||||
keys = collect_customization_keys(DEFAULT_PREFIXES)
|
||||
if not keys:
|
||||
print("No customization files found.")
|
||||
return
|
||||
|
||||
downloaded = 0
|
||||
skipped = 0
|
||||
wanted_files: set[Path] = set()
|
||||
|
||||
for key in keys:
|
||||
file_url = f"{BASE_URL}{key}"
|
||||
output_file = to_output_path(output_root, key)
|
||||
wanted_files.add(output_file)
|
||||
|
||||
if output_file.exists() and not args.overwrite:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
download_file(file_url, output_file)
|
||||
downloaded += 1
|
||||
|
||||
removed = prune_output(output_root, wanted_files) if args.prune else 0
|
||||
|
||||
print(f"Output directory : {output_root}")
|
||||
print(f"Files discovered : {len(keys)}")
|
||||
print(f"Downloaded : {downloaded}")
|
||||
print(f"Skipped existing : {skipped}")
|
||||
if args.prune:
|
||||
print(f"Removed non-customization files: {removed}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user