Skip to content

Commit 237af7a

Browse files
committed
imp: Use json_data to save ChallengeHistory
1 parent 7aa37be commit 237af7a

File tree

4 files changed

+120
-35
lines changed

4 files changed

+120
-35
lines changed

hoyo_buddy/db/models.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import datetime
77
import pickle
88
from functools import cached_property
9-
from typing import TYPE_CHECKING, Any, Literal
9+
from typing import TYPE_CHECKING, Any, Literal, cast
1010

1111
import genshin
1212
import orjson
@@ -30,6 +30,8 @@
3030
from ..utils import blur_uid, get_now
3131

3232
if TYPE_CHECKING:
33+
from collections.abc import Mapping
34+
3335
import aiohttp
3436

3537
from ..hoyo.clients.gpy import GenshinClient
@@ -335,10 +337,11 @@ class ChallengeHistory(BaseModel):
335337
season_id = fields.IntField()
336338
name: fields.Field[str | None] = fields.CharField(max_length=64, null=True)
337339
challenge_type = fields.CharEnumField(ChallengeType, max_length=32)
338-
data = fields.BinaryField()
340+
data: fields.Field[bytes | None] = fields.BinaryField(null=True)
339341
start_time = fields.DatetimeField()
340342
end_time = fields.DatetimeField()
341343
lang: fields.Field[str | None] = fields.CharField(max_length=5, null=True)
344+
json_data: fields.Field[dict[str, Any] | None] = fields.JSONField(null=True)
342345

343346
class Meta:
344347
unique_together = ("uid", "season_id", "challenge_type")
@@ -353,17 +356,50 @@ def duration_str(self) -> str:
353356
@property
354357
def parsed_data(self) -> ChallengeWithLang:
355358
"""Parsed challenge data from binary pickled data."""
356-
challenge = pickle.loads(self.data)
359+
if self.json_data is None:
360+
if self.data is None:
361+
# This shouldn't happen, data could be None because of migration to use json_data
362+
msg = "Both json_data and data are None in ChallengeHistory"
363+
raise ValueError(msg)
364+
challenge = pickle.loads(self.data)
365+
else:
366+
challenge = self.load_data(self.json_data, challenge_type=self.challenge_type)
367+
357368
lang = getattr(challenge, "lang", None)
358369
if lang is None:
359370
# NOTE: Backward compatibility, old data has lang attr, new data doesn't
360371
challenge.__dict__["lang"] = self.lang
361-
return challenge
372+
return cast("ChallengeWithLang", challenge)
373+
374+
@classmethod
375+
def load_data(cls, raw: Mapping[str, Any], *, challenge_type: ChallengeType) -> Challenge:
376+
if challenge_type is ChallengeType.SPIRAL_ABYSS:
377+
return genshin.models.SpiralAbyss(**raw)
378+
if challenge_type is ChallengeType.IMG_THEATER:
379+
return genshin.models.ImgTheaterData(**raw)
380+
if challenge_type is ChallengeType.SHIYU_DEFENSE:
381+
return genshin.models.ShiyuDefense(**raw)
382+
if challenge_type is ChallengeType.ASSAULT:
383+
return genshin.models.DeadlyAssault(**raw)
384+
if challenge_type is ChallengeType.APC_SHADOW:
385+
return genshin.models.StarRailAPCShadow(**raw)
386+
if challenge_type is ChallengeType.MOC:
387+
return genshin.models.StarRailChallenge(**raw)
388+
if challenge_type is ChallengeType.PURE_FICTION:
389+
return genshin.models.StarRailPureFiction(**raw)
362390

363391
@classmethod
364392
async def add_data(
365-
cls, *, uid: int, challenge_type: ChallengeType, season_id: int, data: Challenge, lang: str
393+
cls,
394+
*,
395+
uid: int,
396+
challenge_type: ChallengeType,
397+
season_id: int,
398+
raw: Mapping[str, Any],
399+
lang: str,
366400
) -> None:
401+
data = cls.load_data(raw, challenge_type=challenge_type)
402+
367403
if isinstance(data, genshin.models.SpiralAbyss | genshin.models.DeadlyAssault):
368404
start_time = data.start_time
369405
end_time = data.end_time
@@ -390,15 +426,15 @@ async def add_data(
390426
uid=uid,
391427
season_id=season_id,
392428
challenge_type=challenge_type,
393-
data=pickle.dumps(data),
394429
start_time=start_time,
395430
end_time=end_time,
396431
name=name,
397432
lang=lang,
433+
json_data=raw,
398434
)
399435
except exceptions.IntegrityError:
400436
await cls.filter(uid=uid, season_id=season_id, challenge_type=challenge_type).update(
401-
data=pickle.dumps(data), name=name, lang=lang
437+
name=name, lang=lang, json_data=raw
402438
)
403439

