From 4c702515a06906e815de747564c6bf7665030b23 Mon Sep 17 00:00:00 2001 From: Karako <70872201+karakoo@users.noreply.github.com> Date: Fri, 7 Oct 2022 13:02:49 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Enhance=20AssetsService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xtaodada --- core/assets/__init__.py | 8 - core/assets/service.py | 174 ------ core/base/assets.py | 513 ++++++++++++++++++ metadata/genshin.py | 42 ++ metadata/honey.py | 464 ---------------- metadata/scripts/honey.py | 188 +++++++ metadata/scripts/metadatas.py | 65 +++ metadata/shortname.py | 19 +- modules/apihelper/abyss_team.py | 4 +- modules/wiki/base.py | 13 +- modules/wiki/character.py | 12 +- modules/wiki/material.py | 8 +- modules/wiki/metadata/ascension.json | 85 --- modules/wiki/metadata/elite.json | 92 ---- modules/wiki/metadata/monster.json | 83 --- modules/wiki/other.py | 12 +- modules/wiki/weapon.py | 17 +- plugins/genshin/abyss.py | 25 +- plugins/genshin/abyss_team.py | 6 +- plugins/genshin/daily/material.py | 155 +++--- plugins/genshin/refresh_metadata.py | 29 + plugins/genshin/weapon.py | 19 +- resources/genshin/abyss/abyss.html | 57 +- resources/genshin/abyss/example.html | 18 +- resources/genshin/daily_material/example.html | 70 +-- resources/genshin/daily_material/style.css | 20 +- utils/const.py | 8 + utils/helpers.py | 6 +- utils/typedefs.py | 7 +- 29 files changed, 1120 insertions(+), 1099 deletions(-) delete mode 100644 core/assets/__init__.py delete mode 100644 core/assets/service.py create mode 100644 core/base/assets.py create mode 100644 metadata/genshin.py delete mode 100644 metadata/honey.py create mode 100644 metadata/scripts/honey.py create mode 100644 metadata/scripts/metadatas.py delete mode 100644 modules/wiki/metadata/ascension.json delete mode 100644 modules/wiki/metadata/elite.json delete mode 100644 modules/wiki/metadata/monster.json create mode 100644 plugins/genshin/refresh_metadata.py diff --git a/core/assets/__init__.py b/core/assets/__init__.py deleted file mode 100644 index 41e91f1..0000000 --- a/core/assets/__init__.py +++ /dev/null @@ -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 diff --git a/core/assets/service.py b/core/assets/service.py deleted file mode 100644 index ee4f9d9..0000000 --- a/core/assets/service.py +++ /dev/null @@ -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) diff --git a/core/base/assets.py b/core/base/assets.py new file mode 100644 index 0000000..b137ea6 --- /dev/null +++ b/core/base/assets.py @@ -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) diff --git a/metadata/genshin.py b/metadata/genshin.py new file mode 100644 index 0000000..52fed83 --- /dev/null +++ b/metadata/genshin.py @@ -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) diff --git a/metadata/honey.py b/metadata/honey.py deleted file mode 100644 index 3608f28..0000000 --- a/metadata/honey.py +++ /dev/null @@ -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'] -} diff --git a/metadata/scripts/honey.py b/metadata/scripts/honey.py new file mode 100644 index 0000000..9560aba --- /dev/null +++ b/metadata/scripts/honey.py @@ -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 diff --git a/metadata/scripts/metadatas.py b/metadata/scripts/metadatas.py new file mode 100644 index 0000000..cdea1ce --- /dev/null +++ b/metadata/scripts/metadatas.py @@ -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 diff --git a/metadata/shortname.py b/metadata/shortname.py index 9a0206e..6dd6210 100644 --- a/metadata/shortname.py +++ b/metadata/shortname.py @@ -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) diff --git a/modules/apihelper/abyss_team.py b/modules/apihelper/abyss_team.py index 1512307..7170a9e 100644 --- a/modules/apihelper/abyss_team.py +++ b/modules/apihelper/abyss_team.py @@ -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 diff --git a/modules/wiki/base.py b/modules/wiki/base.py index 68b0c12..cf5dc36 100644 --- a/modules/wiki/base.py +++ b/modules/wiki/base.py @@ -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) diff --git a/modules/wiki/character.py b/modules/wiki/character.py index ced2ea2..e380301 100644 --- a/modules/wiki/character.py +++ b/modules/wiki/character.py @@ -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')) ) diff --git a/modules/wiki/material.py b/modules/wiki/material.py index c0c215b..44294f0 100644 --- a/modules/wiki/material.py +++ b/modules/wiki/material.py @@ -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')) diff --git a/modules/wiki/metadata/ascension.json b/modules/wiki/metadata/ascension.json deleted file mode 100644 index 1883bf8..0000000 --- a/modules/wiki/metadata/ascension.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/modules/wiki/metadata/elite.json b/modules/wiki/metadata/elite.json deleted file mode 100644 index 8243a02..0000000 --- a/modules/wiki/metadata/elite.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/modules/wiki/metadata/monster.json b/modules/wiki/metadata/monster.json deleted file mode 100644 index 61f536d..0000000 --- a/modules/wiki/metadata/monster.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/modules/wiki/other.py b/modules/wiki/other.py index 7446de7..41357fd 100644 --- a/modules/wiki/other.py +++ b/modules/wiki/other.py @@ -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'), } diff --git a/modules/wiki/weapon.py b/modules/wiki/weapon.py index e22173f..ff4afa3 100644 --- a/modules/wiki/weapon.py +++ b/modules/wiki/weapon.py @@ -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')), ) diff --git a/plugins/genshin/abyss.py b/plugins/genshin/abyss.py index dd8f543..c14e638 100644 --- a/plugins/genshin/abyss.py +++ b/plugins/genshin/abyss.py @@ -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) } diff --git a/plugins/genshin/abyss_team.py b/plugins/genshin/abyss_team.py index e9fe434..e46bda3 100644 --- a/plugins/genshin/abyss_team.py +++ b/plugins/genshin/abyss_team.py @@ -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, diff --git a/plugins/genshin/daily/material.py b/plugins/genshin/daily/material.py index f316f42..4ac1ca0 100644 --- a/plugins/genshin/daily/material.py +++ b/plugins/genshin/daily/material.py @@ -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每日素材图标搬运完成!", + time = await self._download_icon(notice) + + async def job(_, n): + await n.edit_text( + n.text_html.split('\n')[0] + "\n每日素材图标搬运完成!", 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正在搬运 {_item[0]} 的图标素材。。。" - 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正在搬运 {_item[0]} 的图标素材。。。成功!" - ) - 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"正在搬运 {name} 的图标素材。。。") + 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"正在搬运 {name} 的图标素材。。。成功!") + + 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): diff --git a/plugins/genshin/refresh_metadata.py b/plugins/genshin/refresh_metadata.py new file mode 100644 index 0000000..978d61c --- /dev/null +++ b/plugins/genshin/refresh_metadata.py @@ -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完成!") diff --git a/plugins/genshin/weapon.py b/plugins/genshin/weapon.py index b702449..b6e8d64 100644 --- a/plugins/genshin/weapon.py +++ b/plugins/genshin/weapon.py @@ -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": '', diff --git a/resources/genshin/abyss/abyss.html b/resources/genshin/abyss/abyss.html index c57f852..f1ad4b0 100644 --- a/resources/genshin/abyss/abyss.html +++ b/resources/genshin/abyss/abyss.html @@ -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%); + } +
深境螺旋
-
UID {{uid}}
-
最深抵达 {{max_floor}}
-
战斗次数 {{total_battles}}
-
获得星级 {{total_stars}}
+
UID {{ uid }}
+
最深抵达 {{ max_floor }}
+
战斗次数 {{ total_battles }}
+
获得星级 {{ total_stars }}
出战次数
{% for most_played in most_played_list %} -
-
-
-
{{most_played.value}}次
-
+
+
+
+
{{ most_played.value }}次
+
{% endfor %}
@@ -53,28 +68,28 @@
-
最多击破数:{{most_kills.value}}
- +
最多击破数:{{ most_kills.value }}
+
-
最强一击:{{strongest_strike.value}}
- +
最强一击:{{ strongest_strike.value }}
+
-
承受最多伤害:{{most_damage_taken.value}}
- +
承受最多伤害:{{ most_damage_taken.value }}
+
-
元素爆发数:{{most_bursts_used.value}}
- +
元素爆发数:{{ most_bursts_used.value }}
+
-
元素战技释放次数:{{most_skills_used.value}}
- +
元素战技释放次数:{{ most_skills_used.value }}
+
diff --git a/resources/genshin/abyss/example.html b/resources/genshin/abyss/example.html index a66d260..d8742e3 100644 --- a/resources/genshin/abyss/example.html +++ b/resources/genshin/abyss/example.html @@ -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%); + } + @@ -70,7 +84,7 @@
最多击破数:21
- +
最强一击:21
diff --git a/resources/genshin/daily_material/example.html b/resources/genshin/daily_material/example.html index 0bac0eb..e03b115 100644 --- a/resources/genshin/daily_material/example.html +++ b/resources/genshin/daily_material/example.html @@ -28,7 +28,7 @@
- +
star @@ -37,7 +37,7 @@
- +
star @@ -47,7 +47,7 @@
- +
star @@ -58,7 +58,7 @@
- +
star @@ -75,17 +75,17 @@
Lv.90
6命
- 一斗 + 八重神子
-
一斗
+
八重神子
Lv.90
6命
- 神里绫华 + 神里绫华
神里绫华
@@ -95,7 +95,7 @@
Lv.90
6命
- 神里绫华 + 神里绫华
神里绫华
@@ -104,7 +104,7 @@
Lv.90
- 神里绫华 + 神里绫华
神里绫华
@@ -112,7 +112,7 @@
- 神里绫华 + 神里绫华
神里绫华
@@ -120,7 +120,7 @@
- 神里绫华 + 神里绫华
神里绫华
@@ -140,7 +140,7 @@
- +
star @@ -149,7 +149,7 @@
- +
star @@ -159,7 +159,7 @@
- +
star @@ -170,7 +170,7 @@
- +
star @@ -187,7 +187,7 @@
Lv.90
6命
- 神里绫华 + 神里绫华
神里绫华
@@ -197,7 +197,7 @@
Lv.90
6命
- 神里绫华 + 神里绫华
神里绫华
@@ -207,7 +207,7 @@
Lv.90
6命
- 神里绫华 + 神里绫华
神里绫华
@@ -216,7 +216,7 @@
Lv.90
- 神里绫华 + 神里绫华
神里绫华
@@ -224,7 +224,7 @@
- 神里绫华 + 神里绫华
神里绫华
@@ -232,7 +232,7 @@
- 神里绫华 + 神里绫华
神里绫华
@@ -256,7 +256,7 @@
- +
star @@ -265,7 +265,7 @@
- +
star @@ -275,7 +275,7 @@
- +
star @@ -286,7 +286,7 @@
- +
star @@ -301,12 +301,12 @@
- 申鹤 + 申鹤
Lv.90
精炼5
- 天空之刃 + 天空之刃
天空之刃
@@ -314,12 +314,12 @@
- 神里绫华 + 神里绫华
Lv.90
精炼5
- 天空之刃 + 天空之刃
天空之刃
@@ -327,12 +327,12 @@
- 神里绫华 + 神里绫华
Lv.90
精炼5
- 天空之刃 + 天空之刃
天空之刃
@@ -340,12 +340,12 @@
- 神里绫华 + 神里绫华
Lv.90
精炼5
- 天空之刃 + 天空之刃
天空之刃
@@ -353,7 +353,7 @@
- 天空之刃 + 天空之刃
天空之刃
@@ -361,7 +361,7 @@
- 天空之刃 + 天空之刃
天空之刃
diff --git a/resources/genshin/daily_material/style.css b/resources/genshin/daily_material/style.css index 13e2ffc..63c9c69 100644 --- a/resources/genshin/daily_material/style.css +++ b/resources/genshin/daily_material/style.css @@ -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; -} \ No newline at end of file +} diff --git a/utils/const.py b/utils/const.py index 84220fd..6105d23 100644 --- a/utils/const.py +++ b/utils/const.py @@ -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/") diff --git a/utils/helpers.py b/utils/helpers.py index 56ba7a1..de626af 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -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} diff --git a/utils/typedefs.py b/utils/typedefs.py index 38fce49..f47dbe1 100644 --- a/utils/typedefs.py +++ b/utils/typedefs.py @@ -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]