From e4b5f0d849e7ed2be0cf6244a0d855db6c403e6c Mon Sep 17 00:00:00 2001 From: esefxdz Date: Tue, 15 Sep 2026 16:07:44 +0300 Subject: [PATCH] Fix gacha portraits: switch to SchaleDB CDN (joexyz is dead) --- blue_archive/constants.py | 41 +--------------------------------- blue_archive/data.py | 17 ++++---------- blue_archive/gacha_renderer.py | 24 +++++--------------- 3 files changed, 11 insertions(+), 71 deletions(-) diff --git a/blue_archive/constants.py b/blue_archive/constants.py index 7c8a1f8..d7117f2 100644 --- a/blue_archive/constants.py +++ b/blue_archive/constants.py @@ -16,7 +16,7 @@ GACHA_ANIM_DURATION = 3.5 # 195 frames × 100 ms (used to be 19.5 will be chang # ── APIs ──────────────────────────────────────────────────────────────────── BANNER_API = "https://api.ennead.cc/buruaka/banner" CHARACTER_API = "https://api.ennead.cc/buruaka/character" -JOEXYZ_CDN = "https://ba.joexyz.online/cdn/v2/images" +PORTRAIT_CDN = "https://schaledb.com/images" # ── Gacha rates (matching real Blue Archive) ──────────────────────────────── # Regular/Pickup: 3★ = 3.0%, 2★ = 18.5%, 1★ = 78.5% @@ -72,45 +72,6 @@ PORTRAIT_H = CARD_H - PORTRAIT_TOP - NAME_STRIP_H - 4 PAGE_SIZE = 10 FILTER_LABELS: dict[str, int | None] = {"All": None, "3★": 3, "2★": 2, "1★": 1} -# ── CDN icon slug mapping (SchaleDB variant suffix → joexyz CDN slug suffix) ─ -# Base names without a parenthesised variant use the lowercased name directly. -# "Aris" is a special case — the CDN uses "aris", not "arisu". -CDN_SUFFIX_MAP: dict[str, str] = { - "New Year": "newyear", - "Hot Spring": "onsen", - "Bunny": "bunnygirl", - "Cheer Squad": "cheerleader", - "Camp": "camp", - "Cycling": "cycling", - "Dress": "dress", - "Small": "small", - "Maid": "maid", - "Track": "track", - "Swimsuit": "swimsuit", - "Band": "band", - "Christmas": "christmas", - "Casual": "casual", - "Guide": "guide", -} - -def cdn_icon_slug(name: str) -> str: - """Convert a SchaleDB student Name to the joexyz CDN icon filename slug.""" - # Normalise "Name Terror" into the same parenthesised path - if name.endswith(" Terror"): - name = name[:-7] + " (Terror)" - # "Aris" is "aris" on the CDN, not "arisu" - if name == "Aris": - return "aris" - - if "(" in name: - base, rest = name.split("(", 1) - base = base.strip().lower().replace(" ", "_") - variant = rest.rstrip(")").strip() - suffix = CDN_SUFFIX_MAP.get(variant, variant.lower().replace(" ", "_")) - return f"{base}_{suffix}" - - return name.lower().replace(" ", "_") - # ── Font paths (tried in order; Pillow default used as last resort) ───────── FONT_PATHS = [ "/usr/share/fonts/TTF/DejaVuSans.ttf", diff --git a/blue_archive/data.py b/blue_archive/data.py index 2771c64..6d34fc9 100644 --- a/blue_archive/data.py +++ b/blue_archive/data.py @@ -14,8 +14,7 @@ from .constants import ( BUNDLED_DB, BANNER_API, CHARACTER_API, - JOEXYZ_CDN, - cdn_icon_slug, + PORTRAIT_CDN, BANNER_RATES, DEFAULT_RATES, PULL10_RATES, @@ -304,17 +303,9 @@ class StudentDB: pullable = [s for s in self.students if s.get("IsLimited", 0) < 2] return random.choice(pullable or self.students) - def cdn_icon(self, student: dict) -> str: - """joexyz CDN icon URL for a student (face/upper-body).""" - slug = cdn_icon_slug(student["Name"]) - return f"{JOEXYZ_CDN}/students/icons/{slug}.png" - - def cdn_skill_portrait(self, student: dict) -> str: - """joexyz CDN skill-portrait URL (full-body fallback).""" - dev = student.get("DevName", "") - if not dev: - return "" - return f"{JOEXYZ_CDN}/skill-portraits/Skill_Portrait_{dev}.png" + def cdn_portrait(self, student: dict) -> str: + """SchaleDB CDN full-body portrait URL for a student (keyed by ID).""" + return f"{PORTRAIT_CDN}/student/portrait/{student['Id']}.webp" # Global singleton diff --git a/blue_archive/gacha_renderer.py b/blue_archive/gacha_renderer.py index 057e2ea..11e9a60 100644 --- a/blue_archive/gacha_renderer.py +++ b/blue_archive/gacha_renderer.py @@ -48,36 +48,24 @@ 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 face icon from the joexyz CDN. + """Download a student's full-body portrait and crop it to the card area. - Uses the student icon CDN (face/upper-body crop). Falls back to the - skill-portrait (full-body art) if the icon is unavailable. + Portraits come from the SchaleDB image CDN, keyed by student ID. """ - # 1. Try the face icon - icon_url = db.cdn_icon(student) + url = db.cdn_portrait(student) try: - async with session.get(icon_url, timeout=aiohttp.ClientTimeout(total=5)) as resp: - if resp.status == 200: - data = await resp.read() - return Image.open(io.BytesIO(data)).convert("RGBA") - except Exception: - log.debug("CDN icon failed for %s, trying skill portrait fallback", student["Name"]) - - # 2. Fallback: skill portrait (full-body character art) - sp_url = db.cdn_skill_portrait(student) - try: - async with session.get(sp_url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + 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") - # Skill portraits are full-body; crop upper portion for card fit + # 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 any portrait for %s", student["Name"]) + log.exception("Failed to fetch portrait for %s", student["Name"]) return None