rollback checkpoint: hardened gacha, disabled ffmpeg, deleted rio, async db

This commit is contained in:
esef committed 2026-08-03 20:05:45 +03:00
1 parent 05c0599d1c
commit 30efb9105f
9 files changed
+367 -260

No files matched your search

+150 -6
View File
@@ -13,6 +13,7 @@ import aiohttp
from .constants import (
BUNDLED_DB,
BANNER_API,
CHARACTER_API,
JOEXYZ_CDN,
cdn_icon_slug,
BANNER_RATES,
@@ -79,11 +80,140 @@ class StudentDB:
excluded,
)
async def merge_ennead(self, banner_cache: dict) -> int:
"""Merge characters from the ennead.cc API that aren't in the SchaleDB.
Fetches the full character list, maps fields to SchaleDB format, and
derives IsLimited from banner history. Only characters with IDs not
already present are added — existing SchaleDB entries are never touched.
Must be called after load() and after banners have been fetched.
Returns the number of new characters merged.
"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(CHARACTER_API, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status != 200:
log.warning("Ennead character API returned %d", resp.status)
return 0
chars = await resp.json()
except Exception:
log.exception("Failed to fetch ennead character API")
return 0
# Build IsLimited from all known banners (0=standard, 1=limited)
is_limited = self._build_is_limited(banner_cache)
try:
added = self._merge_chars(chars, is_limited)
if added:
log.info(
"Merged %d new characters from ennead API (total: %d, std: %d, limited: %d)",
added,
len(self.students),
sum(len(v) for v in self._std_pool.values()),
len(self._limited_pool),
)
except Exception:
log.exception("Failed to merge ennead characters")
return added
def _merge_chars(self, chars: list[dict], is_limited: dict[str, int]) -> int:
"""Add characters from the ennead API payload that aren't already loaded."""
added = 0
for c in chars:
sid = c["id"]
if sid in self.by_id:
continue # already present from SchaleDB — keep original
name = c["name"]
rarity = c["baseStar"]
limit = is_limited.get(name.lower())
if limit is None:
# Never appeared as a banner rate-up.
# 1-2★: welfare (matches SchaleDB: 19/20 welfare are 1★)
# 3★: likely upcoming limited (all known limited chars are 3★)
limit = 2 if rarity <= 2 else 1
student = {
"Id": sid,
"Name": name,
"StarGrade": rarity,
"School": c.get("school", "Unknown"),
"IsLimited": limit,
"SquadType": "Main" if c.get("squadType") == "Striker" else "Support",
"DevName": "",
}
self.by_id[sid] = student
self.by_name[name.lower()] = student
self.students.append(student)
if limit < 2:
if rarity in self.by_rarity:
self.by_rarity[rarity].append(student)
if limit == 0 and rarity in self._std_pool:
self._std_pool[rarity].append(student)
elif limit == 1:
self._limited_pool.append(student)
added += 1
return added
@staticmethod
def _build_is_limited(banner_cache: dict) -> dict[str, int]:
"""Derive IsLimited from banner history.
Returns {name_lower: 0|1} where:
0 = standard (ever appeared in a PickupGacha)
1 = limited (only in LimitedGacha / FesGacha, or never a rate-up)
"""
result: dict[str, int] = {}
all_banners = (
banner_cache.get("current", [])
+ banner_cache.get("upcoming", [])
+ banner_cache.get("ended", [])
)
for b in all_banners:
gtype = b.get("gachaType", "")
is_pickup = gtype == "PickupGacha"
for name in b.get("rateups", []):
key = name.lower()
# PickupGacha takes priority — once standard, always standard
if key not in result or is_pickup:
result[key] = 0 if is_pickup else 1
return result
@property
def pool_totals(self) -> dict[int, int]:
"""Total collectible students by rarity (excludes welfare)."""
return {r: len(v) for r, v in self.by_rarity.items()}
def get(self, student_id: int) -> Optional[dict]:
return self.by_id.get(student_id)
def get_by_name(self, name: str) -> Optional[dict]:
return self.by_name.get(name.lower())
"""Resolve a student by name with fuzzy fallback.
Tries: exact match → strip variant suffix → substring match.
Handles banner-API names like "Maki (Camp)" when only "Maki" exists.
"""
key = name.lower().strip()
if key in self.by_name:
return self.by_name[key]
# "Maki (Camp)" → try "Maki"
if "(" in key:
base = key.split("(", 1)[0].strip()
if base in self.by_name:
return self.by_name[base]
# Last resort: substring match (e.g. "Yuuka Pajamas" → "Yuuka (Pajamas)")
for stored_name, student in self.by_name.items():
if key in stored_name or stored_name in key:
return student
return None
def build_pool(self, banner: Optional[dict]) -> dict[int, list[dict]]:
"""Build the pullable character pool for a specific banner.
@@ -134,13 +264,21 @@ class StudentDB:
For 1-2★: uniform random (rate-ups don't affect lower rarities).
"""
if not pool:
return random.choice(self.students)
# Fallback: any non-welfare student (shouldn't normally happen)
pullable = [s for s in self.students if s.get("IsLimited", 0) < 2]
return random.choice(pullable or self.students)
if rarity == 3 and rateup_names:
# Weighted: rate-up chars get 3x weight each vs normal chars
# Weighted: rate-up chars get 3x weight each vs normal chars.
# Use get_by_name so "Hoshino (Armed)" in API matches "Hoshino" in DB.
rateup_ids = set()
for rn in rateup_names:
rs = self.get_by_name(rn)
if rs:
rateup_ids.add(rs["Id"])
weights = []
for s in pool:
if s["Name"].lower() in rateup_names:
if s["Id"] in rateup_ids:
weights.append(3.0)
else:
weights.append(1.0)
@@ -151,7 +289,10 @@ class StudentDB:
def random_by_rarity(self, rarity: int) -> dict:
"""Pick any student of given rarity from the full pullable pool (fallback)."""
pool = self.by_rarity.get(rarity, [])
return random.choice(pool) if pool else random.choice(self.students)
if pool:
return random.choice(pool)
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)."""
@@ -160,7 +301,10 @@ class StudentDB:
def cdn_skill_portrait(self, student: dict) -> str:
"""joexyz CDN skill-portrait URL (full-body fallback)."""
return f"{JOEXYZ_CDN}/skill-portraits/Skill_Portrait_{student['DevName']}.png"
dev = student.get("DevName", "")
if not dev:
return ""
return f"{JOEXYZ_CDN}/skill-portraits/Skill_Portrait_{dev}.png"
# Global singleton