Enhance AssetsService

Co-authored-by: xtaodada <xtao@xtaolink.cn>
This commit is contained in:
Karako 2022-10-07 13:02:49 +08:00 committed by GitHub
parent e32a4c6e99
commit 4c702515a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1120 additions and 1099 deletions

View File

@ -1,8 +0,0 @@
from core.service import init_service
from core.assets.service import AssetsService
@init_service
def create_wiki_service():
_service = AssetsService()
return _service

View File

@ -1,174 +0,0 @@
from abc import ABC, abstractmethod
from pathlib import Path
from ssl import SSLZeroReturnError
from typing import ClassVar, Optional, Union
from aiofiles import open as async_open
from httpx import AsyncClient, HTTPError
from core.service import Service
from metadata.honey import HONEY_RESERVED_ID_MAP
from metadata.shortname import roleToId, roles
from modules.wiki.base import SCRAPE_HOST
from utils.const import PROJECT_ROOT
from utils.log import logger
from utils.typedefs import StrOrURL
ASSETS_PATH = PROJECT_ROOT.joinpath('resources/assets')
ASSETS_PATH.mkdir(exist_ok=True, parents=True)
class _AssetsService(ABC):
_dir: ClassVar[Path]
id: str
type: str
@property
def path(self) -> Path:
path = self._dir.joinpath(self.id)
path.mkdir(exist_ok=True, parents=True)
return path
def __init__(self, client: AsyncClient):
self._client = client
@abstractmethod
def __call__(self, target):
pass
def __init_subclass__(cls, **kwargs):
cls.type = cls.__name__.lstrip('_').split('Assets')[0].lower()
cls._dir = ASSETS_PATH.joinpath(cls.type)
cls._dir.mkdir(exist_ok=True)
return cls
async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Optional[Path]:
import asyncio
async def _task():
logger.debug(f"正在从 {url} 下载图标至 {path}")
for _ in range(retry):
try:
response = await self._client.get(url, follow_redirects=False)
except (HTTPError, SSLZeroReturnError):
await asyncio.sleep(1)
continue
if response.status_code != 200:
return None
async with async_open(path, 'wb') as file:
await file.write(response.content)
return path
task = asyncio.create_task(_task())
while not task.done():
await asyncio.sleep(0)
return task.result()
@abstractmethod
async def icon(self) -> Path:
pass
class _CharacterAssets(_AssetsService):
# noinspection SpellCheckingInspection
def __call__(self, target: Union[str, int]) -> "_CharacterAssets":
if isinstance(target, int):
if target == 10000005:
self.id = 'playerboy_005'
elif target == 10000007:
self.id = 'playergirl_007'
else:
self.id = f"{roles[target][2]}_{str(target)[-3:]}"
elif not target[-1].isdigit():
target = roleToId(target)
self.id = f"{roles[target][2]}_{str(target)[-3:]}"
else:
self.id = target
return self
async def icon(self) -> Path:
if (path := self.path.joinpath('icon.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_icon.webp')), path)
async def side(self) -> Path:
if (path := self.path.joinpath('side.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_side_icon.webp')), path)
async def gacha(self) -> Path:
if (path := self.path.joinpath('gacha.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_card.webp')), path)
async def splash(self) -> Optional[Path]:
if (path := self.path.joinpath('splash.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_splash.webp')), path)
class _WeaponAssets(_AssetsService):
def __call__(self, target: str) -> '_WeaponAssets':
if not target[-1].isdigit():
self.id = HONEY_RESERVED_ID_MAP['weapon'][target][0]
else:
self.id = target
return self
async def icon(self) -> Path:
if (path := self.path.joinpath('icon.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}.webp')), path)
async def awakened(self) -> Path:
if (path := self.path.joinpath('awakened.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_awaken_icon.webp')), path)
async def gacha(self) -> Path:
if (path := self.path.joinpath('gacha.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_icon.webp')), path)
class _MaterialAssets(_AssetsService):
def __call__(self, target) -> "_MaterialAssets":
if not target[-1].isdigit():
self.id = HONEY_RESERVED_ID_MAP['material'][target][0]
else:
self.id = target
return self
async def icon(self) -> Path:
if (path := self.path.joinpath('icon.webp')).exists():
return path
return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}.webp')), path)
class AssetsService(Service):
"""asset服务
用于储存和管理 asset :
当对应的 asset (如某角色图标)不存在时该服务会先查找本地
若本地不存在则从网络上下载若存在则返回其路径
"""
character: _CharacterAssets
weapon: _WeaponAssets
material: _MaterialAssets
def __init__(self):
self.client = AsyncClient()
self.character = _CharacterAssets(self.client)
self.weapon = _WeaponAssets(self.client)
self.material = _MaterialAssets(self.client)

513
core/base/assets.py Normal file
View File