404440

hoyo_buddy/draw/funcs/hoyo/zzz/shiyu.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,14 @@ def draw_frontiers(self, im: Image.Image, drawer: Drawer) -> None:
224224
style="bold",
225225
anchor="mm",
226226
)
227+
228+
# Backward compatibility, ShiyuDefenseCharacter.mindscape is added in
229+
# https://github.com/thesadru/genshin.py/commit/4e17d37f84048d2b0a478b45e374f980a7bbe3a3
230+
rank: int = getattr(agent, "mindscape", None) or self.agent_ranks.get(
231+
agent.id, 0
232+
)
227233
drawer.write(
228-
str(self.agent_ranks.get(agent.id, 0)),
234+
str(rank),
229235
size=36,
230236
position=(start_pos[0] + 158, start_pos[1] + 22),
231237
color=self.white,

hoyo_buddy/ui/hoyo/challenge.py

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections import defaultdict
4-
from typing import TYPE_CHECKING, Any
4+
from typing import TYPE_CHECKING, Any, cast
55

66
import discord
77
from ambr.utils import remove_html_tags
@@ -19,6 +19,7 @@
1919
TheaterBuff,
2020
)
2121
from genshin.models import Character as GICharacter
22+
from loguru import logger
2223

2324
from hoyo_buddy.bot.error_handler import get_error_embed
2425
from hoyo_buddy.constants import GAME_CHALLENGE_TYPES, GPY_LANG_TO_LOCALE, TRAVELER_IDS
@@ -218,47 +219,74 @@ async def _fetch_data(self) -> None:
218219
client = self.account.client
219220
client.set_lang(self.locale)
220221

221-
if (
222-
self.challenge_type in {ChallengeType.SPIRAL_ABYSS, ChallengeType.IMG_THEATER}
223-
and not self.characters
224-
):
225-
self.characters = await client.get_genshin_characters(self.account.uid)
226-
227222
await client.get_record_cards()
228223

