rollback checkpoint: hardened gacha, disabled ffmpeg, deleted rio, async db
This commit is contained in:
1 parent
05c0599d1c
commit
30efb9105f
9 files changed
+367
-260
No files matched your search
+150
-6
@@ -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
|
||||
|
||||
Reference in new issue
Block a user