Support starrail phone_theme assets

This commit is contained in:
omg-xtao 2024-03-29 23:14:24 +08:00 committed by GitHub
parent 2ab86b71d9
commit 08b558528b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 204 additions and 12 deletions

View File

@ -11,6 +11,7 @@ from modules.wiki.base import WikiModel
from modules.wiki.models.avatar_config import AvatarIcon from modules.wiki.models.avatar_config import AvatarIcon
from modules.wiki.models.head_icon import HeadIcon from modules.wiki.models.head_icon import HeadIcon
from modules.wiki.models.light_cone_config import LightConeIcon 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.const import PROJECT_ROOT
from utils.log import logger from utils.log import logger
from utils.typedefs import StrOrURL, StrOrInt from utils.typedefs import StrOrURL, StrOrInt
@ -23,6 +24,7 @@ DATA_MAP = {
"avatar_eidolon": WikiModel.BASE_URL + "avatar_eidolon_icons.json", "avatar_eidolon": WikiModel.BASE_URL + "avatar_eidolon_icons.json",
"avatar_skill": WikiModel.BASE_URL + "skill/info.json", "avatar_skill": WikiModel.BASE_URL + "skill/info.json",
"head_icon": WikiModel.BASE_URL + "head_icons.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) 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): class AssetsService(BaseService.Dependence):
"""asset服务 """asset服务
@ -361,6 +416,9 @@ class AssetsService(BaseService.Dependence):
head_icon: _HeadIconAssets head_icon: _HeadIconAssets
"""头像""" """头像"""
phone_theme: _PhoneThemeAssets
"""手机壁纸"""
light_cone: _LightConeAssets light_cone: _LightConeAssets
"""光锥""" """光锥"""
@ -368,9 +426,11 @@ class AssetsService(BaseService.Dependence):
self.client = AsyncClient(timeout=60.0) self.client = AsyncClient(timeout=60.0)
self.avatar = _AvatarAssets(self.client) self.avatar = _AvatarAssets(self.client)
self.head_icon = _HeadIconAssets(self.client) self.head_icon = _HeadIconAssets(self.client)
self.phone_theme = _PhoneThemeAssets(self.client)
self.light_cone = _LightConeAssets(self.client) self.light_cone = _LightConeAssets(self.client)
async def initialize(self): # pylint: disable=W0221 async def initialize(self): # pylint: disable=W0221
await self.avatar.initialize() await self.avatar.initialize()
await self.head_icon.initialize() await self.head_icon.initialize()
await self.phone_theme.initialize()
await self.light_cone.initialize() await self.light_cone.initialize()

View File

@ -119,6 +119,15 @@ class PlayerInfoService(BaseService):
return True return True
return False 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): async def get_form_sql(self, player: Player):
return await self._players_info_repository.get(player.user_id, player.player_id) return await self._players_info_repository.get(player.user_id, player.player_id)

View File

@ -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

View File

