Download badge and boss sprite images locally during export

The seed export command now downloads badge images and boss sprites
from remote URLs and stores them in frontend/public/, rewriting the
JSON URLs to local paths. Sprites are namespaced by game version
(e.g. /boss-sprites/red/brock.png) so each generation can have
its own sprite style.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 22:13:30 +01:00
parent d2144e47bf
commit 8bd4ad1ecf
126 changed files with 3948 additions and 194 deletions

View File

@@ -1,6 +1,8 @@
"""Seed runner — reads JSON files and upserts into the database."""
import json
import re
import urllib.request
from pathlib import Path
from sqlalchemy import func, select
@@ -403,11 +405,58 @@ async def _export_routes(session: AsyncSession, vg_data: dict):
print(f"Routes: {exported} game files exported")
FRONTEND_PUBLIC = Path(__file__).resolve().parents[4] / "frontend" / "public"
def _slugify(name: str) -> str:
"""Convert a name to a filename-safe slug: lowercase, hyphens, no special chars."""
slug = name.lower().replace(" ", "-")
slug = re.sub(r"[^a-z0-9-]", "", slug)
return slug
def _download_image(
url: str,
output_dir: Path,
slug: str,
downloaded: set[str],
) -> str:
"""Download an image to output_dir/slug.ext if not already downloaded.
Returns the local path (relative to frontend/public).
"""
url_ext = url.rsplit(".", 1)[-1].split("?")[0].lower()
if url_ext in ("png", "jpg", "jpeg", "gif", "webp", "svg"):
ext = f".{url_ext}"
else:
ext = ".png"
filename = f"{slug}{ext}"
dest = output_dir / filename
if filename not in downloaded:
output_dir.mkdir(parents=True, exist_ok=True)
req = urllib.request.Request(url, headers={"User-Agent": "nuzlocke-tracker/1.0"})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
dest.write_bytes(resp.read())
except (urllib.error.URLError, OSError) as exc:
print(f" Warning: failed to download {url}: {exc}")
return url
downloaded.add(filename)
print(f" Downloaded: {dest.relative_to(FRONTEND_PUBLIC)}")
return f"/{dest.relative_to(FRONTEND_PUBLIC)}"
async def _export_bosses(session: AsyncSession, vg_data: dict):
"""Export boss battles per version group."""
vg_result = await session.execute(select(VersionGroup))
slug_to_vg = {vg.slug: vg for vg in vg_result.scalars().all()}
badge_dir = FRONTEND_PUBLIC / "badges"
downloaded_badges: set[str] = set()
exported = 0
for vg_slug, vg_info in vg_data.items():
vg = slug_to_vg.get(vg_slug)
@@ -429,19 +478,37 @@ async def _export_bosses(session: AsyncSession, vg_data: dict):
continue
first_game_slug = list(vg_info["games"].keys())[0]
data = [
{
sprite_dir = FRONTEND_PUBLIC / "boss-sprites" / first_game_slug
downloaded_sprites: set[str] = set()
data = []
for b in bosses:
badge_image_url = b.badge_image_url
sprite_url = b.sprite_url
if badge_image_url and b.badge_name:
badge_slug = _slugify(b.badge_name)
badge_image_url = _download_image(
badge_image_url, badge_dir, badge_slug, downloaded_badges,
)
if sprite_url:
sprite_slug = _slugify(b.name)
sprite_url = _download_image(
sprite_url, sprite_dir, sprite_slug, downloaded_sprites,
)
data.append({
"name": b.name,
"boss_type": b.boss_type,
"specialty_type": b.specialty_type,
"badge_name": b.badge_name,
"badge_image_url": b.badge_image_url,
"badge_image_url": badge_image_url,
"level_cap": b.level_cap,
"order": b.order,
"after_route_name": b.after_route.name if b.after_route else None,
"location": b.location,
"section": b.section,
"sprite_url": b.sprite_url,
"sprite_url": sprite_url,
"pokemon": [
{
"pokeapi_id": bp.pokemon.pokeapi_id,
@@ -451,9 +518,7 @@ async def _export_bosses(session: AsyncSession, vg_data: dict):
}
for bp in sorted(b.pokemon, key=lambda p: p.order)
],
}
for b in bosses
]
})
_write_json(f"{first_game_slug}-bosses.json", data)
exported += 1