From 08b558528b3ac0e1b36674444a0588babd1295be Mon Sep 17 00:00:00 2001 From: omg-xtao <100690902+omg-xtao@users.noreply.github.com> Date: Fri, 29 Mar 2024 23:14:24 +0800 Subject: [PATCH] :sparkles: Support starrail phone_theme assets --- core/dependence/assets.py | 60 +++++++++++++++++ core/services/players/services.py | 9 +++ modules/wiki/models/phone_theme.py | 23 +++++++ plugins/starrail/avatar_list.py | 4 ++ plugins/starrail/stats.py | 20 +++++- plugins/starrail/wish_log.py | 14 +++- plugins/tools/phone_theme.py | 64 +++++++++++++++++++ resources/starrail/avatar_list/main.html | 2 +- resources/starrail/avatar_list/style.css | 4 +- .../starrail/gacha_count/gacha_count.css | 5 +- .../starrail/gacha_count/gacha_count.html | 2 +- resources/starrail/gacha_log/gacha_log.css | 7 +- resources/starrail/gacha_log/gacha_log.html | 2 +- 13 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 modules/wiki/models/phone_theme.py create mode 100644 plugins/tools/phone_theme.py diff --git a/core/dependence/assets.py b/core/dependence/assets.py index 85a4d69..9d522b5 100644 --- a/core/dependence/assets.py +++ b/core/dependence/assets.py @@ -11,6 +11,7 @@ from modules.wiki.base import WikiModel from modules.wiki.models.avatar_config import AvatarIcon from modules.wiki.models.head_icon import HeadIcon from modules.wiki.models.light_cone_config import LightConeIcon +from modules.wiki.models.phone_theme import PhoneTheme from utils.const import PROJECT_ROOT from utils.log import logger from utils.typedefs import StrOrURL, StrOrInt @@ -23,6 +24,7 @@ DATA_MAP = { "avatar_eidolon": WikiModel.BASE_URL + "avatar_eidolon_icons.json", "avatar_skill": WikiModel.BASE_URL + "skill/info.json", "head_icon": WikiModel.BASE_URL + "head_icons.json", + "phone_theme": WikiModel.BASE_URL + "phone_themes.json", } @@ -345,6 +347,59 @@ class _HeadIconAssets(_AssetsService): raise AssetsCouldNotFound("头像素材图标不存在", target) +class _PhoneThemeAssets(_AssetsService): + path: Path + data: List[PhoneTheme] + id_map: Dict[int, PhoneTheme] + + def __init__(self, client: Optional[AsyncClient] = None) -> None: + super().__init__(client) + self.path = ASSETS_PATH.joinpath("phone_theme") + self.path.mkdir(exist_ok=True, parents=True) + + async def initialize(self): + logger.info("正在初始化手机壁纸素材图标") + html = await self.client.get(DATA_MAP["phone_theme"]) + self.data = [PhoneTheme(**data) for data in html.json()] + self.id_map = {theme.id: theme for theme in self.data} + tasks = [] + for theme in self.data: + path = self.path / f"{theme.id}.png" + if not path.exists(): + if theme.urls[0]: + tasks.append(self._download(theme.urls[0], path)) + elif theme.urls[1]: + tasks.append(self._download(theme.urls[1], path)) + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("手机壁纸素材图标初始化完成") + + def get_path(self, theme: PhoneTheme, ext: str) -> Path: + path = self.path / f"{theme.id}.{ext}" + return path + + def get_by_id(self, id_: int) -> Optional[PhoneTheme]: + return self.id_map.get(id_, None) + + def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[PhoneTheme]: + data = self.get_by_id(target) + if data: + return data + if second_target: + return self.get_target(second_target) + raise AssetsCouldNotFound("手机壁纸素材图标不存在", target) + + def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + theme = self.get_target(target, second_target) + png_path = self.get_path(theme, "png") + if png_path.exists(): + return png_path + raise AssetsCouldNotFound("手机壁纸素材图标不存在", target) + + class AssetsService(BaseService.Dependence): """asset服务 @@ -361,6 +416,9 @@ class AssetsService(BaseService.Dependence): head_icon: _HeadIconAssets """头像""" + phone_theme: _PhoneThemeAssets + """手机壁纸""" + light_cone: _LightConeAssets """光锥""" @@ -368,9 +426,11 @@ class AssetsService(BaseService.Dependence): self.client = AsyncClient(timeout=60.0) self.avatar = _AvatarAssets(self.client) self.head_icon = _HeadIconAssets(self.client) + self.phone_theme = _PhoneThemeAssets(self.client) self.light_cone = _LightConeAssets(self.client) async def initialize(self): # pylint: disable=W0221 await self.avatar.initialize() await self.head_icon.initialize() + await self.phone_theme.initialize() await self.light_cone.initialize() diff --git a/core/services/players/services.py b/core/services/players/services.py index 08ede2e..c3684cf 100644 --- a/core/services/players/services.py +++ b/core/services/players/services.py @@ -119,6 +119,15 @@ class PlayerInfoService(BaseService): return True return False + async def set_name_card(self, player_id: int, phone_theme_id: int): + player_info = await self._players_info_repository.get_by_player_id(player_id) + if player_info is None: + return False + player_info.name_card = phone_theme_id + player_info.last_save_time = datetime.now() + await self._players_info_repository.update(player_info) + return True + async def get_form_sql(self, player: Player): return await self._players_info_repository.get(player.user_id, player.player_id) diff --git a/modules/wiki/models/phone_theme.py b/modules/wiki/models/phone_theme.py new file mode 100644 index 0000000..97b1c26 --- /dev/null +++ b/modules/wiki/models/phone_theme.py @@ -0,0 +1,23 @@ +from typing import Optional, List + +from pydantic import BaseModel + + +class PhoneTheme(BaseModel): + id: int + """ID""" + name: str + """名称""" + description: str + """描述""" + story: Optional[str] = None + """故事""" + urls: List[str] + + +# 原始数据 + + +class PhoneThemeConfig(BaseModel): + ID: int + PhoneThemeMain: str diff --git a/plugins/starrail/avatar_list.py b/plugins/starrail/avatar_list.py index 4f71861..f471fb9 100644 --- a/plugins/starrail/avatar_list.py +++ b/plugins/starrail/avatar_list.py @@ -13,6 +13,7 @@ from core.services.template.services import TemplateService from core.services.wiki.services import WikiService from plugins.tools.genshin import GenshinHelper, CharacterDetails from plugins.tools.head_icon import HeadIconService +from plugins.tools.phone_theme import PhoneThemeService from utils.log import logger from utils.uid import mask_number @@ -64,6 +65,7 @@ class AvatarListPlugin(Plugin): helper: GenshinHelper = None, character_details: CharacterDetails = None, head_icon: HeadIconService = None, + phone_theme: PhoneThemeService = None, ) -> None: self.cookies_service = cookies_service self.assets_service = assets_service @@ -72,6 +74,7 @@ class AvatarListPlugin(Plugin): self.helper = helper self.character_details = character_details self.head_icon = head_icon + self.phone_theme = phone_theme async def get_avatar_data( self, character_id: int, client: "StarRailClient" @@ -170,6 +173,7 @@ class AvatarListPlugin(Plugin): "avatar_datas": avatar_datas, # 角色数据 "has_more": has_more, # 是否显示了全部角色 "avatar": (await self.head_icon.get_head_icon(client.player_id)).as_uri(), + "background": (await self.phone_theme.get_phone_theme(client.player_id)).as_uri(), } as_document = all_avatars and len(characters) > MAX_AVATAR_COUNT diff --git a/plugins/starrail/stats.py b/plugins/starrail/stats.py index 7a9d94f..12b9ad1 100644 --- a/plugins/starrail/stats.py +++ b/plugins/starrail/stats.py @@ -7,10 +7,12 @@ from telegram.ext import CallbackContext, filters from core.plugin import Plugin, handler from core.services.cookies.error import TooManyRequestPublicCookies +from core.services.players.services import PlayerInfoService from core.services.template.models import RenderResult from core.services.template.services import TemplateService from plugins.tools.genshin import GenshinHelper from plugins.tools.head_icon import HeadIconService +from plugins.tools.phone_theme import PhoneThemeService from utils.log import logger from utils.uid import mask_number @@ -29,10 +31,14 @@ class PlayerStatsPlugins(Plugin): template: TemplateService, helper: GenshinHelper, head_icon: HeadIconService, + phone_theme: PhoneThemeService, + player_info_service: PlayerInfoService, ): self.template_service = template self.helper = helper self.head_icon = head_icon + self.phone_theme = phone_theme + self.player_info_service = player_info_service async def get_uid(self, user_id: int, args: List[str], reply: Optional[Message]) -> int: """通过消息获取 uid,优先级:args > reply > self""" @@ -88,7 +94,7 @@ class PlayerStatsPlugins(Plugin): except SimnetBadRequest: rogue = None logger.debug(user_info) - + await self.set_name_card(uid, user_info.phone_background_image_url) data = { "uid": mask_number(uid), "info": user_info.info, @@ -109,7 +115,7 @@ class PlayerStatsPlugins(Plugin): ], "style": "xianzhou", # nosec "avatar": (await self.head_icon.get_head_icon(uid)).as_uri(), - "background": user_info.phone_background_image_url, + "background": (await self.phone_theme.get_phone_theme(uid)).as_uri(), } return await self.template_service.render( @@ -118,3 +124,13 @@ class PlayerStatsPlugins(Plugin): {"width": 650, "height": 440}, full_page=True, ) + + async def set_name_card(self, player_id: int, image_url: str): + if not image_url: + return + try: + phone_theme_id = int(image_url.split("/")[-1].replace(".png", "")) + except (IndexError, ValueError): + return + await self.phone_theme.set_to_cache(player_id, phone_theme_id) + await self.player_info_service.set_name_card(player_id, phone_theme_id) diff --git a/plugins/starrail/wish_log.py b/plugins/starrail/wish_log.py index 6851f4d..7e4dd52 100644 --- a/plugins/starrail/wish_log.py +++ b/plugins/starrail/wish_log.py @@ -1,5 +1,5 @@ from io import BytesIO -from typing import Optional, TYPE_CHECKING, List, Union, Tuple +from typing import Optional, TYPE_CHECKING, List, Union, Tuple, Dict from simnet.models.starrail.wish import StarRailBannerType from telegram import Document, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User @@ -29,6 +29,7 @@ from modules.gacha_log.migrate import GachaLogMigrate from modules.gacha_log.models import GachaLogInfo from plugins.tools.genshin import PlayerNotFoundError from plugins.tools.head_icon import HeadIconService +from plugins.tools.phone_theme import PhoneThemeService from utils.log import logger try: @@ -57,12 +58,14 @@ class WishLogPlugin(Plugin.Conversation): assets: AssetsService, cookie_service: CookiesService, head_icon: HeadIconService, + phone_theme: PhoneThemeService, ): self.template_service = template_service self.players_service = players_service self.assets_service = assets self.cookie_service = cookie_service self.head_icon = head_icon + self.phone_theme = phone_theme self.gacha_log = GachaLog() self.wish_photo = None @@ -292,7 +295,7 @@ class WishLogPlugin(Plugin.Conversation): data = await self.gacha_log.get_analysis(user_id, player_id, pool_type, self.assets_service) if isinstance(data, str): return data - data["avatar"] = (await self.head_icon.get_head_icon(player_id)).as_uri() + await self.add_theme_data(data, player_id) png_data = await self.template_service.render( "starrail/gacha_log/gacha_log.html", data, @@ -473,7 +476,7 @@ class WishLogPlugin(Plugin.Conversation): await callback_query.answer(png_data, show_alert=True) self.add_delete_message_job(message, delay=1) else: - png_data["avatar"] = (await self.head_icon.get_head_icon(uid)).as_uri() + await self.add_theme_data(png_data, uid) await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) document = False if png_data["hasMore"] and not group: @@ -494,6 +497,11 @@ class WishLogPlugin(Plugin.Conversation): else: await png.edit_media(message) + async def add_theme_data(self, data: Dict, player_id: int): + data["avatar"] = (await self.head_icon.get_head_icon(player_id)).as_uri() + data["background"] = (await self.phone_theme.get_phone_theme(player_id)).as_uri() + return data + @staticmethod async def get_migrate_data( old_user_id: int, new_user_id: int, old_players: List["Player"] diff --git a/plugins/tools/phone_theme.py b/plugins/tools/phone_theme.py new file mode 100644 index 0000000..0e342c0 --- /dev/null +++ b/plugins/tools/phone_theme.py @@ -0,0 +1,64 @@ +from pathlib import Path +from typing import Optional + +from core.dependence.assets import AssetsService, AssetsCouldNotFound +from core.services.players.services import PlayerInfoService +from gram_core.dependence.redisdb import RedisDB +from gram_core.plugin import Plugin +from utils.log import logger + + +class PhoneThemeService(Plugin): + def __init__( + self, + player_info_service: PlayerInfoService, + asset_service: AssetsService, + redis: RedisDB, + ) -> None: + self.player_info_service = player_info_service + self.assets = asset_service + self.redis = redis.client + self.expire = 60 * 60 + self.qname = "plugins:phone_theme" + + def get_default_phone_theme(self) -> Optional[Path]: + try: + return self.assets.phone_theme.icon(221000) + except AssetsCouldNotFound as e: + logger.warning(str(e)) + return None + + async def get_from_cache(self, player_id: int) -> Optional[int]: + key = f"{self.qname}:{player_id}" + data = await self.redis.get(key) + if data is None: + return None + return int(data) + + async def set_to_cache(self, player_id: int, phone_theme: int) -> None: + key = f"{self.qname}:{player_id}" + await self.redis.set(key, phone_theme, ex=self.expire) + + async def get_from_sql(self, player_id: int) -> Optional[int]: + player_info = await self.player_info_service.get_by_player_id(player_id) + if player_info is None: + return None + return player_info.name_card + + async def get_phone_theme_id(self, player_id: int) -> Optional[int]: + phone_theme = await self.get_from_cache(player_id) + if phone_theme is not None: + return phone_theme + phone_theme = await self.get_from_sql(player_id) + if phone_theme is not None: + await self.set_to_cache(player_id, phone_theme) + return phone_theme + return None + + async def get_phone_theme(self, player_id: int): + try: + phone_theme = await self.get_phone_theme_id(player_id) + return self.assets.phone_theme.icon(phone_theme) + except AssetsCouldNotFound as e: + logger.warning(str(e)) + return self.get_default_phone_theme() diff --git a/resources/starrail/avatar_list/main.html b/resources/starrail/avatar_list/main.html index c0c3e0b..ab4cba0 100644 --- a/resources/starrail/avatar_list/main.html +++ b/resources/starrail/avatar_list/main.html @@ -9,7 +9,7 @@