@ -13,6 +13,7 @@ from core.services.template.services import TemplateService
from core.services.wiki.services import WikiService from core.services.wiki.services import WikiService
from plugins.tools.genshin import GenshinHelper, CharacterDetails from plugins.tools.genshin import GenshinHelper, CharacterDetails
from plugins.tools.head_icon import HeadIconService from plugins.tools.head_icon import HeadIconService
from plugins.tools.phone_theme import PhoneThemeService
from utils.log import logger from utils.log import logger
from utils.uid import mask_number from utils.uid import mask_number
@ -64,6 +65,7 @@ class AvatarListPlugin(Plugin):
helper: GenshinHelper = None, helper: GenshinHelper = None,
character_details: CharacterDetails = None, character_details: CharacterDetails = None,
head_icon: HeadIconService = None, head_icon: HeadIconService = None,
phone_theme: PhoneThemeService = None,
) -> None: ) -> None:
self.cookies_service = cookies_service self.cookies_service = cookies_service
self.assets_service = assets_service self.assets_service = assets_service
@ -72,6 +74,7 @@ class AvatarListPlugin(Plugin):
self.helper = helper self.helper = helper
self.character_details = character_details self.character_details = character_details
self.head_icon = head_icon self.head_icon = head_icon
self.phone_theme = phone_theme
async def get_avatar_data( async def get_avatar_data(
self, character_id: int, client: "StarRailClient" self, character_id: int, client: "StarRailClient"
@ -170,6 +173,7 @@ class AvatarListPlugin(Plugin):
"avatar_datas": avatar_datas, # 角色数据 "avatar_datas": avatar_datas, # 角色数据
"has_more": has_more, # 是否显示了全部角色 "has_more": has_more, # 是否显示了全部角色
"avatar": (await self.head_icon.get_head_icon(client.player_id)).as_uri(), "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 as_document = all_avatars and len(characters) > MAX_AVATAR_COUNT

View File

@ -7,10 +7,12 @@ from telegram.ext import CallbackContext, filters
from core.plugin import Plugin, handler from core.plugin import Plugin, handler
from core.services.cookies.error import TooManyRequestPublicCookies 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.models import RenderResult
from core.services.template.services import TemplateService from core.services.template.services import TemplateService
from plugins.tools.genshin import GenshinHelper from plugins.tools.genshin import GenshinHelper
from plugins.tools.head_icon import HeadIconService from plugins.tools.head_icon import HeadIconService
from plugins.tools.phone_theme import PhoneThemeService
from utils.log import logger from utils.log import logger
from utils.uid import mask_number from utils.uid import mask_number
@ -29,10 +31,14 @@ class PlayerStatsPlugins(Plugin):
template: TemplateService, template: TemplateService,
helper: GenshinHelper, helper: GenshinHelper,
head_icon: HeadIconService, head_icon: HeadIconService,
phone_theme: PhoneThemeService,
player_info_service: PlayerInfoService,
): ):
self.template_service = template self.template_service = template
self.helper = helper self.helper = helper
self.head_icon = head_icon 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: async def get_uid(self, user_id: int, args: List[str], reply: Optional[Message]) -> int:
"""通过消息获取 uid优先级args > reply > self""" """通过消息获取 uid优先级args > reply > self"""
@ -88,7 +94,7 @@ class PlayerStatsPlugins(Plugin):
except SimnetBadRequest: except SimnetBadRequest:
rogue = None rogue = None
logger.debug(user_info) logger.debug(user_info)
await self.set_name_card(uid, user_info.phone_background_image_url)
data = { data = {
"uid": mask_number(uid), "uid": mask_number(uid),
"info": user_info.info, "info": user_info.info,
@ -109,7 +115,7 @@ class PlayerStatsPlugins(Plugin):
], ],
"style": "xianzhou", # nosec "style": "xianzhou", # nosec
"avatar": (await self.head_icon.get_head_icon(uid)).as_uri(), "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( return await self.template_service.render(
@ -118,3 +124,13 @@ class PlayerStatsPlugins(Plugin):
{"width": 650, "height": 440}, {"width": 650, "height": 440},
full_page=True, 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)

View File