@ -0,0 +1,513 @@
"""用于下载和管理角色、武器、材料等的图标"""
from __future__ import annotations
import asyncio
import re
from abc import ABC, abstractmethod
from functools import cached_property, partial
from multiprocessing import RLock as Lock
from pathlib import Path
from ssl import SSLZeroReturnError
from typing import Awaitable, Callable, ClassVar, Dict, Optional, TYPE_CHECKING, TypeVar, Union
from aiofiles import open as async_open
from aiofiles.os import remove as async_remove
from enkanetwork import Assets as EnkaAssets
from enkanetwork.model.assets import CharacterAsset as EnkaCharacterAsset
from httpx import AsyncClient, HTTPError, URL
from typing_extensions import Self
from core.service import Service
from metadata.genshin import AVATAR_DATA, HONEY_DATA, MATERIAL_DATA, NAMECARD_DATA, WEAPON_DATA
from metadata.scripts.honey import update_honey_metadata
from metadata.scripts.metadatas import update_metadata_from_ambr, update_metadata_from_github
from metadata.shortname import roleToId, weaponToId
from modules.wiki.base import HONEY_HOST
from utils.const import AMBR_HOST, ENKA_HOST, PROJECT_ROOT
from utils.log import logger
from utils.typedefs import StrOrInt, StrOrURL
if TYPE_CHECKING:
from multiprocessing.synchronize import RLock
ICON_TYPE = Union[
Callable[[bool], Awaitable[Optional[Path]]],
Callable[..., Awaitable[Optional[Path]]]
]
NAME_MAP_TYPE = Dict[str, StrOrURL]
ASSETS_PATH = PROJECT_ROOT.joinpath('resources/assets')
ASSETS_PATH.mkdir(exist_ok=True, parents=True)
DATA_MAP = {'avatar': AVATAR_DATA, 'weapon': WEAPON_DATA, 'material': MATERIAL_DATA}
class AssetsServiceError(Exception):
pass
class AssetsCouldNotFound(AssetsServiceError):
pass
class _AssetsService(ABC):
_lock: ClassVar['RLock'] = Lock()
_dir: ClassVar[Path]
icon_types: ClassVar[list[str]]
_client: Optional[AsyncClient] = None
id: int
type: str
icon: ICON_TYPE
"""图标"""
@abstractmethod
@cached_property
def game_name(self) -> str:
"""游戏数据中的名称"""
@cached_property
def honey_id(self) -> str:
"""当前资源在 Honey Impact 所对应的 ID"""
return HONEY_DATA[self.type].get(str(self.id), [''])[0]
@property
def path(self) -> Path:
"""当前资源的文件夹"""
result = self._dir.joinpath(str(self.id)).resolve()
result.mkdir(exist_ok=True, parents=True)
return result
@property
def client(self) -> AsyncClient:
with self._lock:
if self._client is None or self._client.is_closed:
self._client = AsyncClient()
return self._client
def __init__(self, client: Optional[AsyncClient] = None) -> None:
self._client = client
def __call__(self, target: int) -> Self:
"""用于生成与 target 对应的 assets"""
result = self.__class__(self.client)
result.id = target
return result
def __init_subclass__(cls, **kwargs) -> None:
"""初始化一些类变量"""
from itertools import chain
cls.icon_types = [ # 支持的图标类型
k
for k, v in
chain(
cls.__annotations__.items(),
*map(
lambda x: x.__annotations__.items(),
cls.__bases__
)
)
if v in [ICON_TYPE, 'ICON_TYPE']
]
cls.type = cls.__name__.lstrip('_').split('Assets')[0].lower() # 当前 assert 的类型
cls._dir = ASSETS_PATH.joinpath(cls.type) # 图标保存的文件夹
cls._dir.mkdir(exist_ok=True, parents=True)
async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Path | None:
"""从 url 下载图标至 path"""
logger.debug(f"正在从 {url} 下载图标至 {path}")
headers = {'user-agent': 'TGPaimonBot/3.0'} if URL(url).host == 'enka.network' else None
for time in range(retry):
try:
response = await self.client.get(url, follow_redirects=False, headers=headers)
except Exception as error: # pylint: disable=W0703
if not isinstance(error, (HTTPError, SSLZeroReturnError)):
logger.error(error) # 打印未知错误
if time != retry - 1: # 未达到重试次数
await asyncio.sleep(1)
else:
raise error
continue
if response.status_code != 200: # 判定页面是否正常
return None
async with async_open(path, 'wb') as file:
await file.write(response.content) # 保存图标
return path.resolve()
async def _get_from_ambr(self, item: str) -> Path | None: # pylint: disable=W0613,R0201
return None
async def _get_from_enka(self, item: str) -> Path | None: # pylint: disable=W0613,R0201
return None
async def _get_from_honey(self, item: str) -> Path | None:
"""从 honey 获取图标"""
if (url := self.honey_name_map.get(item, None)) is not None:
# 先尝试下载 png 格式的图片
path = self.path.joinpath(f"{item}.png")
if (result := await self._download(HONEY_HOST.join(f"img/{url}.png"), path)) is not None:
return result
path = self.path.joinpath(f"{item}.webp")
return await self._download(HONEY_HOST.join(f"img/{url}.webp"), path)
async def _get_img(self, overwrite: bool = False, *, item: str) -> Path | None:
"""获取图标"""
path = next(filter(lambda x: x.stem == item, self.path.iterdir()), None)
if not overwrite and path:
return path.resolve()
if overwrite and path is not None and path.exists():
await async_remove(path)
# 依次从使用当前 assets class 中的爬虫下载图标,顺序为爬虫名的字母顺序
for func in map(lambda x: getattr(self, x), sorted(filter(lambda x: x.startswith('_get_from_'), dir(self)))):
if (path := await func(item)) is not None:
return path
def __getattr__(self, item: str):
"""魔法"""
if item in self.icon_types:
return partial(self._get_img, item=item)
else:
object.__getattribute__(self, item)
@abstractmethod
@cached_property
def game_name_map(self) -> dict[str, str]:
"""游戏中的图标名"""
@abstractmethod
@cached_property
def honey_name_map(self) -> dict[str, str]:
"""来自honey的图标名"""
class _AvatarAssets(_AssetsService):
enka: EnkaCharacterAsset | None
side: ICON_TYPE
"""侧视图图标"""
card: ICON_TYPE
"""卡片图标"""
gacha: ICON_TYPE
"""抽卡立绘"""
gacha_card: ICON_TYPE
"""抽卡卡片"""
@cached_property
def game_name(self) -> str:
icon = "UI_AvatarIcon_"
if (avatar := AVATAR_DATA.get(str(self.id), None)) is not None:
icon = avatar['icon']
else:
for aid, avatar in AVATAR_DATA.items():
if aid.startswith(str(self.id)):
icon = avatar['icon']
return re.findall(r"UI_AvatarIcon_(.*)", icon)[0]
@cached_property
def honey_id(self) -> str:
return HONEY_DATA['avatar'].get(str(self.id), '')[0]
@cached_property
def enka(self) -> Optional[EnkaCharacterAsset]:
api = getattr(self, '_enka_api', None)
cid = getattr(self, 'id', None)
return None if api is None or cid is None else api.character(cid)
def __init__(self, client: Optional[AsyncClient] = None, enka: Optional[EnkaAssets] = None):
super().__init__(client)
self._enka_api = enka or EnkaAssets()
def __call__(self, target: StrOrInt) -> "_AvatarAssets":
temp = target
result = _AvatarAssets(self.client)
if isinstance(target, str):
try:
target = int(target)
except ValueError:
target = roleToId(target)
if isinstance(target, str) or target is None:
raise AssetsCouldNotFound(f"找不到对应的角色: target={temp}")
result.id = target
result._enka_api = self._enka_api
return result
async def _get_from_ambr(self, item: str) -> Path | None:
if item in {'icon', 'side', 'gacha'}:
url = AMBR_HOST.join(f"assets/UI/{self.game_name_map[item]}.png")
return await self._download(url, self.path.joinpath(f"{item}.png"))
async def _get_from_enka(self, item: str) -> Path | None:
path = self.path.joinpath(f"{item}.png")
item = 'banner' if item == 'gacha' else item
# noinspection PyUnboundLocalVariable
if (
self.enka is not None
and
item in (data := self.enka.images.dict()).keys()
and
(url := data[item]['url'])
):
return await self._download(url, path)
@cached_property
def honey_name_map(self) -> dict[str, str]:
return {
'icon': f"{self.honey_id}_icon",
'side': f"{self.honey_id}_side_icon",
'gacha': f"{self.honey_id}_gacha_splash",
'gacha_card': f"{self.honey_id}_gacha_card",
}
@cached_property
def game_name_map(self) -> dict[str, str]:
return {
'icon': f"UI_AvatarIcon_{self.game_name}",
'card': f"UI_AvatarIcon_{self.game_name}_Card",
'side': f"UI_AvatarIcon_Side_{self.game_name}",
'gacha': f"UI_Gacha_AvatarImg_{self.game_name}",
}
class _WeaponAssets(_AssetsService):
awaken: ICON_TYPE
"""突破后图标"""
gacha: ICON_TYPE
"""抽卡立绘"""
@cached_property
def game_name(self) -> str:
return re.findall(r"UI_EquipIcon_(.*)", WEAPON_DATA[str(self.id)]['icon'])[0]
@cached_property
def game_name_map(self) -> dict[str, str]:
return {
'icon': f"UI_EquipIcon_{self.game_name}",
'awaken': f"UI_EquipIcon_{self.game_name}_Awaken",
'gacha': f"UI_Gacha_EquipIcon_{self.game_name}"
}
@cached_property
def honey_id(self) -> str:
return f"i_n{self.id}"
def __call__(self, target: StrOrInt) -> Self:
temp = target
result = _WeaponAssets(self.client)
if isinstance(target, str):
target = int(target) if target.isnumeric() else weaponToId(target)
if isinstance(target, str) or target is None:
raise AssetsCouldNotFound(f"找不到对应的武器: target={temp}")
result.id = target
return result
async def _get_from_enka(self, item: str) -> Path | None:
if item in self.game_name_map:
url = ENKA_HOST.join(f'ui/{self.game_name_map.get(item)}.png')
path = self.path.joinpath(f"{item}.png")
return await self._download(url, path)
@cached_property
def honey_name_map(self) -> dict[str, str]:
return {
'icon': f'{self.honey_id}',
'awaken': f'{self.honey_id}_awaken_icon',
'gacha': f'{self.honey_id}_gacha_icon',
}
class _MaterialAssets(_AssetsService):
@cached_property
def game_name(self) -> str:
return str(self.id)
@cached_property
def game_name_map(self) -> dict[str, str]:
return {'icon': f"UI_ItemIcon_{self.game_name}"}
@cached_property
def honey_name_map(self) -> dict[str, str]:
return {'icon': self.honey_id}
def __call__(self, target: StrOrInt) -> Self:
temp = target
result = _MaterialAssets(self.client)
if isinstance(target, str):
if target.isnumeric():
target = int(target)
else:
target = {v['name']: int(k) for k, v in MATERIAL_DATA.items()}.get(target)
if isinstance(target, str) or target is None:
raise AssetsCouldNotFound(f"找不到对应的素材: target={temp}")
result.id = target
return result
async def _get_from_ambr(self, item: str) -> Path | None:
if item == 'icon':
url = AMBR_HOST.join(f"assets/UI/{self.game_name_map.get(item)}.png")
path = self.path.joinpath(f"{item}.png")
return await self._download(url, path)
async def _get_from_honey(self, item: str) -> Path | None:
path = self.path.joinpath(f"{item}.png")
url = HONEY_HOST.join(f'/img/{self.honey_name_map.get(item)}.png')
if (result := await self._download(url, path)) is None:
path = self.path.joinpath(f"{item}.webp")
url = HONEY_HOST.join(f'/img/{self.honey_name_map.get(item)}.webp')
return await self._download(url, path)
return result
class _ArtifactAssets(_AssetsService):
flower: ICON_TYPE
"""生之花"""
plume: ICON_TYPE
"""死之羽"""
sands: ICON_TYPE
"""时之沙"""
goblet: ICON_TYPE
"""空之杯"""
circlet: ICON_TYPE
"""理之冠"""
@cached_property
def honey_id(self) -> str:
return HONEY_DATA['artifact'][str(self.id)][0]
@cached_property
def game_name(self) -> str:
return f"UI_RelicIcon_{self.id}"
async def _get_from_enka(self, item: str) -> Path | None:
if item in self.game_name_map:
url = ENKA_HOST.join(f'ui/{self.game_name_map.get(item)}.png')
path = self.path.joinpath(f"{item}.png")
return await self._download(url, path)
async def _get_from_ambr(self, item: str) -> Path | None:
if item in self.game_name_map:
url = AMBR_HOST.join(f"assets/UI/reliquary/{self.game_name_map[item]}.png")
return await self._download(url, self.path.joinpath(f"{item}.png"))
@cached_property
def game_name_map(self) -> dict[str, str]:
return {
"icon": f"UI_RelicIcon_{self.id}_4",
"flower": f"UI_RelicIcon_{self.id}_4",
"plume": f"UI_RelicIcon_{self.id}_2",
"sands": f"UI_RelicIcon_{self.id}_5",
"goblet": f"UI_RelicIcon_{self.id}_1",
"circlet": f"UI_RelicIcon_{self.id}_3",
}
@cached_property
def honey_name_map(self) -> dict[str, str]:
first_id = int(re.findall(r'\d+', HONEY_DATA['artifact'][str(self.id)][-1])[0])
return {
"icon": f"i_n{first_id + 30}",
"flower": f"i_n{first_id + 30}",
"plume": f"i_n{first_id + 10}",
"sands": f"i_n{first_id + 40}",
"goblet": f"i_n{first_id}",
"circlet": f"i_n{first_id + 20}",
}
class _NamecardAssets(_AssetsService):
enka: EnkaCharacterAsset | None
navbar: ICON_TYPE
"""好友名片背景"""
profile: ICON_TYPE
"""个人资料名片背景"""
@cached_property
def honey_id(self) -> str:
return HONEY_DATA['namecard'][str(self.id)][0]
@cached_property
def game_name(self) -> str:
return NAMECARD_DATA[str(self.id)]['icon']
def __call__(self, target: int) -> "_NamecardAssets":
result = _NamecardAssets(self.client)
result.id = target
result.enka = EnkaAssets().namecards(target)
return result
async def _get_from_ambr(self, item: str) -> Path | None:
if item == 'profile':
url = AMBR_HOST.join(f"assets/UI/namecard/{self.game_name_map[item]}.png.png")
return await self._download(url, self.path.joinpath(f"{item}.png"))
async def _get_from_enka(self, item: str) -> Path | None:
path = self.path.joinpath(f"{item}.png")
url = getattr(self.enka, {'profile': 'banner'}.get(item, item), None)
if url is not None:
return await self._download(url.url, path)
@cached_property
def game_name_map(self) -> dict[str, str]:
return {
'icon': self.game_name,
'navbar': NAMECARD_DATA[str(self.id)]['navbar'],
'profile': NAMECARD_DATA[str(self.id)]['profile']
}
@cached_property
def honey_name_map(self) -> dict[str, str]:
return {
'icon': self.honey_id,
'navbar': f"{self.honey_id}_back",
'profile': f"{self.honey_id}_profile",
}
class AssetsService(Service):
"""asset服务
用于储存和管理 asset :
当对应的 asset (如某角色图标)不存在时该服务会先查找本地
若本地不存在则从网络上下载若存在则返回其路径
"""
avatar: _AvatarAssets
"""角色"""
weapon: _WeaponAssets
"""武器"""
material: _MaterialAssets
"""素材"""
artifact: _ArtifactAssets
"""圣遗物"""
namecard: _NamecardAssets
"""名片"""
def __init__(self):
for attr, assets_type_name in filter(
lambda x: (not x[0].startswith('_')) and x[1].endswith('Assets'),
self.__annotations__.items()
):
setattr(self, attr, globals()[assets_type_name]())
async def start(self): # pylint: disable=R0201
logger.info("正在刷新元数据")
await update_metadata_from_github(False)
await update_metadata_from_ambr(False)
await update_honey_metadata(False)
logger.info("刷新元数据成功")
AssetsServiceType = TypeVar('AssetsServiceType', bound=_AssetsService)

42
metadata/genshin.py Normal file
View File

@ -0,0 +1,42 @@
"""此文件用于储存 honey impact 中的部分基础数据"""
from __future__ import annotations
import ujson as json
from utils.const import PROJECT_ROOT
from utils.log import logger
from utils.typedefs import JSONType, StrOrInt
__all__ = [
'HONEY_DATA',
'AVATAR_DATA', 'WEAPON_DATA', 'MATERIAL_DATA', 'ARTIFACT_DATA', 'NAMECARD_DATA',
'honey_id_to_game_id'
]
data_dir = PROJECT_ROOT.joinpath('metadata/data/')
def _get_content(file_name: str) -> JSONType:
path = data_dir.joinpath(file_name).with_suffix('.json')
if not path.exists():
logger.error(
"暂未找到名为 \"{file_name}.json\" 的 metadata , 请先使用 [yellow bold]/refresh_metadata[/] 命令下载",
extra={'markup': True}
)
return {}
with open(path, encoding='utf-8') as file:
return json.load(file)
HONEY_DATA: dict[str, dict[StrOrInt, list[str | int]]] = _get_content('honey')
AVATAR_DATA: dict[str, dict[str, int | str | list[int]]] = _get_content('avatar')
WEAPON_DATA: dict[str, dict[str, int | str]] = _get_content('weapon')
MATERIAL_DATA: dict[str, dict[str, int | str]] = _get_content('material')
ARTIFACT_DATA: dict[str, dict[str, int | str | list[int] | dict[str, str]]] = _get_content('reliquary')
NAMECARD_DATA: dict[str, dict[str, int | str]] = _get_content('namecard')
def honey_id_to_game_id(honey_id: str, item_type: str) -> str | None:
return next((key for key, value in HONEY_DATA[item_type].items() if value[0] == honey_id), None)

View File

