From c510a1b63ffe52f7532219aa8995e6af13e55782 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Tue, 15 Sep 2026 16:35:04 +0300 Subject: [PATCH] Use bluearchive.wiki portraits for gacha cards --- blue_archive/constants.py | 11 +++++++++++ blue_archive/data.py | 21 ++++++++++++++++++++- blue_archive/gacha_renderer.py | 34 ++++++++++++++++------------------ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/blue_archive/constants.py b/blue_archive/constants.py index d7117f2..7feffcf 100644 --- a/blue_archive/constants.py +++ b/blue_archive/constants.py @@ -17,6 +17,17 @@ GACHA_ANIM_DURATION = 3.5 # 195 frames × 100 ms (used to be 19.5 will be chang BANNER_API = "https://api.ennead.cc/buruaka/banner" CHARACTER_API = "https://api.ennead.cc/buruaka/character" PORTRAIT_CDN = "https://schaledb.com/images" +WIKI_PORTRAIT_BASE = "https://bluearchive.wiki/wiki/Special:FilePath/" +WIKI_VARIANT_MAP: dict[str, str] = { + "Bunny": "Bunny Girl", + "School": "School Uniform", + "Armed": "Battle", + "Track": "Sportswear", + "Cheer Squad": "Cheerleader", + "Pajamas": "Pajama", + "Cycling": "Riding", + "Small": "Kid", +} # ── Gacha rates (matching real Blue Archive) ──────────────────────────────── # Regular/Pickup: 3★ = 3.0%, 2★ = 18.5%, 1★ = 78.5% diff --git a/blue_archive/data.py b/blue_archive/data.py index 6d34fc9..d81c779 100644 --- a/blue_archive/data.py +++ b/blue_archive/data.py @@ -7,6 +7,7 @@ import random from datetime import datetime, timezone from pathlib import Path from typing import Optional +from urllib.parse import quote import aiohttp @@ -15,6 +16,8 @@ from .constants import ( BANNER_API, CHARACTER_API, PORTRAIT_CDN, + WIKI_PORTRAIT_BASE, + WIKI_VARIANT_MAP, BANNER_RATES, DEFAULT_RATES, PULL10_RATES, @@ -304,7 +307,23 @@ class StudentDB: return random.choice(pullable or self.students) def cdn_portrait(self, student: dict) -> str: - """SchaleDB CDN full-body portrait URL for a student (keyed by ID).""" + """bluearchive.wiki portrait URL (bust art, keyed by student name).""" + name = student["Name"] + if name.endswith(" Terror"): + name = name[:-7] + " (Terror)" + if name == "Aris": + name = "Arisu" + elif name.startswith("Aris ("): + name = "Arisu " + name[5:] + if "(" in name: + base, variant = name.split("(", 1) + variant = variant.rstrip(")").strip() + variant = WIKI_VARIANT_MAP.get(variant, variant) + name = f"{base.strip()} ({variant})" + return WIKI_PORTRAIT_BASE + quote(f"Portrait {name}.png", safe="") + + def cdn_sprite(self, student: dict) -> str: + """SchaleDB CDN full-body sprite URL (fallback, keyed by ID).""" return f"{PORTRAIT_CDN}/student/portrait/{student['Id']}.webp" diff --git a/blue_archive/gacha_renderer.py b/blue_archive/gacha_renderer.py index 11e9a60..3b26c89 100644 --- a/blue_archive/gacha_renderer.py +++ b/blue_archive/gacha_renderer.py @@ -48,25 +48,23 @@ FOOTER_FONT = _get_font(18, bold=False) # ── Image fetching ───────────────────────────────────────────────────────── async def _fetch_portrait(session: aiohttp.ClientSession, student: dict) -> Optional[Image.Image]: - """Download a student's full-body portrait and crop it to the card area. - - Portraits come from the SchaleDB image CDN, keyed by student ID. - """ - url = db.cdn_portrait(student) - try: - async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: - if resp.status == 200: - data = await resp.read() - img = Image.open(io.BytesIO(data)).convert("RGBA") - # Full-body art; crop the upper portion to fit the card. - p_w, p_h = img.size - crop_h = int(p_w * 1.20) - if crop_h < p_h: - img = img.crop((0, 0, p_w, crop_h)) - return img - except Exception: - log.exception("Failed to fetch portrait for %s", student["Name"]) + """Download a student's portrait (wiki bust art first, full sprite fallback).""" + for url in (db.cdn_portrait(student), db.cdn_sprite(student)): + try: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + if resp.status == 200: + data = await resp.read() + img = Image.open(io.BytesIO(data)).convert("RGBA") + # Full-body art is tall; crop the upper portion to fit the card. + p_w, p_h = img.size + crop_h = int(p_w * 1.20) + if crop_h < p_h: + img = img.crop((0, 0, p_w, crop_h)) + return img + except Exception: + log.debug("portrait fetch failed via %s", url) + log.warning("no portrait for %s", student["Name"]) return None