229224
for previous in (False, True):
230225
if self.challenge_type is ChallengeType.SPIRAL_ABYSS:
231-
challenge = await client.get_genshin_spiral_abyss(
232-
self.account.uid, previous=previous
226+
raw = await client.get_genshin_spiral_abyss(
227+
self.account.uid, previous=previous, raw=True
233228
)
234229
elif self.challenge_type is ChallengeType.MOC:
235-
challenge = await client.get_starrail_challenge(self.account.uid, previous=previous)
230+
raw = await client.get_starrail_challenge(
231+
self.account.uid, previous=previous, raw=True
232+
)
236233
elif self.challenge_type is ChallengeType.PURE_FICTION:
237-
challenge = await client.get_starrail_pure_fiction(
238-
self.account.uid, previous=previous
234+
raw = await client.get_starrail_pure_fiction(
235+
self.account.uid, previous=previous, raw=True
239236
)
240237
elif self.challenge_type is ChallengeType.APC_SHADOW:
241-
challenge = await client.get_starrail_apc_shadow(
242-
self.account.uid, previous=previous
238+
raw = await client.get_starrail_apc_shadow(
239+
self.account.uid, previous=previous, raw=True
243240
)
244241
elif self.challenge_type is ChallengeType.IMG_THEATER:
245-
challenges = (
246-
await client.get_imaginarium_theater(self.account.uid, previous=previous)
247-
).datas
248-
if not challenges:
242+
raw_ = await client.get_imaginarium_theater(
243+
self.account.uid, previous=previous, raw=True
244+
)
245+
datas: list[dict[str, Any]] = raw_.get("data", [])
246+
if not datas:
249247
raise NoChallengeDataError(ChallengeType.IMG_THEATER)
250248

251-
challenge = max(challenges, key=lambda c: c.stats.difficulty.value)
249+
try:
250+
raw = max(datas, key=lambda d: d["stat"]["difficulty_id"])
251+
except KeyError:
252+
logger.error("Failed to get max difficulty ID from data", datas=datas)
253+
raw = datas[-1]
252254
elif self.challenge_type is ChallengeType.SHIYU_DEFENSE:
253-
if not self.agent_ranks:
255+
raw = await client.get_shiyu_defense(self.account.uid, previous=previous, raw=True)
256+
challenge = ChallengeHistory.load_data(raw, challenge_type=self.challenge_type)
257+
challenge = cast("ShiyuDefense", challenge)
258+
259+
# Backward compatibility, ShiyuDefenseCharacter.mindscape is added in
260+
# https://github.com/thesadru/genshin.py/commit/4e17d37f84048d2b0a478b45e374f980a7bbe3a3
261+
is_new_ver = (
262+
challenge.floors
263+
and challenge.floors[0].node_1.characters
264+
and hasattr(challenge.floors[0].node_1.characters[0], "mindscape")
265+
)
266+
267+
# No need to fetch agent ranks if the data is using new version
268+
if challenge.has_data and not self.agent_ranks and not is_new_ver:
254269
agents = await client.get_zzz_agents(self.account.uid)
255270
self.agent_ranks = {agent.id: agent.rank for agent in agents}
256-
challenge = await client.get_shiyu_defense(self.account.uid, previous=previous)
257271
elif self.challenge_type is ChallengeType.ASSAULT:
258-
challenge = await client.get_deadly_assault(self.account.uid, previous=previous)
272+
raw = await client.get_deadly_assault(self.account.uid, previous=previous, raw=True)
259273
else:
260-
msg = f"Invalid challenge type: {self.challenge_type}"
261-
raise ValueError(msg)
274+
msg = f"Fetching data for {self.challenge_type!r}"
275+
raise NotImplementedError(msg)
276+
277+
challenge = ChallengeHistory.load_data(raw, challenge_type=self.challenge_type)
278+
279+
if (
280+
self.challenge_type in {ChallengeType.SPIRAL_ABYSS, ChallengeType.IMG_THEATER}
281+
and not self.characters
282+
):
283+
# Only fetch characters when challenge has data
284+
try:
285+
self.check_challenge_data(challenge)
286+
except NoChallengeDataError:
287+
pass
288+
else:
289+
self.characters = await client.get_genshin_characters(self.account.uid)
262290

263291
try:
264292
season_id = self._get_season_id(challenge, previous)
@@ -276,18 +304,20 @@ async def _fetch_data(self) -> None:
276304
uid=self.account.uid,
277305
challenge_type=self.challenge_type,
278306
season_id=season_id,
279-
data=challenge,
307+
raw=raw,
280308
lang=client.lang,
281309
)
282310

283311
def check_challenge_data(self, challenge: Challenge | None) -> None:
312+
"""Check if the challenge has data and raise an error if it doesn't"""
313+
exc = NoChallengeDataError(self.challenge_type)
284314
if challenge is None:
285-
raise NoChallengeDataError(self.challenge_type)
315+
raise exc
286316
if isinstance(challenge, SpiralAbyss):
287317
if not challenge.floors:
288318
raise NoChallengeDataError(ChallengeType.SPIRAL_ABYSS)
289319
elif not challenge.has_data:
290-
raise NoChallengeDataError(self.challenge_type)
320+
raise exc
291321

292322
def get_season(self, challenge: Challenge) -> StarRailChallengeSeason:
293323
if isinstance(challenge, SpiralAbyss | ImgTheaterData | ShiyuDefense | DeadlyAssault):
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from tortoise import BaseDBAsyncClient
2+
3+
4+
async def upgrade(db: BaseDBAsyncClient) -> str:
5+
return """
6+
ALTER TABLE "challengehistory" ADD "json_data" JSONB;
7+
ALTER TABLE "challengehistory" ALTER COLUMN "data" DROP NOT NULL;"""
8+
9+
10+
async def downgrade(db: BaseDBAsyncClient) -> str:
11+
return """
12+
ALTER TABLE "challengehistory" DROP COLUMN "json_data";
13+
ALTER TABLE "challengehistory" ALTER COLUMN "data" SET NOT NULL;"""

0 commit comments

Comments
 (0)