@ -1,464 +0,0 @@
"""此文件用于储存 honey impact 中的部分基础数据"""
__all__ = [
'HONEY_ID_MAP', 'HONEY_RESERVED_ID_MAP',
'HONEY_ROLE_NAME_MAP'
]
# noinspection SpellCheckingInspection
HONEY_ID_MAP = {
'character': {
'ayaka_002': ['神里绫华', 5],
'xiangling_023': ['香菱', 4],
'xingqiu_025': ['行秋', 4],
'albedo_038': ['阿贝多', 5],
'lisa_006': ['丽莎', 4],
'sucrose_043': ['砂糖', 4],
'mona_041': ['莫娜', 5],
'diona_039': ['迪奥娜', 4],
'venti_022': ['温迪', 5],
'xinyan_044': ['辛焱', 4],
'rosaria_045': ['罗莎莉亚', 4],
'hutao_046': ['胡桃', 5],
'zhongli_030': ['钟离', 5],
'ningguang_027': ['凝光', 4],
'eula_051': ['优菈', 5],
'shougun_052': ['雷电将军', 5],
'sayu_053': ['早柚', 4],
'keqing_042': ['刻晴', 5],
'ganyu_037': ['甘雨', 5],
'gorou_055': ['五郎', 4],
'tartaglia_033': ['达达利亚', 5],
'beidou_024': ['北斗', 4],
'itto_057': ['荒泷一斗', 5],
'ambor_021': ['安柏', 4],
'diluc_016': ['迪卢克', 5],
'chongyun_036': ['重云', 4],
'kaeya_015': ['凯亚', 4],
'aloy_062': ['埃洛伊', 4],
'yunjin_064': ['云堇', 4],
'shinobu_065': ['久岐忍', 4],
'ayato_066': ['神里绫人', 5],
'collei_067': ['柯莱', 4],
'feiyan_048': ['烟绯', 4],
'razor_020': ['雷泽', 4],
'barbara_014': ['芭芭拉', 4],
'dori_068': ['多莉', 4],
'noel_034': ['诺艾尔', 4],
'tighnari_069': ['提纳里', 5],
'kazuha_047': ['枫原万叶', 5],
'qiqi_035': ['七七', 5],
'bennett_032': ['班尼特', 4],
'nilou_070': ['妮露', 5],
'fischl_031': ['菲谢尔', 4],
'klee_029': ['可莉', 5],
'cyno_071': ['赛诺', 5],
'candace_072': ['坎蒂丝', 4],
'qin_003': ['', 5],
'xiao_026': ['', 5],
'playergirl_007': ['', 5],
'heizo_059': ['鹿野院平藏', 4],
'yoimiya_049': ['宵宫', 5],
'playerboy_005': ['', 5],
'sara_056': ['九条裟罗', 4],
'tohma_050': ['托马', 4],
'kokomi_054': ['珊瑚宫心海', 5],
'shenhe_063': ['申鹤', 5],
'yae_058': ['八重神子', 5],
'yelan_060': ['夜兰', 5]
},
'weapon': {
'i_n11401': ['西风剑', 4],
'i_n11305': ['吃虎鱼刀', 3],
'i_n11101': ['无锋剑', 1],
'i_n11303': ['旅行剑', 3],
'i_n11410': ['暗巷闪光', 4],
'i_n11301': ['冷刃', 3],
'i_n11416': ['笼钓瓶一心', 4],
'i_n11407': ['铁蜂刺', 4],
'i_n11501': ['风鹰剑', 5],
'i_n11419': ['「一心传」名刀', 4],
'i_n13409': ['龙脊长枪', 4],
'i_n13406': ['千岩长枪', 4],
'i_n11412': ['降临之剑', 4],
'i_n13505': ['和璞鸢', 5],
'i_n11504': ['斫峰之刃', 5],
'i_n11417': ['原木刀', 4],
'i_n13101': ['新手长枪', 1],
'i_n11509': ['雾切之回光', 5],
'i_n11502': ['天空之刃', 5],
'i_n14304': ['翡玉法球', 3],
'i_n13401': ['匣里灭辰', 4],
'i_n11413': ['腐殖之剑', 4],
'i_n11404': ['宗室长剑', 4],
'i_n11418': ['西福斯的月光', 4],
'i_n11415': ['辰砂之纺锤', 4],
'i_n11503': ['苍古自由之誓', 5],
'i_n13402': ['试作星镰', 4],
'i_n11511': ['圣显之钥', 5],
'i_n11409': ['黑剑', 4],
'i_n11414': ['天目影打刀', 4],
'i_n11405': ['匣里龙吟', 4],
'i_n11510': ['波乱月白经津', 5],
'i_n13405': ['决斗之枪', 4],
'i_n13407': ['西风长枪', 4],
'i_n11408': ['黑岩长剑', 4],
'i_n14306': ['琥珀玥', 3],
'i_n11505': ['磐岩结绿', 5],
'i_n14408': ['黑岩绯玉', 4],
'i_n14417': ['盈满之实', 4],
'i_n14416': ['流浪的晚星', 4],
'i_n12301': ['铁影阔剑', 3],
'i_n14506': ['不灭月华', 5],
'i_n14305': ['甲级宝珏', 3],
'i_n13415': ['「渔获」', 4],
'i_n14402': ['流浪乐章', 4],
'i_n12402': ['钟剑', 4],
'i_n12201': ['佣兵重剑', 2],
'i_n14403': ['祭礼残章', 4],
'i_n14405': ['匣里日月', 4],
'i_n12101': ['训练大剑', 1],
'i_n14501': ['天空之卷', 5],
'i_n14413': ['嘟嘟可故事集', 4],
'i_n14302': ['讨龙英杰谭', 3],
'i_n14303': ['异世界行记', 3],
'i_n12303': ['白铁大剑', 3],
'i_n14409': ['昭心', 4],
'i_n13502': ['天空之脊', 5],
'i_n14404': ['宗室秘法录', 4],
'i_n14401': ['西风秘典', 4],
'i_n14415': ['证誓之明瞳', 4],
'i_n13301': ['白缨枪', 3],
'i_n13404': ['黑岩刺枪', 4],
'i_n13408': ['宗室猎枪', 4],
'i_n13201': ['铁尖枪', 2],
'i_n13511': ['赤沙之杖', 5],
'i_n13416': ['断浪长鳍', 4],
'i_n13509': ['薙草之稻光', 5],
'i_n13403': ['流月针', 4],
'i_n13417': ['贯月矢', 4],
'i_n13419': ['风信之锋', 4],
'i_n13302': ['钺矛', 3],
'i_n11406': ['试作斩岩', 4],
'i_n13414': ['喜多院十文字', 4],
'i_n14201': ['口袋魔导书', 2],
'i_n13501': ['护摩之杖', 5],
'i_n13303': ['黑缨枪', 3],
'i_n14101': ['学徒笔记', 1],
'i_n12401': ['西风大剑', 4],
'i_n12304': ['石英大剑', 3],
'i_n14412': ['忍冬之果', 4],
'i_n14414': ['白辰之环', 4],
'i_n14509': ['神乐之真意', 5],
'i_n14406': ['试作金珀', 4],
'i_n14502': ['四风原典', 5],
'i_n12305': ['以理服人', 3],
'i_n14504': ['尘世之锁', 5],
'i_n12306': ['飞天大御剑', 3],
'i_n14301': ['魔导绪论', 3],
'i_n12302': ['沐浴龙血的剑', 3],
'i_n14407': ['万国诸海图谱', 4],
'i_n13504': ['贯虹之槊', 5],
'i_n12416': ['恶王丸', 4],
'i_n12409': ['螭骨剑', 4],
'i_n12404': ['宗室大剑', 4],
'i_n12405': ['雨裁', 4],
'i_n12414': ['桂木斩长正', 4],
'i_n12408': ['黑岩斩刀', 4],
'i_n12410': ['千岩古剑', 4],
'i_n12406': ['试作古华', 4],
'i_n12415': ['玛海菈的水色', 4],
'i_n12403': ['祭礼大剑', 4],
'i_n12411': ['雪葬的星银', 4],
'i_n12412': ['衔珠海皇', 4],
'i_n12407': ['白影剑', 4],
'i_n11201': ['银剑', 2],
'i_n12504': ['无工之剑', 5],
'i_n15305': ['信使', 3],
'i_n15411': ['落霞', 4],
'i_n15413': ['风花之颂', 4],
'i_n15401': ['西风猎弓', 4],
'i_n12510': ['赤角石溃杵', 5],
'i_n15405': ['弓藏', 4],
'i_n15403': ['祭礼弓', 4],
'i_n15201': ['历练的猎弓', 2],
'i_n15404': ['宗室长弓', 4],
'i_n15302': ['神射手之誓', 3],
'i_n12501': ['天空之傲', 5],
'i_n15402': ['绝弦', 4],
'i_n12417': ['森林王器', 4],
'i_n11306': ['飞天御剑', 3],
'i_n15410': ['暗巷猎手', 4],
'i_n15414': ['破魔之弓', 4],
'i_n15101': ['猎弓', 1],
'i_n15415': ['掠食者', 4],
'i_n15301': ['鸦羽弓', 3],
'i_n11304': ['暗铁剑', 3],
'i_n15303': ['反曲弓', 3],
'i_n15306': ['黑檀弓', 3],
'i_n15408': ['黑岩战弓', 4],
'i_n15304': ['弹弓', 3],
'i_n15409': ['苍翠猎弓', 4],
'i_n15412': ['幽夜华尔兹', 4],
'i_n15406': ['试作澹月', 4],
'i_n15417': ['王下近侍', 4],
'i_n15501': ['天空之翼', 5],
'i_n15418': ['竭泽', 4],
'i_n15507': ['冬极白星', 5],
'i_n15508': ['若水', 5],
'i_n15503': ['终末嗟叹之诗', 5],
'i_n15509': ['飞雷之弦振', 5],
'i_n15511': ['猎人之径', 5],
'i_n12502': ['狼的末路', 5],
'i_n15407': ['钢轮弓', 4],
'i_n12503': ['松籁响起之时', 5],
'i_n15416': ['曚云之月', 4],
'i_n11402': ['笛剑', 4],
'i_n15502': ['阿莫斯之弓', 5],
'i_n11302': ['黎明神剑', 3],
'i_n13507': ['息灾', 5],
'i_n11403': ['祭礼剑', 4],
'i_n14410': ['暗巷的酒与诗', 4]
},
'material': {
'i_413': ['「勤劳」的哲学', 4],
'i_411': ['「勤劳」的教导', 2],
'i_n104333': ['「巧思」的指引', 3],
'i_584': ['今昔剧画之鬼人', 5],
'i_427': ['「天光」的指引', 3],
'i_453': ['「抗争」的哲学', 4],
'i_n112063': ['休眠菌核', 3],
'i_408': ['「浮世」的哲学', 4],
'i_n104336': ['「笃行」的指引', 3],
'i_n104337': ['「笃行」的哲学', 4],
'i_421': ['「自由」的教导', 2],
'i_n112068': ['混沌容器', 2],
'i_n104331': ['「诤言」的哲学', 4],
'i_n104330': ['「诤言」的指引', 3],
'i_402': ['「诗文」的指引', 3],
'i_416': ['「风雅」的教导', 2],
'i_423': ['「自由」的哲学', 4],
'i_407': ['「浮世」的指引', 3],
'i_581': ['今昔剧画之恶尉', 2],
'i_401': ['「诗文」的教导', 2],
'i_441': ['「繁荣」的教导', 2],
'i_n104335': ['「笃行」的教导', 2],
'i_422': ['「自由」的指引', 3],
'i_432': ['「黄金」的指引', 3],
'i_n104332': ['「巧思」的教导', 2],
'i_53': ['历战的箭簇', 3],
'i_417': ['「风雅」的指引', 3],
'i_431': ['「黄金」的教导', 2],
'i_403': ['「诗文」的哲学', 4],
'i_451': ['「抗争」的教导', 2],
'i_462': ['东风之爪', 5],
'i_461': ['东风之翎', 5],
'i_524': ['凛风奔狼的怀乡', 5],
'i_433': ['「黄金」的哲学', 4],
'i_406': ['「浮世」的教导', 2],
'i_452': ['「抗争」的指引', 3],
'i_n104334': ['「巧思」的哲学', 4],
'i_133': ['原素花蜜', 3],
'i_582': ['今昔剧画之虎啮', 3],
'i_442': ['「繁荣」的指引', 3],
'i_483': ['凶将之手眼', 5],
'i_583': ['今昔剧画之一角', 4],
'i_418': ['「风雅」的哲学', 4],
'i_463': ['东风的吐息', 5],
'i_443': ['「繁荣」的哲学', 4],
'i_521': ['凛风奔狼的始龀', 2],
'i_n104329': ['「诤言」的教导', 2],
'i_464': ['北风之尾', 5],
'i_61': ['沉重号角', 2],
'i_523': ['凛风奔狼的断牙', 4],
'i_485': ['万劫之真意', 5],
'i_33': ['不祥的面具', 3],
'i_467': ['吞天之鲸·只角', 5],
'i_465': ['北风之环', 5],
'i_21': ['史莱姆凝液', 1],
'i_513': ['孤云寒林的圣骸', 4],
'i_185': ['浮游干核', 1],
'i_511': ['孤云寒林的光砂', 2],
'i_522': ['凛风奔狼的裂齿', 3],
'i_514': ['孤云寒林的神体', 5],
'i_412': ['「勤劳」的指引', 3],
'i_466': ['北风的魂匣', 5],
'i_173': ['混沌真眼', 4],
'i_73': ['地脉的新芽', 4],
'i_183': ['偏光棱镜', 4],
'i_n112061': ['孢囊晶尘', 3],
'i_83': ['混沌炉心', 4],
'i_142': ['结实的骨片', 3],
'i_22': ['史莱姆清', 2],
'i_112': ['士官的徽记', 2],
'i_23': ['史莱姆原浆', 3],
'i_163': ['名刀镡', 3],
'i_n112062': ['失活菌核', 2],
'i_n112072': ['混浊棱晶', 3],
'i_428': ['「天光」的哲学', 4],
'i_552': ['漆黑陨铁的一片', 3],
'i_172': ['混沌枢纽', 3],
'i_72': ['地脉的枯叶', 3],
'i_426': ['「天光」的教导', 2],
'i_n112070': ['混沌锚栓', 4],
'i_491': ['智识之冕', 5],
'i_123': ['攫金鸦印', 3],
'i_121': ['寻宝鸦印', 1],
'i_153': ['幽邃刻像', 4],
'i_41': ['导能绘卷', 1],
'i_187': ['浮游晶化核', 3],
'i_32': ['污秽的面具', 2],
'i_469': ['武炼之魂·孤影', 5],
'i_553': ['漆黑陨铁的一角', 4],
'i_151': ['晦暗刻像', 2],
'i_182': ['水晶棱镜', 3],
'i_71': ['地脉的旧枝', 2],
'i_512': ['孤云寒林的辉岩', 3],
'i_42': ['封魔绘卷', 2],
'i_132': ['微光花蜜', 2],
'i_152': ['夤夜刻像', 3],
'i_186': ['浮游幽核', 2],
'i_482': ['灰烬之心', 5],
'i_81': ['混沌装置', 2],
'i_82': ['混沌回路', 3],
'i_n114045': ['烈日威权的残响', 2],
'i_541': ['狮牙斗士的枷锁', 2],
'i_n114046': ['烈日威权的余光', 3],
'i_171': ['混沌机关', 2],
'i_51': ['牢固的箭簇', 1],
'i_542': ['狮牙斗士的铁链', 3],
'i_111': ['新兵的徽记', 1],
'i_n112069': ['混沌模块', 3],
'i_n114048': ['烈日威权的旧日', 5],
'i_162': ['影打刀镡', 2],
'i_481': ['狱火之蝶', 5],
'i_554': ['漆黑陨铁的一块', 5],
'i_n112071': ['破缺棱晶', 2],
'i_143': ['石化的骨片', 4],
'i_103': ['督察长祭刀', 4],
'i_31': ['破损的面具', 1],
'i_43': ['禁咒绘卷', 3],
'i_n114043': ['绿洲花园的哀思', 4],
'i_n112067': ['织金红绸', 3],
'i_n114042': ['绿洲花园的恩惠', 3],
'i_n114044': ['绿洲花园的真谛', 5],
'i_n112060': ['荧光孢粉', 2],
'i_122': ['藏银鸦印', 2],
'i_n112065': ['褪色红绸', 1],
'i_141': ['脆弱的骨片', 2],
'i_n114040': ['谧林涓露的金符', 5],
'i_n114039': ['谧林涓露的银符', 4],
'i_562': ['远海夷地的玉枝', 3],
'i_563': ['远海夷地的琼枝', 4],
'i_564': ['远海夷地的金枝', 5],
'i_n114038': ['谧林涓露的铁符', 3],
'i_n114037': ['谧林涓露的铜符', 2],
'i_52': ['锐利的箭簇', 2],
'i_n112066': ['镶边红绸', 2],
'i_174': ['隐兽指爪', 2],
'i_176': ['隐兽鬼爪', 4],
'i_534': ['雾海云间的转还', 5],
'i_531': ['雾海云间的铅丹', 2],
'i_503': ['高塔孤王的断片', 4],
'i_91': ['雾虚花粉', 2],
'i_92': ['雾虚草囊', 3],
'i_501': ['高塔孤王的破瓦', 2],
'i_504': ['高塔孤王的碎梦', 5],
'i_468': ['魔王之刃·残片', 5],
'i_544': ['狮牙斗士的理想', 5],
'i_470': ['龙王之冕', 5],
'i_572': ['鸣神御灵的欢喜', 3],
'i_574': ['鸣神御灵的勇武', 5],
'i_573': ['鸣神御灵的亲爱', 4],
'i_480': ['熔毁之刻', 5],
'i_62': ['黑铜号角', 3],
'i_101': ['猎兵祭刀', 2],
'i_551': ['漆黑陨铁的一粒', 2],
'i_484': ['祸神之禊泪', 5],
'i_n114041': ['绿洲花园的追忆', 2],
'i_n112059': ['蕈兽孢子', 1],
'i_561': ['远海夷地的瑚枝', 2],
'i_n112073': ['辉光棱晶', 4],
'i_175': ['隐兽利爪', 3],
'i_532': ['雾海云间的汞丹', 3],
'i_93': ['雾虚灯芯', 4],
'i_131': ['骗骗花蜜', 1],
'i_571': ['鸣神御灵的明惠', 2],
'i_n114047': ['烈日威权的梦想', 4],
'i_63': ['黑晶号角', 4],
'i_543': ['狮牙斗士的镣铐', 4],
'i_n112064': ['茁壮菌核', 4],
'i_161': ['破旧的刀镡', 1],
'i_472': ['鎏金之鳞', 5],
'i_113': ['尉官的徽记', 3],
'i_502': ['高塔孤王的残垣', 3],
'i_181': ['黯淡棱镜', 2],
'i_102': ['特工祭刀', 3],
'i_471': ['血玉之枝', 5],
'i_533': ['雾海云间的金丹', 4]
}
}
HONEY_RESERVED_ID_MAP = {
k: {j[0]: [i, j[1]] for i, j in v.items()} for k, v in HONEY_ID_MAP.items()
}
# noinspection SpellCheckingInspection
HONEY_ROLE_NAME_MAP = {
10000002: ['ayaka_002', '神里绫华', 'ayaka'],
10000042: ['keqing_042', '刻晴', 'keqing'],
10000030: ['zhongli_030', '钟离', 'zhongli'],
10000026: ['xiao_026', '', 'xiao'],
10000020: ['razor_020', '雷泽', 'razor'],
10000015: ['kaeya_015', '凯亚', 'kaeya'],
10000037: ['ganyu_037', '甘雨', 'ganyu'],
10000041: ['mona_041', '莫娜', 'mona'],
10000038: ['albedo_038', '阿贝多', 'albedo'],
10000014: ['barbara_014', '芭芭拉', 'barbara'],
10000027: ['ningguang_027', '凝光', 'ningguang'],
10000054: ['kokomi_054', '珊瑚宫心海', 'kokomi'],
10000044: ['xinyan_044', '辛焱', 'xinyan'],
10000056: ['sara_056', '九条裟罗', 'sara'],
10000053: ['sayu_053', '早柚', 'sayu'],
10000043: ['sucrose_043', '砂糖', 'sucrose'],
10000059: ['heizo_059', '鹿野院平藏', 'heizo'],
10000060: ['yelan_060', '夜兰', 'yelan'],
10000064: ['yunjin_064', '云堇', 'yunjin'],
10000050: ['tohma_050', '托马', 'tohma'],
10000066: ['ayato_066', '神里绫人', 'ayato'],
10000067: ['collei_067', '柯莱', 'collei'],
10000052: ['shougun_052', '雷电将军', 'shougun'],
10000069: ['tighnari_069', '提纳里', 'tighnari'],
10000007: ['playergirl_007', '', 'playergirl'],
10000016: ['diluc_016', '迪卢克', 'diluc'],
10000070: ['nilou_070', '妮露', 'nilou'],
10000047: ['kazuha_047', '枫原万叶', 'kazuha'],
10000055: ['gorou_055', '五郎', 'gorou'],
10000034: ['noel_034', '诺艾尔', 'noel'],
10000024: ['beidou_024', '北斗', 'beidou'],
10000032: ['bennett_032', '班尼特', 'bennett'],
10000062: ['aloy_062', '埃洛伊', 'aloy'],
10000025: ['xingqiu_025', '行秋', 'xingqiu'],
10000022: ['venti_022', '温迪', 'venti'],
10000036: ['chongyun_036', '重云', 'chongyun'],
10000049: ['yoimiya_049', '宵宫', 'yoimiya'],
10000029: ['klee_029', '可莉', 'klee'],
10000006: ['lisa_006', '丽莎', 'lisa'],
10000033: ['tartaglia_033', '达达利亚', 'tartaglia'],
10000039: ['diona_039', '迪奥娜', 'diona'],
10000063: ['shenhe_063', '申鹤', 'shenhe'],
10000072: ['candace_072', '坎蒂丝', 'candace'],
10000045: ['rosaria_045', '罗莎莉亚', 'rosaria'],
10000051: ['eula_051', '优菈', 'eula'],
10000035: ['qiqi_035', '七七', 'qiqi'],
10000057: ['itto_057', '荒泷一斗', 'itto'],
10000005: ['playerboy_005', '', 'playerboy'],
10000048: ['feiyan_048', '烟绯', 'feiyan'],
10000003: ['qin_003', '', 'qin'],
10000023: ['xiangling_023', '香菱', 'xiangling'],
10000071: ['cyno_071', '赛诺', 'cyno'],
10000031: ['fischl_031', '菲谢尔', 'fischl'],
10000046: ['hutao_046', '胡桃', 'hutao'],
10000021: ['ambor_021', '安柏', 'ambor'],
10000068: ['dori_068', '多莉', 'dori'],
10000065: ['shinobu_065', '久岐忍', 'shinobu'],
10000058: ['yae_058', '八重神子', 'yae']
}