@ -1,5 +1,5 @@
from io import BytesIO 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 simnet.models.starrail.wish import StarRailBannerType
from telegram import Document, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User 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 modules.gacha_log.models import GachaLogInfo
from plugins.tools.genshin import PlayerNotFoundError from plugins.tools.genshin import PlayerNotFoundError
from plugins.tools.head_icon import HeadIconService from plugins.tools.head_icon import HeadIconService
from plugins.tools.phone_theme import PhoneThemeService
from utils.log import logger from utils.log import logger
try: try:
@ -57,12 +58,14 @@ class WishLogPlugin(Plugin.Conversation):
assets: AssetsService, assets: AssetsService,
cookie_service: CookiesService, cookie_service: CookiesService,
head_icon: HeadIconService, head_icon: HeadIconService,
phone_theme: PhoneThemeService,
): ):
self.template_service = template_service self.template_service = template_service
self.players_service = players_service self.players_service = players_service
self.assets_service = assets self.assets_service = assets
self.cookie_service = cookie_service self.cookie_service = cookie_service
self.head_icon = head_icon self.head_icon = head_icon
self.phone_theme = phone_theme
self.gacha_log = GachaLog() self.gacha_log = GachaLog()
self.wish_photo = None 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) data = await self.gacha_log.get_analysis(user_id, player_id, pool_type, self.assets_service)
if isinstance(data, str): if isinstance(data, str):
return data 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( png_data = await self.template_service.render(
"starrail/gacha_log/gacha_log.html", "starrail/gacha_log/gacha_log.html",
data, data,
@ -473,7 +476,7 @@ class WishLogPlugin(Plugin.Conversation):
await callback_query.answer(png_data, show_alert=True) await callback_query.answer(png_data, show_alert=True)
self.add_delete_message_job(message, delay=1) self.add_delete_message_job(message, delay=1)
else: 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) await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False)
document = False document = False
if png_data["hasMore"] and not group: if png_data["hasMore"] and not group:
@ -494,6 +497,11 @@ class WishLogPlugin(Plugin.Conversation):
else: else:
await png.edit_media(message) 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 @staticmethod
async def get_migrate_data( async def get_migrate_data(
old_user_id: int, new_user_id: int, old_players: List["Player"] old_user_id: int, new_user_id: int, old_players: List["Player"]

View File

@ -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()

View File

@ -9,7 +9,7 @@
</head> </head>
<body> <body>
<div class="container flex flex-col justify-center p-6"> <div class="container flex flex-col justify-center p-6">
<div class="header rounded-xl p-2 mb-6"> <div class="header rounded-xl p-2 mb-6" style='background-image: url("{{ background }}") '>
<div class="frame rounded-lg border-solid border-2 bg-contain p-4 flex items-center"> <div class="frame rounded-lg border-solid border-2 bg-contain p-4 flex items-center">
<img class="w-28 h-28 rounded-full mr-4" src="{{ avatar }}" alt="Avatar"> <img class="w-28 h-28 rounded-full mr-4" src="{{ avatar }}" alt="Avatar">
<div> <div>

View File

@ -20,11 +20,13 @@
.header { .header {
background: #e0dad3 url(../gacha_log/img/starrail.png) no-repeat right; background: #e0dad3 url(../gacha_log/img/starrail.png) no-repeat right;
box-shadow: 0 0 8px #72a2ae79; box-shadow: 0 0 8px #72a2ae79;
background-size: cover;
background-position: top;
} }
.frame { .frame {
border-color: #cdbea8; border-color: #cdbea8;
color: #8a4d30; color: white;
} }
.text-shadow { .text-shadow {

View File

@ -20,12 +20,15 @@
} }
.header { .header {
background: #e0dad3 url(../gacha_log/img/starrail.png) no-repeat right; /*background: #e0dad3 url(../gacha_log/img/starrail.png) no-repeat right;*/
box-shadow: 0 0 8px #72a2ae79; box-shadow: 0 0 8px #72a2ae79;
background-size: cover;
background-position: top;
} }
.frame { .frame {
border-color: #cdbea8; border-color: #cdbea8;
color: white;
} }
body { body {

View File

@ -15,7 +15,7 @@
<body id="container" class="body_box"> <body id="container" class="body_box">
<div class="container"> <div class="container">
<div class="info_box"> <div class="info_box">
<div class="header p-2 rounded-xl bg-contain mb-6"> <div class="header p-2 rounded-xl mb-6" style='background-image: url("{{ background }}")'>
<div class="frame p-4 rounded-lg border-solid border-2 flex items-center"> <div class="frame p-4 rounded-lg border-solid border-2 flex items-center">
<img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar"> <img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar">
<div> <div>

View File

@ -6,12 +6,15 @@
} }
.header { .header {
background: #e0dad3 url(./img/starrail.png) no-repeat right; /*background: #e0dad3 url(./img/starrail.png) no-repeat right;*/
box-shadow: 0 0 8px #72a2ae79; box-shadow: 0 0 8px #72a2ae79;
background-size: cover;
background-position: top;
} }
.frame { .frame {
border-color: #cdbea8; border-color: #cdbea8;
color: white;
} }
body { body {
@ -225,7 +228,7 @@ body {
} }
.label { .label {
color: #fff; color: #fff!important;
border-radius: 10px; border-radius: 10px;
font-size: 12px; font-size: 12px;
padding: 2px 7px; padding: 2px 7px;

View File

@ -11,7 +11,7 @@
<body id="container" class="body_box"> <body id="container" class="body_box">
<div class="container"> <div class="container">
<div class="info_box"> <div class="info_box">
<div class="header p-2 rounded-xl bg-contain mb-6"> <div class="header p-2 rounded-xl mb-6" style='background-image: url("{{ background }}")'>
<div class="frame p-4 rounded-lg border-solid border-2 flex items-center"> <div class="frame p-4 rounded-lg border-solid border-2 flex items-center">
<img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar"> <img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar">
<div> <div>