188
metadata/scripts/honey.py Normal file
View File

@ -0,0 +1,188 @@
from __future__ import annotations
import asyncio
import re
from typing import Dict, List, Optional
import ujson as json
from aiofiles import open as async_open
from httpx import AsyncClient, HTTPError, Response
from modules.wiki.base import HONEY_HOST
from utils.const import PROJECT_ROOT
from utils.log import logger
from utils.typedefs import StrOrInt
__all__ = [
'get_avatar_data', 'get_artifact_data', 'get_material_data', 'get_namecard_data', 'get_weapon_data',
'update_honey_metadata',
]
DATA_TYPE = Dict[StrOrInt, List[str]]
FULL_DATA_TYPE = Dict[str, DATA_TYPE]
client = AsyncClient()
async def request(url: str, retry: int = 5) -> Optional[Response]:
for time in range(retry):
try:
return await client.get(url)
except HTTPError:
if time != retry - 1:
await asyncio.sleep(1)
continue
return None
except Exception as e:
raise e
async def get_avatar_data() -> DATA_TYPE:
result = {}
url = "https://genshin.honeyhunterworld.com/fam_chars/?lang=CHS"
response = await request(url)
chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', response.text)[0]
json_data = json.loads(chaos_data) # 转为 json
for data in json_data:
cid = int("10000" + re.findall(r'\d+', data[1])[0])
honey_id = re.findall(r"/(.*?)/", data[1])[0]
name = re.findall(r'>(.*)<', data[1])[0]
rarity = int(re.findall(r">(\d)<", data[2])[0])
result[cid] = [honey_id, name, rarity]
return result
async def get_weapon_data() -> DATA_TYPE:
from modules.wiki.other import WeaponType
result = {}
urls = [HONEY_HOST.join(f"fam_{i.lower()}/?lang=CHS") for i in WeaponType.__members__]
for url in urls:
response = await request(url)
chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', response.text)[0]
json_data = json.loads(chaos_data) # 转为 json
for data in json_data:
name = re.findall(r'>(.*)<', data[1])[0]
if name in ['「一心传」名刀', '石英大剑', '琥珀玥', '黑檀弓']: # 跳过特殊的武器
continue
wid = int(re.findall(r'\d+', data[1])[0])
honey_id = re.findall(r"/(.*?)/", data[1])[0]
rarity = int(re.findall(r">(\d)<", data[2])[0])
result[wid] = [honey_id, name, rarity]
return result
async def get_material_data() -> DATA_TYPE:
result = {}
weapon = [HONEY_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']]
talent = [HONEY_HOST.join(f'fam_talent_{i}/?lang=CHS') for i in ['book', 'boss', 'common', 'reward']]
namecard = [HONEY_HOST.join("fam_nameplate/?lang=CHS")]
urls = weapon + talent + namecard
response = await request("https://api.ambr.top/v2/chs/material")
ambr_data = json.loads(response.text)['data']['items']
for url in urls:
response = await request(url)
chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', response.text)[0]
json_data = json.loads(chaos_data) # 转为 json
for data in json_data:
honey_id = re.findall(r'/(.*?)/', data[1])[0]
name = re.findall(r'>(.*)<', data[1])[0]
rarity = int(re.findall(r">(\d)<", data[2])[0])
mid = None
for mid, item in ambr_data.items():
if name == item['name']:
break
mid = int(mid) or int(re.findall(r'\d+', data[1])[0])
result[mid] = [honey_id, name, rarity]
return result
async def get_artifact_data() -> DATA_TYPE:
async def get_first_id(_link) -> str:
_response = await request(_link)
_chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', _response.text)[0]
_json_data = json.loads(_chaos_data)
return re.findall(r"/(.*?)/", _json_data[-1][1])[0]
result = {}
url = "https://genshin.honeyhunterworld.com/fam_art_set/?lang=CHS"
response = await request("https://api.ambr.top/v2/chs/reliquary")
ambr_data = json.loads(response.text)['data']['items']
response = await request(url)
chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', response.text)[0]
json_data = json.loads(chaos_data) # 转为 json
for data in json_data:
honey_id = re.findall(r'/(.*?)/', data[1])[0]
name = re.findall(r"alt=\"(.*?)\"", data[0])[0]
link = HONEY_HOST.join(re.findall(r'href="(.*?)"', data[0])[0])
first_id = await get_first_id(link)
aid = None
for aid, item in ambr_data.items():
if name == item['name']:
break
aid = aid or re.findall(r'\d+', data[1])[0]
result[aid] = [honey_id, name, first_id]
return result
async def get_namecard_data() -> DATA_TYPE:
from metadata.genshin import NAMECARD_DATA
if not NAMECARD_DATA:
# noinspection PyProtectedMember
from metadata.genshin import _get_content
from metadata.scripts.metadatas import update_metadata_from_github
await update_metadata_from_github()
# noinspection PyPep8Naming
NAMECARD_DATA = _get_content('namecard')
url = HONEY_HOST.join("fam_nameplate/?lang=CHS")
result = {}
response = await request(url)
chaos_data = re.findall(r'sortable_data\.push\((.*)\);\s*sortable_cur_page', response.text)[0]
json_data = json.loads(chaos_data)
for data in json_data:
honey_id = re.findall(r'/(.*?)/', data[1])[0]
name = re.findall(r"alt=\"(.*?)\"", data[0])[0]
try:
nid = [key for key, value in NAMECARD_DATA.items() if value['name'] == name][0]
except IndexError: # 暂不支持 beta 的名片
continue
rarity = int(re.findall(r">(\d)<", data[2])[0])
result[nid] = [honey_id, name, rarity]
return result
async def update_honey_metadata(overwrite: bool = True) -> FULL_DATA_TYPE | None:
path = PROJECT_ROOT.joinpath('metadata/data/honey.json')
if not overwrite and path.exists():
return
avatar_data = await get_avatar_data()
logger.success("Avatar data is done.")
weapon_data = await get_weapon_data()
logger.success("Weapon data is done.")
material_data = await get_material_data()
logger.success("Material data is done.")
artifact_data = await get_artifact_data()
logger.success("Artifact data is done.")
namecard_data = await get_namecard_data()
logger.success("Namecard data is done.")
result = {
'avatar': avatar_data,
'weapon': weapon_data,
'material': material_data,
'artifact': artifact_data,
'namecard': namecard_data,
}
path.parent.mkdir(parents=True, exist_ok=True)
async with async_open(path, mode='w', encoding='utf-8') as file:
await file.write(json.dumps(result, ensure_ascii=False))
return result

View File

@ -0,0 +1,65 @@
import ujson as json
from aiofiles import open as async_open
from httpx import AsyncClient, URL
from utils.const import AMBR_HOST, PROJECT_ROOT
__all__ = ['update_metadata_from_ambr', 'update_metadata_from_github']
client = AsyncClient()
async def update_metadata_from_ambr(overwrite: bool = True):
result = []
targets = ['material', 'weapon', 'avatar', 'reliquary']
for target in targets:
path = PROJECT_ROOT.joinpath(f'metadata/data/{target}.json')
if not overwrite and path.exists():
continue
url = AMBR_HOST.join(f"v2/chs/{target}")
path.parent.mkdir(parents=True, exist_ok=True)
response = await client.get(url)
json_data = json.loads(response.text)['data']['items']
async with async_open(path, mode='w', encoding='utf-8') as file:
data = json.dumps(json_data, ensure_ascii=False)
await file.write(data)
result.append(json_data)
return result
async def update_metadata_from_github(overwrite: bool = True):
path = PROJECT_ROOT.joinpath('metadata/data/namecard.json')
if not overwrite and path.exists():
return
host = URL("https://raw.fastgit.org/Dimbreath/GenshinData/master/")
text_map_url = host.join("TextMap/TextMapCHS.json")
material_url = host.join("ExcelBinOutput/MaterialExcelConfigData.json")
text_map_json_data = json.loads((await client.get(text_map_url)).text)
material_json_data = json.loads((await client.get(material_url)).text)
data = {}
for namecard_data in filter(lambda x: x.get('materialType', None) == 'MATERIAL_NAMECARD', material_json_data):
name = text_map_json_data[str(namecard_data['nameTextMapHash'])]
icon = namecard_data['icon']
navbar = namecard_data['picPath'][0]
banner = namecard_data['picPath'][1]
rank = namecard_data['rankLevel']
description = text_map_json_data[str(namecard_data['descTextMapHash'])].replace('\\n', '\n')
data.update({
str(namecard_data['id']): {
"id": namecard_data['id'],
"name": name,
"rank": rank,
"icon": icon,
"navbar": navbar,
"profile": banner,
"description": description,
}
})
async with async_open(path, mode='w', encoding='utf-8') as file:
data = json.dumps(data, ensure_ascii=False)
await file.write(data)
return data

View File

@ -1,8 +1,10 @@
from typing import Optional
from __future__ import annotations
from metadata.genshin import WEAPON_DATA
__all__ = [
'roles', 'weapons',
'roleToId', 'roleToName', 'weaponToName',
'roleToId', 'roleToName', 'weaponToName', 'weaponToId'
]
# noinspection SpellCheckingInspection
@ -89,7 +91,8 @@ roles = {
10000067: ['柯莱', 'Collei', 'collei', '柯来', '科莱', '科来', '小天使', '须弥安柏', '须弥飞行冠军', '见习巡林员',
'克莱', '草安伯'],
10000068: ['多莉', 'Dori', 'dori', '多利', '多力', '多丽', '奸商'],
10000069: ['提纳里', 'Tighnari', 'tighnari', '小提', '提那里', '缇娜里', '提哪里', '', '柯莱老师', '柯莱师傅', '巡林官',
10000069: ['提纳里', 'Tighnari', 'tighnari', '小提', '提那里', '缇娜里', '提哪里', '', '柯莱老师', '柯莱师傅',
'巡林官',
'提那里'],
10000070: ['妮露', 'Nilou', 'nilou', '尼露', '尼禄'],
10000071: ['赛诺', 'Cyno', 'cyno', '赛洛'],
@ -109,7 +112,7 @@ weapons = {
"松籁响起之时": ["松籁", "乐团大剑", "松剑"],
"苍古自由之誓": ["苍古", "乐团剑"],
"渔获": [""],
"渔获": ["", ""],
"衔珠海皇": ["海皇", "咸鱼剑", "咸鱼大剑"],
"匣里日月": ["日月"],
@ -185,7 +188,7 @@ def roleToName(shortname: str) -> str:
# noinspection PyPep8Naming
def roleToId(name: str) -> Optional[int]:
def roleToId(name: str) -> int | None:
"""获取角色ID"""
return next((key for key, value in roles.items() for n in value if n == name), None)
@ -194,3 +197,9 @@ def roleToId(name: str) -> Optional[int]:
def weaponToName(shortname: str) -> str:
"""讲武器昵称转为正式名"""
return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname)
# noinspection PyPep8Naming
def weaponToId(name: str) -> int | None:
"""获取武器ID"""
return next((int(key) for key, value in WEAPON_DATA.items() if weaponToName(name) in value['name']), None)

View File

@ -2,7 +2,7 @@ import time
from typing import List, Optional
import httpx
from pydantic import BaseModel, validator, parse_obj_as
from pydantic import BaseModel, parse_obj_as, validator
class Member(BaseModel):
@ -17,7 +17,7 @@ class TeamRate(BaseModel):
ownerNum: Optional[int]
@validator('rate', pre=True)
def str2float(cls, v): # pylint: disable=R0201
def str2float(cls, v): # pylint: disable=R0201
return float(v.replace('%', '')) / 100.0 if isinstance(v, str) else v

View File

@ -16,9 +16,9 @@ from pydantic import (
)
from typing_extensions import Self
__all__ = ['Model', 'WikiModel', 'SCRAPE_HOST']
__all__ = ['Model', 'WikiModel', 'HONEY_HOST']
SCRAPE_HOST = URL("https://genshin.honeyhunterworld.com/")
HONEY_HOST = URL("https://genshin.honeyhunterworld.com/")
class Model(PydanticBaseModel):
@ -130,10 +130,7 @@ class WikiModel(Model):
返回对应的 WikiModel
"""
url = await cls.get_url_by_name(name)
if url is None:
return None
else:
return await cls._scrape(url)
return None if url is None else await cls._scrape(url)
@classmethod
async def get_full_data(cls) -> List[Self]:
@ -186,7 +183,7 @@ class WikiModel(Model):
Returns:
返回对应的 url
"""
return SCRAPE_HOST.join(f"{id_}/?lang=CHS")
return HONEY_HOST.join(f"{id_}/?lang=CHS")
@classmethod
async def _name_list_generator(cls, *, with_url: bool = False) -> AsyncIterator[Union[str, Tuple[str, URL]]]:
@ -210,7 +207,7 @@ class WikiModel(Model):
for data in json_data: # 遍历 json
data_name = re.findall(r'>(.*)<', data[1])[0] # 获取 Model 的名称
if with_url: # 如果需要返回对应的 url
data_url = SCRAPE_HOST.join(re.findall(r'\"(.*?)\"', data[0])[0])
data_url = HONEY_HOST.join(re.findall(r'\"(.*?)\"', data[0])[0])
await queue.put((data_name, data_url))
else:
await queue.put(data_name)

View File

@ -4,7 +4,7 @@ from typing import List, Optional
from bs4 import BeautifulSoup
from httpx import URL
from modules.wiki.base import Model, SCRAPE_HOST
from modules.wiki.base import Model, HONEY_HOST
from modules.wiki.base import WikiModel
from modules.wiki.other import Association, Element, WeaponType
@ -94,7 +94,7 @@ class Character(WikiModel):
@classmethod
def scrape_urls(cls) -> List[URL]:
return [SCRAPE_HOST.join("fam_chars/?lang=CHS")]
return [HONEY_HOST.join("fam_chars/?lang=CHS")]
@classmethod
async def _parse_soup(cls, soup: BeautifulSoup) -> 'Character':
@ -170,8 +170,8 @@ class Character(WikiModel):
@property
def icon(self) -> CharacterIcon:
return CharacterIcon(
icon=str(SCRAPE_HOST.join(f'/img/{self.id}_icon.webp')),
side=str(SCRAPE_HOST.join(f'/img/{self.id}_side_icon.webp')),
gacha=str(SCRAPE_HOST.join(f'/img/{self.id}_gacha_card.webp')),
splash=str(SCRAPE_HOST.join(f'/img/{self.id}_gacha_splash.webp'))
icon=str(HONEY_HOST.join(f'/img/{self.id}_icon.webp')),
side=str(HONEY_HOST.join(f'/img/{self.id}_side_icon.webp')),
gacha=str(HONEY_HOST.join(f'/img/{self.id}_gacha_card.webp')),
splash=str(HONEY_HOST.join(f'/img/{self.id}_gacha_splash.webp'))
)

View File

@ -4,7 +4,7 @@ from typing import List, Optional, Tuple, Union
from bs4 import BeautifulSoup
from httpx import URL
from modules.wiki.base import SCRAPE_HOST, WikiModel
from modules.wiki.base import HONEY_HOST, WikiModel
__all__ = ['Material']
@ -28,8 +28,8 @@ class Material(WikiModel):
@staticmethod
def scrape_urls() -> List[URL]:
weapon = [SCRAPE_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']]
talent = [SCRAPE_HOST.join(f'fam_talent_{i}/?lang=CHS') for i in ['book', 'boss', 'common', 'reward']]
weapon = [HONEY_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']]
talent = [HONEY_HOST.join(f'fam_talent_{i}/?lang=CHS') for i in ['book', 'boss', 'common', 'reward']]
return weapon + talent
@classmethod
@ -77,4 +77,4 @@ class Material(WikiModel):
@property
def icon(self) -> str:
return str(SCRAPE_HOST.join(f'/img/{self.id}.webp'))
return str(HONEY_HOST.join(f'/img/{self.id}.webp'))

View File

@ -1,85 +0,0 @@
{
"504": {
"city": "Mondstadt",
"name": "高塔孤王",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_504.png"
},
"524": {
"city": "Mondstadt",
"name": "凛风奔狼",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_524.png"
},
"544": {
"city": "Mondstadt",
"name": "狮牙斗士",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_544.png"
},
"514": {
"city": "Liyue",
"name": "孤云寒林",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"Key": "Weapon_Guyun",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_514.png"
},
"534": {
"city": "Liyue",
"name": "雾海云间",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_534.png"
},
"554": {
"city": "Liyue",
"name": "漆黑陨铁",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_554.png"
},
"564": {
"city": "Inazuma",
"name": "远海夷地",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"Key": "Weapon_DistantSea",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_564.png"
},
"574": {
"city": "Inazuma",
"name": "鸣神御灵",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_574.png"
},
"584": {
"city": "Inazuma",
"name": "今昔剧画",
"star": {
"value": 5,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/5star.png"
},
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/weapon/i_584.png"
}
}

View File

@ -1,92 +0,0 @@
{
"63": {
"name": "号角",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_Horn",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_63.png"
},
"73": {
"name": "地脉",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_LeyLine",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_73.png"
},
"83": {
"name": "混沌",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_Chaos",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_83.png"
},
"93": {
"name": "雾虚",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_MistGrass",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_93.png"
},
"103": {
"name": "祭刀",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_SacrificialKnife",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_103.png"
},
"143": {
"name": "骨片",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_BoneShard",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_143.png"
},
"153": {
"name": "刻像",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_Statuette",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_153.png"
},
"173": {
"name": "混沌2",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_Chaos2",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_173.png"
},
"176": {
"name": "隐兽",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_Concealed",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_176.png"
},
"183": {
"name": "棱镜",
"star": {
"value": 4,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/4star.png"
},
"key": "Elite_Prism",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_183.png"
}
}

View File

@ -1,83 +0,0 @@
{
"23": {
"name": "史莱姆",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"key": "Monster_Slime",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_23.png"
},
"33": {
"name": "面具",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Mask",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_33.png"
},
"43": {
"name": "绘卷",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Scroll",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_43.png"
},
"53": {
"name": "箭簇",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Scroll",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_43.png"
},
"113": {
"name": "徽记",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Insignia",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_113.png"
},
"123": {
"name": "鸦印",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_RavenInsignia",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_123.png"
},
"133": {
"name": "花蜜",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Nectar",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_133.png"
},
"163": {
"name": "刀镡",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Handguard",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_163.png"
},
"187": {
"name": "浮游",
"star": {
"value": 3,
"icon": "https://genshin.honeyhunterworld.com/img/back/item/3star.png"
},
"Key": "Monster_Spectral",
"icon": "https://genshin.honeyhunterworld.com/img/upgrade/material/i_187.png"
}
}

View File

@ -3,7 +3,7 @@ from typing import Optional
from typing_extensions import Self
from modules.wiki.base import SCRAPE_HOST
from modules.wiki.base import HONEY_HOST
__all__ = [
'Element',
@ -26,11 +26,11 @@ class Element(Enum):
_WEAPON_ICON_MAP = {
'Sword': SCRAPE_HOST.join('img/s_23101.png'),
'Claymore': SCRAPE_HOST.join('img/s_163101.png'),
'Polearm': SCRAPE_HOST.join('img/s_233101.png'),
'Catalyst': SCRAPE_HOST.join('img/s_43101.png'),
'Bow': SCRAPE_HOST.join('img/s_213101.png'),
'Sword': HONEY_HOST.join('img/s_23101.png'),
'Claymore': HONEY_HOST.join('img/s_163101.png'),
'Polearm': HONEY_HOST.join('img/s_233101.png'),
'Catalyst': HONEY_HOST.join('img/s_43101.png'),
'Bow': HONEY_HOST.join('img/s_213101.png'),
}

View File

@ -5,7 +5,7 @@ from typing import List, Optional, Tuple, Union
from bs4 import BeautifulSoup
from httpx import URL
from modules.wiki.base import Model, SCRAPE_HOST, WikiModel
from modules.wiki.base import Model, HONEY_HOST, WikiModel
from modules.wiki.other import AttributeType, WeaponType
__all__ = ['Weapon', 'WeaponAffix', 'WeaponAttribute']
@ -45,7 +45,7 @@ class Weapon(WikiModel):
"""武器
Attributes:
type: 武器类型
weapon_type: 武器类型
attack: 基础攻击力
attribute:
affix: 武器技能
@ -66,7 +66,7 @@ class Weapon(WikiModel):
@staticmethod
def scrape_urls() -> List[URL]:
return [SCRAPE_HOST.join(f"fam_{i.lower()}/?lang=CHS") for i in WeaponType.__members__]
return [HONEY_HOST.join(f"fam_{i.lower()}/?lang=CHS") for i in WeaponType.__members__]
@classmethod
async def _parse_soup(cls, soup: BeautifulSoup) -> 'Weapon':
@ -99,10 +99,7 @@ class Weapon(WikiModel):
affix = WeaponAffix(name=get_table_text(7), description=[
i.find_all('td')[1].text for i in tables[3].find_all('tr')[1:]
])
if len(tables) < 11:
description = get_table_text(-1)
else:
description = get_table_text(9)
description = get_table_text(-1) if len(tables) < 11 else get_table_text(9)
if story_table := find_table('quotes'):
story = story_table[0].text.strip()
else:
@ -137,7 +134,7 @@ class Weapon(WikiModel):
@property
def icon(self) -> WeaponIcon:
return WeaponIcon(
icon=str(SCRAPE_HOST.join(f'/img/{self.id}.webp')),
awakened=str(SCRAPE_HOST.join(f'/img/{self.id}_awaken_icon.webp')),
gacha=str(SCRAPE_HOST.join(f'/img/{self.id}_gacha_icon.webp')),
icon=str(HONEY_HOST.join(f'/img/{self.id}.webp')),
awakened=str(HONEY_HOST.join(f'/img/{self.id}_awaken_icon.webp')),
gacha=str(HONEY_HOST.join(f'/img/{self.id}_gacha_icon.webp')),
)

View File

@ -5,6 +5,7 @@ from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters
from core.base.assets import AssetsService
from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.cookies.services import CookiesService
@ -14,7 +15,7 @@ from core.user import UserService
from core.user.error import UserNotFoundError
from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts
from utils.helpers import get_genshin_client, url_to_file, get_public_genshin_client
from utils.helpers import get_genshin_client, get_public_genshin_client
from utils.log import logger
@ -31,11 +32,17 @@ class NoMostKills(Exception):
class Abyss(Plugin, BasePlugin):
"""深渊数据查询"""
def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None,
template_service: TemplateService = None):
def __init__(
self,
user_service: UserService = None,
cookies_service: CookiesService = None,
template_service: TemplateService = None,
assets_service: AssetsService = None
):
self.template_service = template_service
self.cookies_service = cookies_service
self.user_service = user_service
self.assets_service = assets_service
@staticmethod
def _get_role_star_bg(value: int):
@ -65,23 +72,23 @@ class Abyss(Plugin, BasePlugin):
"total_stars": spiral_abyss_info.total_stars,
"most_played_list": [],
"most_kills": {
"icon": await url_to_file(ranks.most_kills[0].side_icon),
"icon": await self.assets_service.avatar(ranks.most_kills[0].id).side(),
"value": ranks.most_kills[0].value,
},
"strongest_strike": {
"icon": await url_to_file(ranks.strongest_strike[0].side_icon),
"icon": await self.assets_service.avatar(ranks.strongest_strike[0].id).side(),
"value": ranks.strongest_strike[0].value
},
"most_damage_taken": {
"icon": await url_to_file(ranks.most_damage_taken[0].side_icon),
"icon": await self.assets_service.avatar(ranks.most_damage_taken[0].id).side(),
"value": ranks.most_damage_taken[0].value
},
"most_bursts_used": {
"icon": await url_to_file(ranks.most_bursts_used[0].side_icon),
"icon": await self.assets_service.avatar(ranks.most_bursts_used[0].id).side(),
"value": ranks.most_bursts_used[0].value
},
"most_skills_used": {
"icon": await url_to_file(ranks.most_skills_used[0].side_icon),
"icon": await self.assets_service.avatar(ranks.most_skills_used[0].id).side(),
"value": ranks.most_skills_used[0].value
}
}
@ -89,7 +96,7 @@ class Abyss(Plugin, BasePlugin):
most_played_list = ranks.most_played
for most_played in most_played_list:
temp = {
"icon": await url_to_file(most_played.icon),
"icon": await self.assets_service.avatar(most_played.id).icon(),
"value": most_played.value,
"background": self._get_role_star_bg(most_played.rarity)
}

View File

@ -2,7 +2,7 @@ from telegram import Update, User
from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters
from core.assets import AssetsService
from core.base.assets import AssetsService
from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.plugin import Plugin, handler
@ -67,7 +67,7 @@ class AbyssTeam(Plugin, BasePlugin):
}
for i in team_data.rateListUp[0].formation:
temp = {
"icon": (await self.assets_service.character(roleToId(i.name)).icon()).as_uri(),
"icon": (await self.assets_service.avatar(roleToId(i.name)).icon()).as_uri(),
"name": i.name,
"background": self._get_role_star_bg(i.star),
"hava": i.name in user_data,
@ -75,7 +75,7 @@ class AbyssTeam(Plugin, BasePlugin):
abyss_team_data["up"].append(temp)
for i in team_data.rateListDown[0].formation:
temp = {
"icon": (await self.assets_service.character(roleToId(i.name)).icon()).as_uri(),
"icon": (await self.assets_service.avatar(roleToId(i.name)).icon()).as_uri(),
"name": i.name,
"background": self._get_role_star_bg(i.star),
"hava": i.name in user_data,

View File

@ -1,8 +1,10 @@
import asyncio
import contextlib
import re
from asyncio import Lock
from ctypes import c_double
from datetime import datetime
from functools import partial
from multiprocessing import Value
from pathlib import Path
from ssl import SSLZeroReturnError
@ -20,13 +22,13 @@ from telegram.constants import ChatAction, ParseMode
from telegram.error import RetryAfter, TimedOut
from telegram.ext import CallbackContext
from core.assets import AssetsService
from core.base.assets import AssetsService, AssetsServiceType
from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.plugin import Plugin, handler
from core.template import TemplateService
from core.user.error import UserNotFoundError
from metadata.honey import HONEY_ID_MAP, HONEY_ROLE_NAME_MAP
from metadata.genshin import AVATAR_DATA, HONEY_DATA
from utils.bot import get_all_args
from utils.decorators.admins import bot_admins_rights_check
from utils.decorators.error import error_callable
@ -34,6 +36,8 @@ from utils.decorators.restricts import restricts
from utils.helpers import get_genshin_client
from utils.log import logger
INTERVAL = 1
DATA_TYPE = Dict[str, List[List[str]]]
DATA_FILE_PATH = Path(__file__).joinpath('../daily.json').resolve()
DOMAINS = ['忘却之峡', '太山府', '菫色之庭', '昏识塔', '塞西莉亚苗圃', '震雷连山密宫', '砂流之庭', '有顶塔']
@ -118,30 +122,32 @@ class DailyMaterial(Plugin, BasePlugin):
async def _get_data_from_user(self, user: User) -> Tuple[Optional[Client], Dict[str, List[Any]]]:
"""获取已经绑定的账号的角色、武器信息"""
client = None
user_data = {'character': [], 'weapon': []}
user_data = {'avatar': [], 'weapon': []}
try:
logger.debug("尝试获取已绑定的原神账号")
client = await get_genshin_client(user.id)
logger.debug(f"获取成功, UID: {client.uid}")
logger.debug(f"获取账号数据成功: UID={client.uid}")
characters = await client.get_genshin_characters(client.uid)
for character in characters:
cid = HONEY_ROLE_NAME_MAP[character.id][0]
if character.name == '旅行者': # 跳过主角
continue
cid = AVATAR_DATA[str(character.id)]['id']
weapon = character.weapon
user_data['character'].append(
user_data['avatar'].append(
ItemData(
id=cid, name=character.name, rarity=character.rarity, level=character.level,
constellation=character.constellation,
icon=(await self.assets_service.character(cid).icon()).as_uri()
icon=(await self.assets_service.avatar(cid).icon()).as_uri()
)
)
user_data['weapon'].append(
ItemData(
id=(wid := f"i_n{weapon.id}"), name=weapon.name, level=weapon.level, rarity=weapon.rarity,
id=str(weapon.id), name=weapon.name, level=weapon.level, rarity=weapon.rarity,
refinement=weapon.refinement,
icon=(await getattr( # 判定武器的突破次数是否大于 2 ;若是, 则将图标替换为 awakened (觉醒) 的图标
self.assets_service.weapon(wid), 'icon' if weapon.ascension < 2 else 'awakened'
self.assets_service.weapon(weapon.id), 'icon' if weapon.ascension < 2 else 'awaken'
)()).as_uri(),
c_path=(await self.assets_service.character(cid).side()).as_uri()
c_path=(await self.assets_service.avatar(cid).side()).as_uri()
)
)
except (UserNotFoundError, CookiesNotFoundError):
@ -190,13 +196,13 @@ class DailyMaterial(Plugin, BasePlugin):
await update.message.reply_chat_action(ChatAction.TYPING)
# 获取已经缓存的秘境素材信息
local_data = {'character': [], 'weapon': []}
local_data = {'avatar': [], 'weapon': []}
if not self.data: # 若没有缓存每日素材表的数据
logger.info("正在获取每日素材缓存")
await self._refresh_data()
self.data = await self._refresh_data()
for domain, sche in self.data.items():
area = DOMAIN_AREA_MAP[domain] # 获取秘境所在的区域
type_ = 'character' if DOMAINS.index(domain) < 4 else 'weapon' # 获取秘境的培养素材的类型:是天赋书还是武器突破材料
type_ = 'avatar' if DOMAINS.index(domain) < 4 else 'weapon' # 获取秘境的培养素材的类型:是天赋书还是武器突破材料
# 将读取到的数据存入 local_data 中
local_data[type_].append({'name': area, 'materials': sche[weekday][0], 'items': sche[weekday][1]})
@ -205,36 +211,36 @@ class DailyMaterial(Plugin, BasePlugin):
await update.message.reply_chat_action(ChatAction.TYPING)
render_data = RenderData(title=title, time=time, uid=client.uid if client else client)
for type_ in ['character', 'weapon']:
for type_ in ['avatar', 'weapon']:
areas = []
for area_data in local_data[type_]: # 遍历每个区域的信息:蒙德、璃月、稻妻、须弥
items = []
for id_ in area_data['items']: # 遍历所有该区域下当天weekday可以培养的角色、武器
added = False
for i in user_data[type_]: # 从已经获取的角色数据中查找对应角色、武器
if id_ == i.id:
if id_ == str(i.id):
if i.rarity > 3: # 跳过 3 星及以下的武器
items.append(i)
added = True
if added:
continue
item = HONEY_ID_MAP[type_][id_]
if item[1] < 4: # 跳过 3 星及以下的武器
item = HONEY_DATA[type_][id_]
if item[2] < 4: # 跳过 3 星及以下的武器
continue
items.append(ItemData( # 添加角色数据中未找到的
id=id_, name=item[0], rarity=item[1],
icon=(await getattr(self.assets_service, f'{type_}')(id_).icon()).as_uri()
id=id_, name=item[1], rarity=item[2],
icon=(await getattr(self.assets_service, type_)(id_).icon()).as_uri()
))
materials = []
for mid in area_data['materials']: # 添加这个区域当天weekday的培养素材
path = (await self.assets_service.material(mid).icon()).as_uri()
material = HONEY_ID_MAP['material'][mid]
materials.append(ItemData(id=mid, icon=path, name=material[0], rarity=material[1]))
material = HONEY_DATA['material'][mid]
materials.append(ItemData(id=mid, icon=path, name=material[1], rarity=material[2]))
areas.append(AreaData(
name=area_data['name'], materials=materials, items=sort_item(items),
material_name=get_material_serial_name(map(lambda x: x.name, materials))
))
setattr(render_data, type_, areas)
setattr(render_data, {'avatar': 'character'}.get(type_, type_), areas)
await update.message.reply_chat_action(ChatAction.TYPING)
render_tasks = [
@ -270,7 +276,12 @@ class DailyMaterial(Plugin, BasePlugin):
@handler.command('refresh_daily_material', block=False)
@bot_admins_rights_check
async def refresh(self, update: Update, context: CallbackContext):
user = update.effective_user
message = update.effective_message
logger.info(
f"用户 {user.full_name}[{user.id}] 刷新[bold]每日素材[/]缓存命令", extra={'markup': True}
)
if self.locks[0].locked():
notice = await message.reply_text("派蒙还在抄每日素材表呢,我有在好好工作哦~")
self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10)
@ -290,12 +301,19 @@ class DailyMaterial(Plugin, BasePlugin):
parse_mode=ParseMode.HTML
)
self.data = data or self.data
await self._download_icon(notice)
notice = await notice.edit_text(
notice.text_html.split('\n')[0] + "\n每日素材图标搬运<b>完成!</b>",
time = await self._download_icon(notice)
async def job(_, n):
await n.edit_text(
n.text_html.split('\n')[0] + "\n每日素材图标搬运<b>完成!</b>",
parse_mode=ParseMode.HTML
)
self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10)
await asyncio.sleep(INTERVAL)
await notice.delete()
context.application.job_queue.run_once(
partial(job, n=notice), when=time + INTERVAL, name='notice_msg_final_job'
)
async def _refresh_data(self, retry: int = 5) -> DATA_TYPE:
"""刷新来自 honey impact 的每日素材表"""
@ -313,11 +331,20 @@ class DailyMaterial(Plugin, BasePlugin):
key = tag.find('a').text
result[key] = [[[], []] for _ in range(7)]
for day, div in enumerate(tag.find_all('div')):
result[key][day][0] = [re.findall(r"/(.*)?/", a['href'])[0] for a in div.find_all('a')]
result[key][day][0] = []
for a in div.find_all('a'):
honey_id = re.findall(r"/(.*)?/", a['href'])[0]
mid: str = [
i[0]
for i in HONEY_DATA['material'].items()
if i[1][0] == honey_id
][0]
result[key][day][0].append(mid)
else: # 如果是角色或武器
id_ = re.findall(r"/(.*)?/", tag['href'])[0]
if tag.text.strip() == '旅行者': # 忽略主角
continue
id_ = ("" if id_.startswith('i_n') else "10000") + re.findall(r'\d+', id_)[0]
for day in map(int, tag.find('div')['data-days']): # 获取该角色/武器的可培养天
result[key][day][1].append(id_)
for stage, schedules in result.items():
@ -339,51 +366,53 @@ class DailyMaterial(Plugin, BasePlugin):
# noinspection PyTypeChecker
return result
async def _download_icon(self, message: Optional[Message] = None):
async def _download_icon(self, message: Optional[Message] = None) -> float:
"""下载素材图标"""
asset_list = []
from time import time as time_
lock = asyncio.Lock()
interval = 2
the_time = Value(c_double, time_() - interval)
async def task(_id, _item, _type):
logger.debug(f"正在开始下载 \"{_item[0]}\" 的图标素材")
async with lock:
if message is not None and time_() >= the_time.value + interval: # 判定现在是否距离上次修改消息已经有了足够的时间
text = '\n'.join(message.text_html.split('\n')[:2]) + f"\n正在搬运 <b>{_item[0]}</b> 的图标素材。。。"
try:
await message.edit_text(text, parse_mode=ParseMode.HTML)
the_time.value = time_()
except (TimedOut, RetryAfter): # 修改消息失败
pass
asset = getattr(self.assets_service, _type)(_id) # 获取素材对象
icon_types = list(filter( # 找到该素材对象的所有图标类型
lambda x: not x.startswith('_') and x not in ['path'] and callable(getattr(asset, x)),
dir(asset)
))
icon_coroutines = map(lambda x: getattr(asset, x), icon_types) # 根据图标类型找到下载对应图标的函数
for coroutine in icon_coroutines:
await coroutine() # 执行下载函数
logger.debug(f"\"{_item[0]}\" 的图标素材下载成功")
async with lock:
if message is not None and time_() >= the_time.value + interval:
text = (
'\n'.join(message.text_html.split('\n')[:2]) +
f"\n正在搬运 <b>{_item[0]}</b> 的图标素材。。。<b>成功!</b>"
)
try:
await message.edit_text(text, parse_mode=ParseMode.HTML)
the_time.value = time_()
except (TimedOut, RetryAfter):
pass
the_time = Value(c_double, time_() - INTERVAL)
for type_, items in HONEY_ID_MAP.items(): # 遍历每个对象
async def edit_message(text):
"""修改提示消息"""
async with lock:
if (
message is not None
and
time_() >= (the_time.value + INTERVAL)
):
with contextlib.suppress(TimedOut, RetryAfter):
await message.edit_text(
'\n'.join(message.text_html.split('\n')[:2] + [text]),
parse_mode=ParseMode.HTML
)
the_time.value = time_()
async def task(item_id, name, item_type):
logger.debug(f"正在开始下载 \"{name}\" 的图标素材")
await edit_message(f"正在搬运 <b>{name}</b> 的图标素材。。。")
asset: AssetsServiceType = getattr(self.assets_service, item_type)(item_id) # 获取素材对象
asset_list.append(asset.honey_id)
# 找到该素材对象的所有图标类型
# 并根据图标类型找到下载对应图标的函数
for icon_type in asset.icon_types:
await getattr(asset, icon_type)(True) # 执行下载函数
logger.debug(f"\"{name}\" 的图标素材下载成功")
await edit_message(f"正在搬运 <b>{name}</b> 的图标素材。。。<b>成功!</b>")
for TYPE, ITEMS in HONEY_DATA.items(): # 遍历每个对象
task_list = []
for id_, item in items.items():
task_list.append(asyncio.create_task(task(id_, item, type_)))
new_items = []
for ID, DATA in ITEMS.items():
if (ITEM := [ID, DATA[1], TYPE]) not in new_items:
new_items.append(ITEM)
task_list.append(asyncio.create_task(task(*ITEM)))
await asyncio.gather(*task_list) # 等待所有任务执行完成
logger.info("图标素材下载完成")
return the_time.value
class ItemData(BaseModel):

View File

@ -0,0 +1,29 @@
from telegram import Update
from core.plugin import Plugin, handler
from metadata.scripts.honey import update_honey_metadata
from metadata.scripts.metadatas import update_metadata_from_ambr, update_metadata_from_github
from utils.decorators.admins import bot_admins_rights_check
from utils.log import logger
class MetadataPlugin(Plugin):
@handler.command('refresh_metadata')
@bot_admins_rights_check
async def refresh(self, update: Update, _) -> None:
user = update.effective_user
message = update.effective_message
logger.info(
f"用户 {user.full_name}[{user.id}] 刷新[bold]metadata[/]缓存命令", extra={'markup': True}
)
msg = await message.reply_text("正在刷新元数据,请耐心等待...")
logger.info("正在从 github 上获取元数据")
await update_metadata_from_github()
logger.info("正在从 ambr 上获取元数据")
await update_metadata_from_ambr()
logger.info("正在从 honey 上获取元数据")
await update_honey_metadata()
await msg.edit_text("正在刷新元数据,请耐心等待...\n完成!")

View File

@ -2,11 +2,12 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters
from core.assets import AssetsService
from core.base.assets import AssetsService
from core.baseplugin import BasePlugin
from core.plugin import Plugin, handler
from core.template import TemplateService
from core.wiki.services import WikiService
from metadata.genshin import honey_id_to_game_id
from metadata.shortname import weaponToName
from modules.wiki.weapon import Weapon
from utils.bot import get_all_args
@ -27,11 +28,11 @@ class WeaponPlugin(Plugin, BasePlugin):
self,
template_service: TemplateService = None,
wiki_service: WikiService = None,
assert_service: AssetsService = None
assets_service: AssetsService = None
):
self.wiki_service = wiki_service
self.template_service = template_service
self.assert_service = assert_service
self.assets_service = assets_service
@handler(CommandHandler, command="weapon", block=False)
@handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False)
@ -78,11 +79,13 @@ class WeaponPlugin(Plugin, BasePlugin):
"weapon_info_type_img": await url_to_file(_weapon_data.weapon_type.icon_url()),
"progression_secondary_stat_value": bonus,
"progression_secondary_stat_name": _weapon_data.attribute.type.value,
"weapon_info_source_img": (await self.assert_service.weapon(_weapon_data.id).icon()).as_uri(),
"weapon_info_source_img": (
await self.assets_service.weapon(honey_id_to_game_id(_weapon_data.id, 'weapon')).icon()
).as_uri(),
"weapon_info_max_level": _weapon_data.stats[-1].level,
"progression_base_atk": round(_weapon_data.stats[-1].ATK),
"weapon_info_source_list": [
(await self.assert_service.material(mid).icon()).as_uri()
(await self.assets_service.material(honey_id_to_game_id(mid, 'material')).icon()).as_uri()
for mid in _weapon_data.ascension[-3:]
],
"special_ability_name": _weapon_data.affix.name,
@ -94,11 +97,13 @@ class WeaponPlugin(Plugin, BasePlugin):
"weapon_info_type_img": await url_to_file(_weapon_data.weapon_type.icon_url()),
"progression_secondary_stat_value": ' ',
"progression_secondary_stat_name": '无其它属性加成',
"weapon_info_source_img": (await self.assert_service.weapon(_weapon_data.id).icon()).as_uri(),
"weapon_info_source_img": (
await self.assets_service.weapon(honey_id_to_game_id(_weapon_data.id, 'weapon')).icon()
).as_uri(),
"weapon_info_max_level": _weapon_data.stats[-1].level,
"progression_base_atk": round(_weapon_data.stats[-1].ATK),
"weapon_info_source_list": [
(await self.assert_service.material(mid).icon()).as_uri()
(await self.assets_service.material(honey_id_to_game_id(mid, 'material')).icon()).as_uri()
for mid in _weapon_data.ascension[-3:]
],
"special_ability_name": '',

View File

@ -19,30 +19,45 @@
}
.character-side-icon {
width: 32px;
position: absolute;
right: 0;
bottom: 1px;
width: 48px;
height: 48px;
}
div:has(> img.character-side-icon) {
position: relative;
height: 32px;
}
div:has(> img.character-side-icon) > div {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
</style>
</head>
<body>
<div class="mx-auto flex flex-col h-full bg-no-repeat bg-cover" id="container">
<div class="title text-2xl my-4 text-yellow-500 mx-auto">深境螺旋</div>
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-white bg-opacity-10">
<div class="uid text-center mx-auto">UID {{uid}}</div>
<div class="text-center mx-auto">最深抵达 {{max_floor}}</div>
<div class="text-center mx-auto">战斗次数 {{total_battles}}</div>
<div class="text-center mx-auto">获得星级 {{total_stars}}</div>
<div class="uid text-center mx-auto">UID {{ uid }}</div>
<div class="text-center mx-auto">最深抵达 {{ max_floor }}</div>
<div class="text-center mx-auto">战斗次数 {{ total_battles }}</div>
<div class="text-center mx-auto">获得星级 {{ total_stars }}</div>
</div>
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
<div class="text-center mr-auto text-yellow-500">出战次数</div>
<div class="mx-auto flex my-2">
{% for most_played in most_played_list %}
<div class="rounded-lg mx-2 overflow-hidden" style="background-color: rgb(233, 229, 220)">
<div class="character-icon rounded-br-3xl bg-cover overflow-hidden"
style="background: url({{most_played.background}});background-size: cover;">
<img src="{{most_played.icon}}" alt=""></div>
<div class="text-center">{{most_played.value}}次</div>
</div>
<div class="rounded-lg mx-2 overflow-hidden" style="background-color: rgb(233, 229, 220)">
<div class="character-icon rounded-br-3xl bg-cover overflow-hidden"
style="background: url({{ most_played.background }});background-size: cover;">
<img src="{{ most_played.icon }}" alt=""></div>
<div class="text-center">{{ most_played.value }}次</div>
</div>
{% endfor %}
</div>
</div>
@ -53,28 +68,28 @@
<div>
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">最多击破数:{{most_kills.value}}</div>
<img class="character-side-icon ml-auto" src="{{most_kills.icon}}" alt="">
<div class="my-auto">最多击破数:{{ most_kills.value }}</div>
<img class="character-side-icon ml-auto" src="{{ most_kills.icon }}" alt="">
</div>
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">最强一击:{{strongest_strike.value}}</div>
<img class="character-side-icon ml-auto" src="{{strongest_strike.icon}}" alt="">
<div class="my-auto">最强一击:{{ strongest_strike.value }}</div>
<img class="character-side-icon ml-auto" src="{{ strongest_strike.icon }}" alt="">
</div>
</div>
<div class="base-info flex flex-row px-20 py-1 my-1 text-white">
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">承受最多伤害:{{most_damage_taken.value}}</div>
<img class="character-side-icon ml-auto" src="{{most_damage_taken.icon}}" alt="">
<div class="my-auto">承受最多伤害:{{ most_damage_taken.value }}</div>
<img class="character-side-icon ml-auto" src="{{ most_damage_taken.icon }}" alt="">
</div>
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">元素爆发数:{{most_bursts_used.value}}</div>
<img class="character-side-icon ml-auto" src="{{most_bursts_used.icon}}" alt="">
<div class="my-auto">元素爆发数:{{ most_bursts_used.value }}</div>
<img class="character-side-icon ml-auto" src="{{ most_bursts_used.icon }}" alt="">
</div>
</div>
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">元素战技释放次数:{{most_skills_used.value}}</div>
<img class="character-side-icon ml-auto" src="{{most_skills_used.icon}}" alt="">
<div class="my-auto">元素战技释放次数:{{ most_skills_used.value }}</div>
<img class="character-side-icon ml-auto" src="{{ most_skills_used.icon }}" alt="">
</div>
<div class="text-center flex flex-row flex-1 mr-6"></div>
</div>

View File

@ -18,10 +18,24 @@
}
.character-side-icon {
width: 32px;
position: absolute;
right: 0;
bottom: 1px;
width: 48px;
height: 48px;
}
div:has(> img.character-side-icon) {
position: relative;
height: 32px;
}
div:has(> img.character-side-icon) > div {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
</style>
</head>
<body>
@ -70,7 +84,7 @@
<div class="base-info flex flex-row px-20 py-1 my-1 text-white bg-black bg-opacity-10 ">
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">最多击破数21</div>
<img class="character-side-icon ml-auto" src="./../../img/example/256x256.png" alt="">
<img class="character-side-icon" src="./../../img/example/256x256.png" alt="">
</div>
<div class="text-center flex flex-row flex-1 mr-6">
<div class="my-auto">最强一击21</div>

View File

@ -28,7 +28,7 @@
<span class="materials">
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/2.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -37,7 +37,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/3.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -47,7 +47,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/4.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -58,7 +58,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/5.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -75,17 +75,17 @@
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: rgba(255, 20, 147, 0.8); backdrop-filter: blur(3px);">6命</div>
<img src="../../assets/character/itto_057/icon.webp" alt="一斗"/>
<img src="../../assets/avatar/10000058/icon.png" alt="八重神子"/>
</div>
<div class="item-name">
<div>一斗</div>
<div>八重神子</div>
</div>
</div>
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: rgba(255, 20, 147, 0.8); backdrop-filter: blur(3px);">6命</div>
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -95,7 +95,7 @@
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: rgba(255, 20, 147, 0.8); backdrop-filter: blur(3px);">6命</div>
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -104,7 +104,7 @@
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -112,7 +112,7 @@
</div>
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -120,7 +120,7 @@
</div>
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -140,7 +140,7 @@
<span class="materials">
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/2.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -149,7 +149,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/3.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -159,7 +159,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/4.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -170,7 +170,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/5.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -187,7 +187,7 @@
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: rgba(255, 20, 147, 0.8); backdrop-filter: blur(3px);">6命</div>
<img src="../../assets/character/itto_057/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000058/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -197,7 +197,7 @@
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: rgba(255, 20, 147, 0.8); backdrop-filter: blur(3px);">6命</div>
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -207,7 +207,7 @@
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: rgba(255, 20, 147, 0.8); backdrop-filter: blur(3px);">6命</div>
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -216,7 +216,7 @@
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -224,7 +224,7 @@
</div>
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -232,7 +232,7 @@
</div>
<div class="item character">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<img src="../../assets/character/ayaka_002/icon.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/icon.png" alt="神里绫华"/>
</div>
<div class="item-name">
<div>神里绫华</div>
@ -256,7 +256,7 @@
<span class="materials">
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/2.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -265,7 +265,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/3.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -275,7 +275,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/4.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -286,7 +286,7 @@
</div>
<div class="material">
<div class="material-icon" style="background-image: url(./bg/rarity/half/5.png)">
<img alt="" src="../../assets/material/i_21/icon.webp"/>
<img alt="" src="../../assets/material/114003/icon.png"/>
</div>
<div class="material-star">
<img alt="star" src="./bg/rarity/star.webp"/>
@ -301,12 +301,12 @@
<div class="area-content">
<div class="item weapon item-not-owned">
<div class="role">
<img src="../../assets/character/shenhe_063/side.webp" alt="申鹤"/>
<img src="../../assets/avatar/10000063/side.png" alt="申鹤"/>
</div>
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: deepskyblue">精炼5</div>
<img src="../../assets/weapon/i_n14501/awakened.webp" alt="天空之刃"/>
<img src="../../assets/weapon/14501/awaken.png" alt="天空之刃"/>
</div>
<div class="item-name">
<div>天空之刃</div>
@ -314,12 +314,12 @@
</div>
<div class="item weapon">
<div class="role">
<img src="../../assets/character/mona_041/side.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000041/side.png" alt="神里绫华"/>
</div>
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: deepskyblue">精炼5</div>
<img src="../../assets/weapon/i_n11502/awakened.webp" alt="天空之刃"/>
<img src="../../assets/weapon/14502/awaken.png" alt="天空之刃"/>
</div>
<div class="item-name">
<div>天空之刃</div>
@ -327,12 +327,12 @@
</div>
<div class="item weapon">
<div class="role">
<img src="../../assets/character/collei_067/side.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000067/side.png" alt="神里绫华"/>
</div>
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: deepskyblue">精炼5</div>
<img src="../../assets/weapon/i_n11502/awakened.webp" alt="天空之刃"/>
<img src="../../assets/weapon/14502/awaken.png" alt="天空之刃"/>
</div>
<div class="item-name">
<div>天空之刃</div>
@ -340,12 +340,12 @@
</div>
<div class="item weapon">
<div class="role">
<img src="../../assets/character/ayaka_002/side.webp" alt="神里绫华"/>
<img src="../../assets/avatar/10000002/side.png" alt="神里绫华"/>
</div>
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<div>Lv.90</div>
<div style="background-color: deepskyblue">精炼5</div>
<img src="../../assets/weapon/i_n11502/awakened.webp" alt="天空之刃"/>
<img src="../../assets/weapon/14502/awaken.png" alt="天空之刃"/>
</div>
<div class="item-name">
<div>天空之刃</div>
@ -353,7 +353,7 @@
</div>
<div class="item weapon">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<img src="../../assets/weapon/i_n11502/awakened.webp" alt="天空之刃"/>
<img src="../../assets/weapon/14502/awaken.png" alt="天空之刃"/>
</div>
<div class="item-name">
<div>天空之刃</div>
@ -361,7 +361,7 @@
</div>
<div class="item weapon">
<div class="item-icon" style="background-image: url(./bg/rarity/full/5.png)">
<img src="../../assets/weapon/i_n11502/awakened.webp" alt="天空之刃"/>
<img src="../../assets/weapon/14502/awaken.png" alt="天空之刃"/>
</div>
<div class="item-name">
<div>天空之刃</div>

View File

@ -180,7 +180,7 @@ body {
}
.material-icon > img {
height: calc(100% - 10px);
height: 100%;
}
.material-star {
@ -211,21 +211,23 @@ body {
box-shadow: 3px 3px 10px var(--shadow);
margin: 10px 12px;
justify-content: center;
position: relative;
}
.item-not-owned:before {
.item-not-owned::after {
content: "";
position: absolute;
width: 150px;
height: 187px;
top: 0;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
z-index: 2;
z-index: 1;
border-radius: 10px;
}
.item-icon {
width: inherit;
height: 152px;
height: 150px;
background-size: 104% auto;
background-position: center;
overflow: hidden;
@ -313,9 +315,9 @@ body {
}
.weapon > .role > img {
height: 50px;
height: 58px;
position: absolute;
left: 58%;
left: 20px;
transform: translateX(-50%);
bottom: 0;
}

View File

@ -1,9 +1,12 @@
"""常量"""
from pathlib import Path
from httpx import URL
__all__ = [
'PROJECT_ROOT', 'PLUGIN_DIR', 'RESOURCE_DIR',
'NOT_SET',
'HONEY_HOST', 'ENKA_HOST', 'AMBR_HOST', 'CELESTIA_HOST',
]
# 项目根目录
@ -14,3 +17,8 @@ PLUGIN_DIR = PROJECT_ROOT / 'plugins'
RESOURCE_DIR = PROJECT_ROOT / 'resources'
NOT_SET = object()
HONEY_HOST = URL("https://genshin.honeyhunterworld.com/")
ENKA_HOST = URL("https://enka.network/")
AMBR_HOST = URL("https://api.ambr.top/")
CELESTIA_HOST = URL("https://www.projectcelestia.com/")

View File

@ -1,13 +1,14 @@
import hashlib
import os
from pathlib import Path
from typing import Optional, Tuple, Union, cast
from typing import Optional, Tuple, TypeVar, Union, cast
import aiofiles
import genshin
import httpx
from genshin import Client, types
from httpx import UnsupportedProtocol
from typing_extensions import ParamSpec
from core.bot import bot
from core.cookies.services import CookiesService, PublicCookiesService
@ -17,6 +18,9 @@ from utils.error import UrlResourcesNotFoundError
from utils.log import logger
from utils.models.base import RegionEnum
T = TypeVar('T')
P = ParamSpec('P')
USER_AGENT: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \
"Chrome/90.0.4430.72 Safari/537.36"
REQUEST_HEADERS: dict = {'User-Agent': USER_AGENT}

View File

@ -5,16 +5,19 @@ from typing import Any, Dict, Optional, Tuple, Type, Union
from httpx import URL
__all__ = [
'StrOrPath', 'StrOrURL',
'StrOrPath', 'StrOrURL', 'StrOrInt',
'SysExcInfoType', 'ExceptionInfoType',
'JSONDict',
'JSONDict', 'JSONType'
]
StrOrPath = Union[str, Path]
StrOrURL = Union[str, URL]
StrOrInt = Union[str, int]
SysExcInfoType = Union[
Tuple[Type[BaseException], BaseException, Optional[TracebackType]],
Tuple[None, None, None]
]
ExceptionInfoType = Union[bool, SysExcInfoType, BaseException]
JSONDict = Dict[str, Any]
JSONType = Union[JSONDict, list]