diff --git a/core/dependence/assets.py b/core/dependence/assets.py new file mode 100644 index 0000000..a476a10 --- /dev/null +++ b/core/dependence/assets.py @@ -0,0 +1,359 @@ +import asyncio +from pathlib import Path +from ssl import SSLZeroReturnError +from typing import Optional, List, Dict + +from aiofiles import open as async_open +from httpx import AsyncClient, HTTPError + +from core.base_service import BaseService +from modules.wiki.base import WikiModel +from modules.wiki.models.avatar import Avatar +from modules.wiki.models.weapon import Weapon +from modules.wiki.models.buddy import Buddy +from modules.wiki.models.equipment_suit import EquipmentSuit +from utils.const import PROJECT_ROOT +from utils.log import logger +from utils.typedefs import StrOrURL, StrOrInt + +ASSETS_PATH = PROJECT_ROOT.joinpath("resources/assets") +ASSETS_PATH.mkdir(exist_ok=True, parents=True) +DATA_MAP = { + "avatar": WikiModel.BASE_URL + "avatars.json", + "weapon": WikiModel.BASE_URL + "weapons.json", + "buddy": WikiModel.BASE_URL + "buddy.json", + "equipment_suit": WikiModel.BASE_URL + "equipment_suits.json", +} + + +class AssetsServiceError(Exception): + pass + + +class AssetsCouldNotFound(AssetsServiceError): + def __init__(self, message: str, target: str): + self.message = message + self.target = target + super().__init__(f"{message}: target={target}") + + +class _AssetsService: + client: Optional[AsyncClient] = None + + def __init__(self, client: Optional[AsyncClient] = None) -> None: + self.client = client + + async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Optional[Path]: + """从 url 下载图标至 path""" + if not url: + return None + logger.debug("正在从 %s 下载图标至 %s", url, path) + headers = 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() + + +class _AvatarAssets(_AssetsService): + path: Path + data: List[Avatar] + name_map: Dict[str, Avatar] + id_map: Dict[int, Avatar] + + def __init__(self, client: Optional[AsyncClient] = None) -> None: + super().__init__(client) + self.path = ASSETS_PATH.joinpath("agent") + self.path.mkdir(exist_ok=True, parents=True) + + async def initialize(self): + logger.info("正在初始化角色素材图标") + html = await self.client.get(DATA_MAP["avatar"]) + self.data = [Avatar(**data) for data in html.json()] + self.name_map = {icon.name: icon for icon in self.data} + self.id_map = {icon.id: icon for icon in self.data} + tasks = [] + for icon in self.data: + base_path = self.path / f"{icon.id}" + base_path.mkdir(exist_ok=True, parents=True) + gacha_path = base_path / "gacha.png" + icon_path = base_path / "icon.png" + normal_path = base_path / "normal.png" + if not gacha_path.exists(): + tasks.append(self._download(icon.gacha, gacha_path)) + if not icon_path.exists(): + tasks.append(self._download(icon.icon_, icon_path)) + if not normal_path.exists(): + tasks.append(self._download(icon.normal, normal_path)) + + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("角色素材图标初始化完成") + + def get_path(self, icon: Avatar, name: str, ext: str = "png") -> Path: + path = self.path / f"{icon.id}" + path.mkdir(exist_ok=True, parents=True) + return path / f"{name}.{ext}" + + def get_by_id(self, id_: int) -> Optional[Avatar]: + return self.id_map.get(id_, None) + + def get_by_name(self, name: str) -> Optional[Avatar]: + return self.name_map.get(name, None) + + def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Avatar: + data = None + if isinstance(target, int): + data = self.get_by_id(target) + elif isinstance(target, str): + data = self.get_by_name(target) + if data is None: + if second_target: + return self.get_target(second_target) + raise AssetsCouldNotFound("角色素材图标不存在", target) + return data + + def gacha(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + return self.get_path(icon, "gacha") + + def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + return self.get_path(icon, "icon") + + def normal(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + return self.get_path(icon, "normal") + + +class _WeaponAssets(_AssetsService): + path: Path + data: List[Weapon] + name_map: Dict[str, Weapon] + id_map: Dict[int, Weapon] + + def __init__(self, client: Optional[AsyncClient] = None) -> None: + super().__init__(client) + self.path = ASSETS_PATH.joinpath("engines") + self.path.mkdir(exist_ok=True, parents=True) + + async def initialize(self): + logger.info("正在初始化武器素材图标") + html = await self.client.get(DATA_MAP["weapon"]) + self.data = [Weapon(**data) for data in html.json()] + self.name_map = {icon.name: icon for icon in self.data} + self.id_map = {icon.id: icon for icon in self.data} + tasks = [] + for icon in self.data: + base_path = self.path / f"{icon.id}" + base_path.mkdir(exist_ok=True, parents=True) + icon_path = base_path / "icon.webp" + if not icon_path.exists(): + tasks.append(self._download(icon.icon, icon_path)) + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("武器素材图标初始化完成") + + def get_path(self, icon: Weapon, name: str) -> Path: + path = self.path / f"{icon.id}" + path.mkdir(exist_ok=True, parents=True) + return path / f"{name}.webp" + + def get_by_id(self, id_: int) -> Optional[Weapon]: + return self.id_map.get(id_, None) + + def get_by_name(self, name: str) -> Optional[Weapon]: + return self.name_map.get(name, None) + + def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[Weapon]: + if isinstance(target, int): + return self.get_by_id(target) + elif isinstance(target, str): + return self.get_by_name(target) + if second_target: + return self.get_target(second_target) + raise AssetsCouldNotFound("武器素材图标不存在", target) + + def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + return self.get_path(icon, "icon") + + +class _BuddyAssets(_AssetsService): + path: Path + data: List[Buddy] + id_map: Dict[int, Buddy] + name_map: Dict[str, Buddy] + + def __init__(self, client: Optional[AsyncClient] = None) -> None: + super().__init__(client) + self.path = ASSETS_PATH.joinpath("buddy") + self.path.mkdir(exist_ok=True, parents=True) + + async def initialize(self): + logger.info("正在初始化邦布素材图标") + html = await self.client.get(DATA_MAP["buddy"]) + self.data = [Buddy(**data) for data in html.json()] + self.id_map = {icon.id: icon for icon in self.data} + self.name_map = {icon.name: icon for icon in self.data} + tasks = [] + for icon in self.data: + webp_path = self.path / f"{icon.id}.webp" + png_path = self.path / f"{icon.id}.png" + if not webp_path.exists() and icon.webp: + tasks.append(self._download(icon.webp, webp_path)) + if not png_path.exists() and icon.png: + tasks.append(self._download(icon.png, png_path)) + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("邦布素材图标初始化完成") + + def get_path(self, icon: Buddy, ext: str) -> Path: + path = self.path / f"{icon.id}.{ext}" + return path + + def get_by_id(self, id_: int) -> Optional[Buddy]: + return self.id_map.get(id_, None) + + def get_by_name(self, name: str) -> Optional[Buddy]: + return self.name_map.get(name, None) + + def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[Buddy]: + if isinstance(target, int): + return self.get_by_id(target) + elif isinstance(target, str): + return self.get_by_name(target) + if second_target: + return self.get_target(second_target) + raise AssetsCouldNotFound("邦布素材图标不存在", target) + + def webp(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + return self.get_path(icon, "webp") + + def png(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + return self.get_path(icon, "png") + + def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + icon = self.get_target(target, second_target) + webp_path = self.get_path(icon, "webp") + png_path = self.get_path(icon, "png") + if webp_path.exists(): + return webp_path + if png_path.exists(): + return png_path + raise AssetsCouldNotFound("邦布素材图标不存在", target) + + +class _EquipmentSuitAssets(_AssetsService): + path: Path + data: List[EquipmentSuit] + id_map: Dict[int, EquipmentSuit] + name_map: Dict[str, EquipmentSuit] + + def __init__(self, client: Optional[AsyncClient] = None) -> None: + super().__init__(client) + self.path = ASSETS_PATH.joinpath("equipment_suit") + self.path.mkdir(exist_ok=True, parents=True) + + async def initialize(self): + logger.info("正在初始化驱动盘素材图标") + html = await self.client.get(DATA_MAP["equipment_suit"]) + self.data = [EquipmentSuit(**data) for data in html.json()] + self.id_map = {theme.id: theme for theme in self.data} + self.name_map = {theme.name: theme for theme in self.data} + tasks = [] + for theme in self.data: + path = self.path / f"{theme.id}.webp" + if not path.exists(): + tasks.append(self._download(theme.icon, path)) + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("驱动盘素材图标初始化完成") + + def get_path(self, theme: EquipmentSuit, ext: str) -> Path: + path = self.path / f"{theme.id}.{ext}" + return path + + def get_by_id(self, id_: int) -> Optional[EquipmentSuit]: + return self.id_map.get(id_, None) + + def get_by_name(self, name_: str) -> Optional[EquipmentSuit]: + return self.name_map.get(name_, None) + + def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[EquipmentSuit]: + if isinstance(target, int): + return self.get_by_id(target) + elif isinstance(target, str): + return self.get_by_name(target) + if second_target: + return self.get_target(second_target) + raise AssetsCouldNotFound("驱动盘素材图标不存在", target) + + def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path: + theme = self.get_target(target, second_target) + webp_path = self.get_path(theme, "webp") + if webp_path.exists(): + return webp_path + raise AssetsCouldNotFound("驱动盘素材图标不存在", target) + + +class AssetsService(BaseService.Dependence): + """asset服务 + + 用于储存和管理 asset : + 当对应的 asset (如某角色图标)不存在时,该服务会先查找本地。 + 若本地不存在,则从网络上下载;若存在,则返回其路径 + """ + + client: Optional[AsyncClient] = None + + avatar: _AvatarAssets + """角色""" + + weapon: _WeaponAssets + """武器""" + + buddy: _BuddyAssets + """邦布""" + + equipment_suit: _EquipmentSuitAssets + """驱动盘""" + + def __init__(self): + self.client = AsyncClient(timeout=60.0) + self.avatar = _AvatarAssets(self.client) + self.weapon = _WeaponAssets(self.client) + self.buddy = _BuddyAssets(self.client) + self.equipment_suit = _EquipmentSuitAssets(self.client) + + async def initialize(self): # pylint: disable=W0221 + await self.avatar.initialize() + await self.weapon.initialize() + await self.buddy.initialize() + await self.equipment_suit.initialize() diff --git a/core/services/game/__init__.py b/core/services/game/__init__.py new file mode 100644 index 0000000..78e0e2f --- /dev/null +++ b/core/services/game/__init__.py @@ -0,0 +1 @@ +"""GameService""" diff --git a/core/services/game/cache.py b/core/services/game/cache.py new file mode 100644 index 0000000..d287adb --- /dev/null +++ b/core/services/game/cache.py @@ -0,0 +1,61 @@ +from typing import List + +from core.base_service import BaseService +from core.dependence.redisdb import RedisDB + +__all__ = [ + "GameCache", + "GameCacheForAvatar", + "GameCacheForStrategy", + "GameCacheForBuddy", + "GameCacheForWeapon", + "GameCacheForEquipmentSuit", +] + + +class GameCache: + qname: str + + def __init__(self, redis: RedisDB, ttl: int = 3600): + self.client = redis.client + self.ttl = ttl + + async def get_url_list(self, character_name: str): + qname = f"{self.qname}:{character_name}" + return [str(str_data, encoding="utf-8") for str_data in await self.client.lrange(qname, 0, -1)][::-1] + + async def set_url_list(self, character_name: str, str_list: List[str]): + qname = f"{self.qname}:{character_name}" + await self.client.ltrim(qname, 1, 0) + await self.client.lpush(qname, *str_list) + await self.client.expire(qname, self.ttl) + return await self.client.llen(qname) + + async def get_file(self, character_name: str): + qname = f"{self.qname}:{character_name}" + return await self.client.get(qname) + + async def set_file(self, character_name: str, file: str): + qname = f"{self.qname}:{character_name}" + await self.client.set(qname, file) + await self.client.expire(qname, self.ttl) + + +class GameCacheForAvatar(BaseService.Component, GameCache): + qname = "game:avatar" + + +class GameCacheForStrategy(BaseService.Component, GameCache): + qname = "game:strategy" + + +class GameCacheForBuddy(BaseService.Component, GameCache): + qname = "game:buddy" + + +class GameCacheForWeapon(BaseService.Component, GameCache): + qname = "game:weapon" + + +class GameCacheForEquipmentSuit(BaseService.Component, GameCache): + qname = "game:relics" diff --git a/core/services/game/services.py b/core/services/game/services.py new file mode 100644 index 0000000..62f3ab3 --- /dev/null +++ b/core/services/game/services.py @@ -0,0 +1,66 @@ +from core.base_service import BaseService +from core.services.game.cache import ( + GameCacheForAvatar, + GameCacheForStrategy, + GameCacheForBuddy, + GameCacheForWeapon, + GameCacheForEquipmentSuit, +) + +__all__ = "GameCacheService" + + +class GameCacheService(BaseService): + def __init__( + self, + avatar_cache: GameCacheForAvatar, + strategy_cache: GameCacheForStrategy, + buddy_cache: GameCacheForBuddy, + weapon_cache: GameCacheForWeapon, + equipment_suit_cache: GameCacheForEquipmentSuit, + ): + self.avatar_cache = avatar_cache + self.strategy_cache = strategy_cache + self.buddy_cache = buddy_cache + self.weapon_cache = weapon_cache + self.equipment_suit_cache = equipment_suit_cache + + async def get_avatar_cache(self, character_name: str) -> str: + cache = await self.avatar_cache.get_file(character_name) + if cache is not None: + return cache.decode("utf-8") + + async def set_avatar_cache(self, character_name: str, file: str) -> None: + await self.avatar_cache.set_file(character_name, file) + + async def get_strategy_cache(self, character_name: str) -> str: + cache = await self.strategy_cache.get_file(character_name) + if cache is not None: + return cache.decode("utf-8") + + async def set_strategy_cache(self, character_name: str, file: str) -> None: + await self.strategy_cache.set_file(character_name, file) + + async def get_buddy_cache(self, character_name: str) -> str: + cache = await self.buddy_cache.get_file(character_name) + if cache is not None: + return cache.decode("utf-8") + + async def set_buddy_cache(self, character_name: str, file: str) -> None: + await self.buddy_cache.set_file(character_name, file) + + async def get_weapon_cache(self, weapon_name: str) -> str: + cache = await self.weapon_cache.get_file(weapon_name) + if cache is not None: + return cache.decode("utf-8") + + async def set_weapon_cache(self, weapon_name: str, file: str) -> None: + await self.weapon_cache.set_file(weapon_name, file) + + async def get_equipment_suit_cache(self, relics_name: str) -> str: + cache = await self.equipment_suit_cache.get_file(relics_name) + if cache is not None: + return cache.decode("utf-8") + + async def set_equipment_suit_cache(self, relics_name: str, file: str) -> None: + await self.equipment_suit_cache.set_file(relics_name, file) diff --git a/core/services/wiki/__init__.py b/core/services/wiki/__init__.py new file mode 100644 index 0000000..042a3d3 --- /dev/null +++ b/core/services/wiki/__init__.py @@ -0,0 +1 @@ +"""WikiService""" diff --git a/core/services/wiki/services.py b/core/services/wiki/services.py new file mode 100644 index 0000000..fd57e3f --- /dev/null +++ b/core/services/wiki/services.py @@ -0,0 +1,44 @@ +from core.base_service import BaseService +from modules.wiki.character import Character +from modules.wiki.weapon import Weapon +from modules.wiki.buddy import Buddy +from modules.wiki.raider import Raider +from modules.wiki.equipment_suit import EquipmentSuit +from utils.log import logger + +__all__ = ["WikiService"] + + +class WikiService(BaseService): + def __init__(self): + self.character = Character() + self.weapon = Weapon() + self.buddy = Buddy() + self.raider = Raider() + self.equipment_suit = EquipmentSuit() + + async def initialize(self) -> None: + logger.info("正在加载 Wiki 数据") + try: + await self.character.read() + await self.weapon.read() + await self.buddy.read() + await self.raider.read() + await self.equipment_suit.read() + except Exception as e: + logger.error("加载 Wiki 数据失败", exc_info=e) + logger.info("加载 Wiki 数据完成") + + async def refresh_wiki(self) -> None: + logger.info("正在重新获取Wiki") + logger.info("正在重新获取角色信息") + await self.character.refresh() + logger.info("正在重新获取武器信息") + await self.weapon.refresh() + logger.info("正在重新获取邦布信息") + await self.buddy.refresh() + logger.info("正在重新获取攻略信息") + await self.raider.refresh() + logger.info("正在重新获取驱动盘信息") + await self.equipment_suit.refresh() + logger.info("刷新成功") diff --git a/metadata/pool/pool.py b/metadata/pool/pool.py index 8b27a15..6c34d1d 100644 --- a/metadata/pool/pool.py +++ b/metadata/pool/pool.py @@ -1,19 +1,16 @@ -from metadata.pool.pool_100 import POOL_100 -from metadata.pool.pool_200 import POOL_200 -from metadata.pool.pool_301 import POOL_301 -from metadata.pool.pool_302 import POOL_302 -from metadata.pool.pool_500 import POOL_500 +from metadata.pool.pool_1 import POOL_1 +from metadata.pool.pool_2 import POOL_2 +from metadata.pool.pool_3 import POOL_3 +from metadata.pool.pool_5 import POOL_5 def get_pool_by_id(pool_type): - if pool_type == 100: - return POOL_100 - if pool_type == 200: - return POOL_200 - if pool_type in [301, 400]: - return POOL_301 - if pool_type == 302: - return POOL_302 - if pool_type == 500: - return POOL_500 + if pool_type == 1: + return POOL_1 + if pool_type == 2: + return POOL_2 + if pool_type == 3: + return POOL_3 + if pool_type == 5: + return POOL_5 return None diff --git a/metadata/pool/pool_1.py b/metadata/pool/pool_1.py new file mode 100644 index 0000000..c03b1b9 --- /dev/null +++ b/metadata/pool/pool_1.py @@ -0,0 +1,3 @@ +POOL_1 = [ + {"five": ["热门卡司"], "four": [], "from": "2024-07-04 06:00:00", "name": "热门卡司", "to": "2050-09-15 17:59:59"} +] diff --git a/metadata/pool/pool_100.py b/metadata/pool/pool_100.py deleted file mode 100644 index 4df0371..0000000 --- a/metadata/pool/pool_100.py +++ /dev/null @@ -1,3 +0,0 @@ -POOL_100 = [ - {"five": ["新手池"], "four": [], "from": "2020-09-15 06:00:00", "name": "新手池", "to": "2050-09-15 17:59:59"} -] diff --git a/metadata/pool/pool_2.py b/metadata/pool/pool_2.py new file mode 100644 index 0000000..fd08003 --- /dev/null +++ b/metadata/pool/pool_2.py @@ -0,0 +1,9 @@ +POOL_2 = [ + { + "five": ["艾莲"], + "four": ["苍角", "安东"], + "name": "慵懒逐浪", + "from": "2024-07-04 06:00:00", + "to": "2024-07-24 11:59:59", + }, +] diff --git a/metadata/pool/pool_200.py b/metadata/pool/pool_200.py deleted file mode 100644 index b3a8032..0000000 --- a/metadata/pool/pool_200.py +++ /dev/null @@ -1,3 +0,0 @@ -POOL_200 = [ - {"five": ["常驻池"], "four": [], "from": "2020-09-15 06:00:00", "name": "奔行世间", "to": "2050-09-15 17:59:59"} -] diff --git a/metadata/pool/pool_3.py b/metadata/pool/pool_3.py new file mode 100644 index 0000000..c1f87f7 --- /dev/null +++ b/metadata/pool/pool_3.py @@ -0,0 +1,9 @@ +POOL_3 = [ + { + "five": ["深海访客"], + "four": ["含羞恶面", "旋钻机-赤轴"], + "name": "喧哗奏鸣", + "from": "2024-07-04 06:00:00", + "to": "2024-07-24 11:59:59", + }, +] diff --git a/metadata/pool/pool_301.py b/metadata/pool/pool_301.py deleted file mode 100644 index 7b1008b..0000000 --- a/metadata/pool/pool_301.py +++ /dev/null @@ -1,471 +0,0 @@ -POOL_301 = [ - { - "five": ["希格雯", "芙宁娜"], - "four": ["诺艾尔", "嘉明", "罗莎莉亚"], - "name": "柔柔海露心|众水的颂诗", - "from": "2024-06-25 18:00:00", - "to": "2024-07-16 14:59:59", - }, - { - "five": ["克洛琳德", "艾尔海森"], - "four": ["赛索斯", "班尼特", "托马"], - "name": "流霆贯夜|敕诫枢谋", - "from": "2024-06-05 06:00:00", - "to": "2024-06-25 17:59:59", - }, - { - "five": ["流浪者", "白术"], - "four": ["莱依拉", "珐露珊", "北斗"], - "name": "余火变相|心珠循琅", - "from": "2024-05-14 18:00:00", - "to": "2024-06-04 14:59:59", - }, - { - "five": ["阿蕾奇诺", "林尼"], - "four": ["菲米尼", "琳妮特", "香菱"], - "name": "炉边烬影|光与影的戏术", - "from": "2024-04-24 06:00:00", - "to": "2024-05-14 17:59:59", - }, - { - "five": ["那维莱特", "枫原万叶"], - "four": ["芭芭拉", "行秋", "烟绯"], - "name": "谕告的潮音|红叶逐荒波", - "from": "2024-04-02 18:00:00", - "to": "2024-04-23 14:59:59", - }, - { - "five": ["千织", "荒泷一斗"], - "four": ["五郎", "云堇", "多莉"], - "name": "千云绘羽织|鬼门斗宴", - "from": "2024-03-13 06:00:00", - "to": "2024-04-02 17:59:59", - }, - { - "five": ["魈", "八重神子"], - "four": ["瑶瑶", "辛焱", "凝光"], - "name": "烟火之邀|华紫樱绯", - "from": "2024-02-20 18:00:00", - "to": "2024-03-12 14:59:59", - }, - { - "five": ["闲云", "纳西妲"], - "four": ["嘉明", "珐露珊", "诺艾尔"], - "name": "云府鹤行|月草的赐慧", - "from": "2024-01-31 06:00:00", - "to": "2024-02-20 17:59:59", - }, - { - "five": ["雷电将军", "宵宫"], - "four": ["夏沃蕾", "九条裟罗", "班尼特"], - "name": "影寂天下人|琉焰华舞", - "from": "2024-01-09 18:00:00", - "to": "2024-01-30 14:59:59", - }, - { - "five": ["娜维娅", "神里绫华"], - "four": ["砂糖", "罗莎莉亚", "坎蒂丝"], - "name": "刺玫的铭誓|白鹭之庭", - "from": "2023-12-20 06:00:00", - "to": "2024-01-09 17:59:59", - }, - { - "five": ["赛诺", "神里绫人"], - "four": ["绮良良", "久岐忍", "香菱"], - "name": "雳裁冥昭|苍流踏花", - "from": "2023-11-28 18:00:00", - "to": "2023-12-19 14:59:59", - }, - { - "five": ["芙宁娜", "白术"], - "four": ["夏洛蒂", "柯莱", "北斗"], - "from": "2023-11-08 06:00:00", - "name": "众水的颂诗|心珠循琅", - "to": "2023-11-28 17:59:59", - }, - { - "five": ["莱欧斯利", "温迪"], - "four": ["重云", "托马", "多莉"], - "from": "2023-10-17 18:00:00", - "name": "劫中泛滥|杯装之诗", - "to": "2023-11-07 14:59:59", - }, - { - "five": ["那维莱特", "胡桃"], - "four": ["菲谢尔", "行秋", "迪奥娜"], - "from": "2023-09-27 06:00:00", - "name": "谕告的潮音|雪霁梅香", - "to": "2023-10-17 17:59:59", - }, - { - "five": ["钟离", "达达利亚"], - "four": ["菲米尼", "早柚", "诺艾尔"], - "from": "2023-09-05 18:00:00", - "name": "陵薮市朝|暂别冬都", - "to": "2023-09-26 14:59:59", - }, - { - "five": ["林尼", "夜兰"], - "four": ["琳妮特", "班尼特", "芭芭拉"], - "from": "2023-08-16 06:00:00", - "name": "光与影的戏术|素霓伣天", - "to": "2023-09-05 17:59:59", - }, - { - "five": ["珊瑚宫心海", "流浪者"], - "four": ["珐露珊", "罗莎莉亚", "烟绯"], - "from": "2023-07-25 18:00:00", - "name": "浮岳虹珠|余火变相", - "to": "2023-08-15 14:59:59", - }, - { - "five": ["优菈", "可莉"], - "four": ["米卡", "雷泽", "托马"], - "from": "2023-07-05 06:00:00", - "name": "浪涌之瞬|闪焰的驻足", - "to": "2023-07-25 17:59:59", - }, - { - "five": ["艾尔海森", "枫原万叶"], - "four": ["瑶瑶", "鹿野院平藏", "香菱"], - "from": "2023-06-13 18:00:00", - "name": "敕诫枢谋|叶落风随", - "to": "2023-07-04 14:59:59", - }, - { - "five": ["宵宫", "八重神子"], - "four": ["绮良良", "云堇", "重云"], - "from": "2023-05-24 06:00:00", - "name": "琉焰华舞|浮世笑百姿", - "to": "2023-06-13 17:59:59", - }, - { - "five": ["白术", "甘雨"], - "four": ["卡维", "坎蒂丝", "菲谢尔"], - "from": "2023-05-02 18:00:00", - "name": "心珠循琅|浮生孰来", - "to": "2023-05-23 14:59:59", - }, - { - "five": ["纳西妲", "妮露"], - "four": ["久岐忍", "多莉", "莱依拉"], - "from": "2023-04-12 06:00:00", - "name": "月草的赐慧|翩舞歈莲", - "to": "2023-05-02 17:59:59", - }, - { - "five": ["申鹤", "神里绫华"], - "four": ["米卡", "砂糖", "迪奥娜"], - "from": "2023-03-21 18:00:00", - "name": "孤辰茕怀|白鹭霜华", - "to": "2023-04-11 14:59:59", - }, - { - "five": ["迪希雅", "赛诺"], - "four": ["班尼特", "芭芭拉", "柯莱"], - "from": "2023-03-01 06:00:00", - "name": "烈阳烁金|雳裁冥昭", - "to": "2023-03-21 17:59:59", - }, - { - "five": ["胡桃", "夜兰"], - "four": ["行秋", "凝光", "北斗"], - "from": "2023-02-07 18:00:00", - "name": "赤团开时|素霓伣天", - "to": "2023-02-28 14:59:59", - }, - { - "five": ["艾尔海森", "魈"], - "four": ["瑶瑶", "云堇", "辛焱"], - "from": "2023-01-18 06:00:00", - "name": "敕诫枢谋|烟火之邀", - "to": "2023-02-07 17:59:59", - }, - { - "five": ["雷电将军", "神里绫人"], - "four": ["罗莎莉亚", "早柚", "九条裟罗"], - "from": "2022-12-27 18:00:00", - "name": "影寂天下人|苍流踏花", - "to": "2023-01-17 14:59:59", - }, - { - "five": ["流浪者", "荒泷一斗"], - "four": ["珐露珊", "五郎", "烟绯"], - "from": "2022-12-07 06:00:00", - "name": "余火变相|鬼门斗宴", - "to": "2022-12-27 17:59:59", - }, - { - "five": ["八重神子", "达达利亚"], - "four": ["莱依拉", "托马", "鹿野院平藏"], - "from": "2022-11-18 18:00:00", - "name": "华紫樱绯|暂别冬都", - "to": "2022-12-06 14:59:59", - }, - { - "five": ["纳西妲", "宵宫"], - "four": ["雷泽", "诺艾尔", "班尼特"], - "from": "2022-11-2 06:00:00", - "name": "月草的赐慧|焰色天河", - "to": "2022-11-18 17:59:59", - }, - { - "five": ["妮露", "阿贝多"], - "four": ["北斗", "芭芭拉", "香菱"], - "from": "2022-10-14 18:00:00", - "name": "翩舞歈莲|深秘之息", - "to": "2022-11-01 14:59:59", - }, - { - "five": ["赛诺", "温迪"], - "four": ["久岐忍", "早柚", "坎蒂丝"], - "from": "2022-09-28 06:00:00", - "name": "雳裁冥昭|杯装之诗", - "to": "2022-10-14 17:59:59", - }, - { - "five": ["甘雨", "心海"], - "four": ["行秋", "砂糖", "多莉"], - "from": "2022-09-09 18:00:00", - "name": "浮生孰来|浮岳虹珠", - "to": "2022-09-27 14:59:59", - }, - { - "five": ["提纳里", "钟离"], - "four": ["云堇", "辛焱", "班尼特"], - "from": "2022-08-24 06:00:00", - "name": "巡御蘙荟|陵薮市朝", - "to": "2022-09-09 17:59:59", - }, - { - "five": ["宵宫"], - "four": ["云堇", "辛焱", "班尼特"], - "from": "2022-08-02 18:00:00", - "name": "焰色天河", - "to": "2022-08-23 14:59:59", - }, - { - "five": ["枫原万叶", "可莉"], - "four": ["凝光", "鹿野院平藏", "托马"], - "from": "2022-07-13 06:00:00", - "name": "红叶逐荒波", - "to": "2022-08-02 17:59:59", - }, - { - "five": ["荒泷一斗"], - "four": ["烟绯", "芭芭拉", "诺艾尔"], - "from": "2022-06-21 18:00:00", - "name": "鬼门斗宴", - "to": "2022-07-12 14:59:59", - }, - { - "five": ["夜兰", "魈"], - "four": ["烟绯", "芭芭拉", "诺艾尔"], - "from": "2022-05-31 06:00:00", - "name": "素霓伣天|烟火之邀", - "to": "2022-06-21 17:59:59", - }, - { - "five": ["神里绫华"], - "four": ["罗莎莉亚", "早柚", "雷泽"], - "from": "2022-04-19 17:59:59", - "name": "白鹭之庭", - "to": "2022-05-31 05:59:59", - }, - { - "five": ["神里绫人", "温迪"], - "four": ["香菱", "砂糖", "云堇"], - "from": "2022-03-30 06:00:00", - "name": "苍流踏花|杯装之诗", - "to": "2022-04-19 17:59:59", - }, - { - "five": ["雷电将军", "珊瑚宫心海"], - "four": ["辛焱", "九条裟罗", "班尼特"], - "from": "2022-03-08 18:00:00", - "name": "影寂天下人|浮岳虹珠", - "to": "2022-03-29 14:59:59", - }, - { - "five": ["八重神子"], - "four": ["菲谢尔", "迪奥娜", "托马"], - "from": "2022-02-16 06:00:00", - "name": "华紫樱绯", - "to": "2022-03-08 17:59:59", - }, - { - "five": ["甘雨", "钟离"], - "four": ["行秋", "北斗", "烟绯"], - "from": "2022-01-25 18:00:00", - "name": "浮生孰来|陵薮市朝", - "to": "2022-02-15 14:59:59", - }, - { - "five": ["申鹤", "魈"], - "four": ["云堇", "凝光", "重云"], - "from": "2022-01-05 06:00:00", - "name": "出尘入世|烟火之邀", - "to": "2022-01-25 17:59:59", - }, - { - "five": ["荒泷一斗"], - "four": ["五郎", "芭芭拉", "香菱"], - "from": "2021-12-14 18:00:00", - "name": "鬼门斗宴", - "to": "2022-01-04 14:59:59", - }, - { - "five": ["阿贝多", "优菈"], - "four": ["班尼特", "诺艾尔", "罗莎莉亚"], - "from": "2021-11-24 06:00:00", - "name": "深秘之息|浪涌之瞬", - "to": "2021-12-14 17:59:59", - }, - { - "five": ["胡桃"], - "four": ["托马", "迪奥娜", "早柚"], - "from": "2021-11-02 18:00:00", - "name": "赤团开时", - "to": "2021-11-23 14:59:59", - }, - { - "five": ["达达利亚"], - "four": ["凝光", "重云", "烟绯"], - "from": "2021-10-13 06:00:00", - "name": "暂别冬都", - "to": "2021-11-02 17:59:59", - }, - { - "five": ["珊瑚宫心海"], - "four": ["罗莎莉亚", "北斗", "行秋"], - "from": "2021-09-21 18:00:00", - "name": "浮岳虹珠", - "to": "2021-10-12 14:59:59", - }, - { - "five": ["雷电将军"], - "four": ["九条裟罗", "香菱", "砂糖"], - "from": "2021-09-01 06:00:00", - "name": "影寂天下人", - "to": "2021-09-21 17:59:59", - }, - { - "five": ["宵宫"], - "four": ["早柚", "迪奥娜", "辛焱"], - "from": "2021-08-10 18:00:00", - "name": "焰色天河", - "to": "2021-08-31 14:59:59", - }, - { - "five": ["神里绫华"], - "four": ["凝光", "重云", "烟绯"], - "from": "2021-07-21 06:00:00", - "name": "白鹭之庭", - "to": "2021-08-10 17:59:59", - }, - { - "five": ["枫原万叶"], - "four": ["罗莎莉亚", "班尼特", "雷泽"], - "from": "2021-06-29 18:00:00", - "name": "红叶逐荒波", - "to": "2021-07-20 14:59:59", - }, - { - "five": ["可莉"], - "four": ["芭芭拉", "砂糖", "菲谢尔"], - "from": "2021-06-09 06:00:00", - "name": "逃跑的太阳", - "to": "2021-06-29 17:59:59", - }, - { - "five": ["优菈"], - "four": ["辛焱", "行秋", "北斗"], - "from": "2021-05-18 18:00:00", - "name": "浪沫的旋舞", - "to": "2021-06-08 14:59:59", - }, - { - "five": ["钟离"], - "four": ["烟绯", "诺艾尔", "迪奥娜"], - "from": "2021-04-28 06:00:00", - "name": "陵薮市朝", - "to": "2021-05-18 17:59:59", - }, - { - "five": ["达达利亚"], - "four": ["罗莎莉亚", "芭芭拉", "菲谢尔"], - "from": "2021-04-06 18:00:00", - "name": "暂别冬都", - "to": "2021-04-27 14:59:59", - }, - { - "five": ["温迪"], - "four": ["砂糖", "雷泽", "诺艾尔"], - "from": "2021-03-17 06:00:00", - "name": "杯装之诗", - "to": "2021-04-06 15:59:59", - }, - { - "five": ["胡桃"], - "four": ["行秋", "香菱", "重云"], - "from": "2021-03-02 18:00:00", - "name": "赤团开时", - "to": "2021-03-16 14:59:59", - }, - { - "five": ["刻晴"], - "four": ["凝光", "班尼特", "芭芭拉"], - "from": "2021-02-17 18:00:00", - "name": "鱼龙灯昼", - "to": "2021-03-02 15:59:59", - }, - { - "five": ["魈"], - "four": ["迪奥娜", "北斗", "辛焱"], - "from": "2021-02-03 06:00:00", - "name": "烟火之邀", - "to": "2021-02-17 15:59:59", - }, - { - "five": ["甘雨"], - "four": ["香菱", "行秋", "诺艾尔"], - "from": "2021-01-12 18:00:00", - "name": "浮生孰来", - "to": "2021-02-02 14:59:59", - }, - { - "five": ["阿贝多"], - "four": ["菲谢尔", "砂糖", "班尼特"], - "from": "2020-12-23 06:00:00", - "name": "深秘之息", - "to": "2021-01-12 15:59:59", - }, - { - "five": ["钟离"], - "four": ["辛焱", "雷泽", "重云"], - "from": "2020-12-01 18:00:00", - "name": "陵薮市朝", - "to": "2020-12-22 14:59:59", - }, - { - "five": ["达达利亚"], - "four": ["迪奥娜", "北斗", "凝光"], - "from": "2020-11-11 06:00:00", - "name": "暂别冬都", - "to": "2020-12-01 15:59:59", - }, - { - "five": ["可莉"], - "four": ["行秋", "诺艾尔", "砂糖"], - "from": "2020-10-20 18:00:00", - "name": "闪焰的驻足", - "to": "2020-11-10 14:59:59", - }, - { - "five": ["温迪"], - "four": ["芭芭拉", "菲谢尔", "香菱"], - "from": "2020-9-28 06:00:00", - "name": "杯装之诗", - "to": "2020-10-18 17:59:59", - }, -] diff --git a/metadata/pool/pool_302.py b/metadata/pool/pool_302.py deleted file mode 100644 index e1a0248..0000000 --- a/metadata/pool/pool_302.py +++ /dev/null @@ -1,464 +0,0 @@ -POOL_302 = [ - { - "five": ["白雨心弦", "静水流涌之辉"], - "four": ["千岩古剑", "匣里龙吟", "西风长枪", "西风秘典", "祭礼弓"], - "name": "神铸赋形", - "from": "2024-06-25 18:00:00", - "to": "2024-07-16 14:59:59", - }, - { - "five": ["赦罪", "裁叶萃光"], - "four": ["千岩长枪", "祭礼剑", "祭礼大剑", "流浪乐章", "绝弦"], - "name": "神铸赋形", - "from": "2024-06-05 06:00:00", - "to": "2024-06-25 17:59:59", - }, - { - "five": ["图莱杜拉的回忆", "碧落之珑"], - "four": ["勘探钻机", "测距规", "西风剑", "雨裁", "祭礼残章"], - "name": "神铸赋形", - "from": "2024-05-14 18:00:00", - "to": "2024-06-04 14:59:59", - }, - { - "five": ["赤月之形", "最初的大魔术"], - "four": ["船坞长剑", "便携动力锯", "匣里灭辰", "昭心", "西风猎弓"], - "name": "神铸赋形", - "from": "2024-04-24 06:00:00", - "to": "2024-05-14 17:59:59", - }, - { - "five": ["万世流涌大典", "苍古自由之誓"], - "four": ["暗巷的酒与诗", "幽夜华尔兹", "笛剑", "西风大剑", "西风长枪"], - "name": "神铸赋形", - "from": "2024-04-02 18:00:00", - "to": "2024-04-23 14:59:59", - }, - { - "five": ["有乐御簾切", "赤角石溃杵"], - "four": ["暗巷闪光", "暗巷猎手", "钟剑", "匣里灭辰", "西风秘典"], - "name": "神铸赋形", - "from": "2024-03-13 06:00:00", - "to": "2024-04-02 17:59:59", - }, - { - "five": ["神乐之真意", "和璞鸢"], - "four": ["千岩古剑", "匣里龙吟", "西风长枪", "流浪乐章", "绝弦"], - "name": "神铸赋形", - "from": "2024-02-20 18:00:00", - "to": "2024-03-12 14:59:59", - }, - { - "five": ["鹤鸣余音", "千夜浮梦"], - "four": ["千岩长枪", "祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓"], - "name": "神铸赋形", - "from": "2024-01-31 06:00:00", - "to": "2024-02-20 17:59:59", - }, - { - "five": ["薙草之稻光", "飞雷之弦振"], - "four": ["断浪长鳍", "西风剑", "雨裁", "昭心", "弓藏"], - "name": "神铸赋形", - "from": "2024-01-09 18:00:00", - "to": "2024-01-30 14:59:59", - }, - { - "five": ["裁断", "雾切之回光"], - "four": ["恶王丸", "曚云之月", "笛剑", "匣里灭辰", "西风秘典"], - "name": "神铸赋形", - "from": "2023-12-20 06:00:00", - "to": "2024-01-09 17:59:59", - }, - { - "five": ["赤沙之杖", "波乱月白经津"], - "four": ["西风猎弓", "流浪乐章", "西风长枪", "西风大剑", "匣里龙吟"], - "name": "神铸赋形", - "from": "2023-11-28 18:00:00", - "to": "2023-12-19 14:59:59", - }, - { - "five": ["静水流涌之辉", "碧落之珑"], - "four": ["祭礼剑", "钟剑", "匣里灭辰", "祭礼残", "绝弦"], - "from": "2023-11-08 06:00:00", - "name": "神铸赋形", - "to": "2023-11-28 17:59:59", - }, - { - "five": ["金流监督", "终末嗟叹之诗"], - "four": ["勘探钻机", "测距规", "西风剑", "雨裁", "昭心"], - "from": "2023-10-17 18:00:00", - "name": "神铸赋形", - "to": "2023-11-07 14:59:59", - }, - { - "five": ["万世流涌大典", "护摩之杖"], - "four": ["船坞长剑", "便携动力锯", "幽夜华尔兹", "西风长枪", "西风秘典"], - "from": "2023-09-27 06:00:00", - "name": "神铸赋形", - "to": "2023-10-17 17:59:59", - }, - { - "five": ["贯虹之槊", "冬极白星"], - "four": ["笛剑", "祭礼大剑", "匣里灭辰", "流浪乐章", "弓藏"], - "from": "2023-09-05 18:00:00", - "name": "神铸赋形", - "to": "2023-09-26 14:59:59", - }, - { - "five": ["最初的大魔术", "若水"], - "four": ["祭礼剑", "西风大剑", "西风长枪", "祭礼残章", "祭礼弓"], - "from": "2023-08-16 06:00:00", - "name": "神铸赋形", - "to": "2023-09-05 17:59:59", - }, - { - "five": ["不灭月华", "图莱杜拉的回忆"], - "four": ["暗巷的酒与诗", "匣里龙吟", "钟剑", "匣里灭辰", "西风猎弓"], - "from": "2023-07-25 18:00:00", - "name": "神铸赋形", - "to": "2023-08-15 14:59:59", - }, - { - "five": ["松籁响起之时", "四风原典"], - "four": ["暗巷闪光", "暗巷猎手", "雨裁", "西风长枪", "昭心"], - "from": "2023-07-05 06:00:00", - "name": "神铸赋形", - "to": "2023-07-25 17:59:59", - }, - { - "five": ["裁叶萃光", "苍古自由之誓"], - "four": ["断浪长鳍", "曚云之月", "西风剑", "祭礼大剑", "西风秘典"], - "from": "2023-06-13 18:00:00", - "name": "神铸赋形", - "to": "2023-07-04 14:59:59", - }, - { - "five": ["飞雷之弦振", "神乐之真意"], - "four": ["恶王丸", "笛剑", "匣里灭辰", "流浪乐章", "弓藏"], - "from": "2023-05-24 06:00:00", - "name": "神铸赋形", - "to": "2023-06-13 17:59:59", - }, - { - "five": ["碧落之珑", "阿莫斯之弓"], - "four": ["玛海菈的水色", "流浪的晚星", "匣里龙吟", "西风长枪", "祭礼弓"], - "from": "2023-05-02 18:00:00", - "name": "神铸赋形", - "to": "2023-05-23 14:59:59", - }, - { - "five": ["千夜浮梦", "圣显之钥"], - "four": ["西福斯的月光", "西风大剑", "匣里灭辰", "祭礼残章", "绝弦"], - "from": "2023-04-12 06:00:00", - "name": "神铸赋形", - "to": "2023-05-02 17:59:59", - }, - { - "five": ["息灾", "雾切之回光"], - "four": ["暗巷的酒与诗", "祭礼剑", "钟剑", "西风长枪", "西风猎弓"], - "from": "2023-03-21 18:00:00", - "name": "神铸赋形", - "to": "2023-04-11 14:59:59", - }, - { - "five": ["苇海信标", "赤沙之杖"], - "four": ["暗巷闪光", "暗巷猎手", "祭礼大剑", "匣里灭辰", "昭心"], - "from": "2023-03-01 06:00:00", - "name": "神铸赋形", - "to": "2023-03-21 17:59:59", - }, - { - "five": ["护摩之杖", "若水"], - "four": ["千岩古剑", "西风剑", "匣里灭辰", "西风秘典", "弓藏"], - "from": "2023-02-07 18:00:00", - "name": "神铸赋形", - "to": "2023-02-28 14:59:59", - }, - { - "five": ["裁叶萃光", "和璞鸢"], - "four": ["千岩长枪", "笛剑", "雨裁", "流浪乐章", "祭礼弓"], - "from": "2023-01-18 06:00:00", - "name": "神铸赋形", - "to": "2023-02-07 17:59:59", - }, - { - "five": ["薙草之稻光", "波乱月白经津"], - "four": ["恶王丸", "曚云之月", "匣里龙吟", "西风长枪", "祭礼残章"], - "from": "2022-12-27 18:00:00", - "name": "神铸赋形", - "to": "2023-01-17 14:59:59", - }, - { - "five": ["图莱杜拉的回忆", "赤角石溃杵"], - "four": ["祭礼剑", "西风大剑", "断浪长鳍", "昭心", "西风猎弓"], - "from": "2022-12-07 06:00:00", - "name": "神铸赋形", - "to": "2022-12-27 17:59:59", - }, - { - "five": ["神乐之真意", "冬极白星"], - "four": ["西风剑", "钟剑", "匣里灭辰", "西风秘典", "绝弦"], - "from": "2022-11-18 18:00:00", - "name": "神铸赋形", - "to": "2022-12-06 14:59:59", - }, - { - "five": ["千夜浮梦", "飞雷之弦振"], - "four": ["笛剑", "祭礼大剑", "西风长枪", "流浪乐章", "弓藏"], - "from": "2022-11-2 06:00:00", - "name": "神铸赋形", - "to": "2022-11-18 17:59:59", - }, - { - "five": ["圣显之钥", "磐岩结绿"], - "four": ["西福斯的月光", "雨裁", "匣里灭辰", "流浪的晚星", "祭礼弓"], - "from": "2022-10-14 18:00:00", - "name": "神铸赋形", - "to": "2022-11-01 14:59:59", - }, - { - "five": ["赤沙之杖", "终末嗟叹之诗"], - "four": ["匣里龙吟", "玛海菈的水色", "西风长枪", "祭礼残章", "西风猎弓"], - "from": "2022-09-28 06:00:00", - "name": "神铸赋形", - "to": "2022-10-14 17:59:59", - }, - { - "five": ["阿莫斯之弓", "不灭月华"], - "four": ["祭礼剑", "西风大剑", "匣里灭辰", "昭心", "弓藏"], - "from": "2022-09-09 18:00:00", - "name": "神铸赋形", - "to": "2022-09-27 14:59:59", - }, - { - "five": ["猎人之径", "贯虹之槊"], - "four": ["西风剑", "钟剑", "西风长枪", "西风秘典", "绝弦"], - "from": "2022-08-24 06:00:00", - "name": "神铸赋形", - "to": "2022-09-09 17:59:59", - }, - { - "five": ["飞雷之弦振", "斫峰之刃"], - "four": ["暗巷的酒与诗", "暗巷猎手", "笛剑", "祭礼大剑", "匣里灭辰"], - "from": "2022-08-02 18:00:00", - "name": "神铸赋形", - "to": "2022-08-23 14:59:59", - }, - { - "five": ["苍古自由之誓", "四风原典"], - "four": ["千岩古剑", "匣里龙吟", "匣里灭辰", "祭礼残章", "绝弦"], - "from": "2022-07-13 06:00:00", - "name": "神铸赋形", - "to": "2022-08-02 17:59:59", - }, - { - "five": ["赤角石溃杵", "尘世之锁"], - "four": ["千岩古剑", "匣里龙吟", "匣里灭辰", "祭礼残章", "绝弦"], - "from": "2022-06-21 18:00:00", - "name": "神铸赋形", - "to": "2022-07-12 14:59:59", - }, - { - "five": ["若水", "和璞鸢"], - "four": ["千岩长枪", "祭礼剑", "西风大剑", "昭心", "祭礼弓"], - "from": "2022-05-31 06:00:00", - "name": "神铸赋形", - "to": "2022-06-21 17:59:59", - }, - { - "five": ["雾切之回光", "无工之剑"], - "four": ["西风剑", "钟剑", "西风长枪", "西风秘典", "西风猎弓"], - "from": "2022-04-19 17:59:59", - "name": "神铸赋形", - "to": "2022-05-31 05:59:59", - }, - { - "five": ["波乱月白经津", "终末嗟叹之诗"], - "four": ["弓藏", "笛剑", "流浪乐章", "匣里灭辰", "祭礼大剑"], - "from": "2022-03-30 06:00:00", - "name": "神铸赋形", - "to": "2022-04-19 17:59:59", - }, - { - "five": ["薙草之稻光", "不灭月华"], - "four": ["恶王丸", "曚云之月", "匣里龙吟", "西风长枪", "祭礼残章"], - "from": "2022-03-08 18:00:00", - "name": "神铸赋形", - "to": "2022-03-29 14:59:59", - }, - { - "five": ["神乐之真意", "磐岩结绿"], - "four": ["祭礼剑", "雨裁", "断浪长鳍", "昭心", "绝弦"], - "from": "2022-02-16 06:00:00", - "name": "神铸赋形", - "to": "2022-03-08 17:59:59", - }, - { - "five": ["贯虹之槊", "阿莫斯之弓"], - "four": ["西风剑", "千岩古剑", "匣里灭辰", "西风秘典", "祭礼弓"], - "from": "2022-01-25 18:00:00", - "name": "神铸赋形", - "to": "2022-02-15 14:59:59", - }, - { - "five": ["息灾", "和璞鸢"], - "four": ["笛剑", "西风大剑", "千岩长枪", "流浪乐章", "西风猎弓"], - "from": "2022-01-05 06:00:00", - "name": "神铸赋形", - "to": "2022-01-25 17:59:59", - }, - { - "five": ["赤角石溃杵", "天空之翼"], - "four": ["暗巷闪光", "钟剑", "西风长枪", "祭礼残章", "幽夜华尔兹"], - "from": "2021-12-14 18:00:00", - "name": "神铸赋形", - "to": "2022-01-04 14:59:59", - }, - { - "five": ["苍古自由之誓", "松籁响起之时"], - "four": ["匣里龙吟", "祭礼大剑", "匣里灭辰", "暗巷的酒与诗", "暗巷猎手"], - "from": "2021-11-24 06:00:00", - "name": "神铸赋形", - "to": "2021-12-14 17:59:59", - }, - { - "five": ["护摩之杖", "终末嗟叹之诗"], - "four": ["祭礼剑", "雨裁", "断浪长鳍", "流浪乐章", "曚云之月"], - "from": "2021-11-02 18:00:00", - "name": "神铸赋形", - "to": "2021-11-23 14:59:59", - }, - { - "five": ["冬极白星", "尘世之锁"], - "four": ["西风剑", "恶王丸", "西风长枪", "昭心", "弓藏"], - "from": "2021-10-13 06:00:00", - "name": "神铸赋形", - "to": "2021-11-02 17:59:59", - }, - { - "five": ["不灭月华", "磐岩结绿"], - "four": ["笛剑", "西风大剑", "匣里灭辰", "西风秘典", "绝弦"], - "from": "2021-09-21 18:00:00", - "name": "神铸赋形", - "to": "2021-10-12 14:59:59", - }, - { - "five": ["薙草之稻光", "无工之剑"], - "four": ["匣里龙吟", "钟剑", "西风长枪", "流浪乐章", "祭礼弓"], - "from": "2021-09-01 06:00:00", - "name": "神铸赋形", - "to": "2021-09-21 17:59:59", - }, - { - "five": ["飞雷之弦振", "天空之刃"], - "four": ["祭礼剑", "雨裁", "匣里灭辰", "祭礼残章", "西风猎弓"], - "from": "2021-08-10 18:00:00", - "name": "神铸赋形", - "to": "2021-08-31 14:59:59", - }, - { - "five": ["雾切之回光", "天空之脊"], - "four": ["西风剑", "祭礼大剑", "西风长枪", "西风秘典", "绝弦"], - "from": "2021-07-21 06:00:00", - "name": "神铸赋形", - "to": "2021-08-10 17:59:59", - }, - { - "five": ["苍古自由之誓", "天空之卷"], - "four": ["暗巷闪光", "西风大剑", "匣里灭辰", "暗巷的酒与诗", "暗巷猎手"], - "from": "2021-06-29 18:00:00", - "name": "神铸赋形", - "to": "2021-07-20 14:59:59", - }, - { - "five": ["天空之傲", "四风原典"], - "four": ["匣里龙吟", "钟剑", "西风长枪", "流浪乐章", "幽夜华尔兹"], - "from": "2021-06-09 06:00:00", - "name": "神铸赋形", - "to": "2021-06-29 17:59:59", - }, - { - "five": ["松籁响起之时", "风鹰剑"], - "four": ["祭礼剑", "雨裁", "匣里灭辰", "祭礼残章", "弓藏"], - "from": "2021-05-18 18:00:00", - "name": "神铸赋形", - "to": "2021-06-08 14:59:59", - }, - { - "five": ["斫峰之刃", "尘世之锁"], - "four": ["笛剑", "千岩古剑", "祭礼弓", "昭心", "千岩长枪"], - "from": "2021-04-28 06:00:00", - "name": "神铸赋形", - "to": "2021-05-18 17:59:59", - }, - { - "five": ["天空之翼", "四风原典"], - "four": ["西风剑", "祭礼大剑", "暗巷猎手", "西风秘典", "西风长枪"], - "from": "2021-04-06 18:00:00", - "name": "神铸赋形", - "to": "2021-04-27 14:59:59", - }, - { - "five": ["终末嗟叹之诗", "天空之刃"], - "four": ["暗巷闪光", "西风大剑", "西风猎弓", "暗巷的酒与诗", "匣里灭辰"], - "from": "2021-03-17 06:00:00", - "name": "神铸赋形", - "to": "2021-04-06 15:59:59", - }, - { - "five": ["护摩之杖", "狼的末路"], - "four": ["匣里龙吟", "千岩古剑", "祭礼弓", "流浪乐章", "千岩长枪"], - "from": "2021-02-23 18:00:00", - "name": "神铸赋形", - "to": "2021-03-16 14:59:59", - }, - { - "five": ["磐岩结绿", "和璞鸢"], - "four": ["笛剑", "祭礼大剑", "弓藏", "昭心", "西风长枪"], - "from": "2021-02-03 06:00:00", - "name": "神铸赋形", - "to": "2021-02-23 15:59:59", - }, - { - "five": ["阿莫斯之弓", "天空之傲"], - "four": ["祭礼剑", "钟剑", "匣里灭辰", "昭心", "西风猎弓"], - "from": "2021-01-12 18:00:00", - "name": "神铸赋形", - "to": "2021-02-02 14:59:59", - }, - { - "five": ["斫峰之刃", "天空之卷"], - "four": ["西风剑", "西风大剑", "西风长枪", "祭礼残章", "绝弦"], - "from": "2020-12-23 06:00:00", - "name": "神铸赋形", - "to": "2021-01-12 15:59:59", - }, - { - "five": ["贯虹之槊", "无工之剑"], - "four": ["匣里龙吟", "钟剑", "西风秘典", "西风猎弓", "匣里灭辰"], - "from": "2020-12-01 18:00:00", - "name": "神铸赋形", - "to": "2020-12-22 14:59:59", - }, - { - "five": ["天空之翼", "尘世之锁"], - "four": ["笛剑", "雨裁", "昭心", "弓藏", "西风长枪"], - "from": "2020-11-11 06:00:00", - "name": "神铸赋形", - "to": "2020-12-01 15:59:59", - }, - { - "five": ["四风原典", "狼的末路"], - "four": ["祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓", "匣里灭辰"], - "from": "2020-10-20 18:00:00", - "name": "神铸赋形", - "to": "2020-11-10 14:59:59", - }, - { - "five": ["风鹰剑", "阿莫斯之弓"], - "four": ["祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓", "匣里灭辰"], - "from": "2020-09-28 06:00:00", - "name": "神铸赋形", - "to": "2020-10-18 17:59:59", - }, -] diff --git a/metadata/pool/pool_5.py b/metadata/pool/pool_5.py new file mode 100644 index 0000000..a3c68fc --- /dev/null +++ b/metadata/pool/pool_5.py @@ -0,0 +1,3 @@ +POOL_5 = [ + {"five": ["卓越搭档"], "four": [], "from": "2024-07-04 06:00:00", "name": "卓越搭档", "to": "2050-09-15 17:59:59"} +] diff --git a/metadata/pool/pool_500.py b/metadata/pool/pool_500.py deleted file mode 100644 index 2ee7bc1..0000000 --- a/metadata/pool/pool_500.py +++ /dev/null @@ -1,9 +0,0 @@ -POOL_500 = [ - { - "five": ["晨风之诗"], - "four": [], - "name": "晨风之诗", - "from": "2024-03-13 06:00:00", - "to": "2024-04-02 17:59:59", - }, -] diff --git a/metadata/shortname.py b/metadata/shortname.py index a92fe7b..bddc315 100644 --- a/metadata/shortname.py +++ b/metadata/shortname.py @@ -3,939 +3,106 @@ from __future__ import annotations import functools from typing import List -from metadata.genshin import WEAPON_DATA - -__all__ = [ - "roles", - "weapons", - "idToName", - "roleToId", - "roleToName", - "weaponToName", - "weaponToId", - "elementToName", - "elementsToColor", - "not_real_roles", - "roleToTag", -] - # noinspection SpellCheckingInspection roles = { - 20000000: [ - "旅行者", - "主角", - "卑鄙的外乡人", - "荣誉骑士", - "爷", - "履刑者", - "人之子", - "命定之人", - "荣誉骑士", - "小可爱", # 丽莎 - "小家伙", # 八重神子 - "金发异乡人", - "大黄金钓鱼手", # 派蒙 - "黄毛阿姨", - "黄毛叔叔", - "大黄倭瓜那菈", - ], - 10000002: [ - "神里绫华", - "ayaka", - "kamisato ayaka", - "神里", - "绫华", - "神里凌华", - "凌华", - "白鹭公主", - "神里大小姐", - "冰骗骗花", - "龟龟", - ], - 10000003: ["琴", "jean", "团长", "代理团长", "琴团长", "蒲公英骑士", "蒙德砍王", "骑士团的魂"], - 10000005: ["空", "aether", "男主", "男主角", "龙哥", "空哥", "王子"], - 10000006: ["丽莎", "lisa", "图书管理员", "图书馆管理员", "蔷薇魔女"], - 10000007: ["荧", "lumine", "女主", "女主角", "莹", "萤", "黄毛阿姨", "荧妹", "公主殿下"], - 10000014: ["芭芭拉", "barbara", "巴巴拉", "拉粑粑", "拉巴巴", "内鬼", "加湿器", "闪耀偶像", "偶像", "蒙德辣王"], - 10000015: [ - "凯亚", - "kaeya", - "盖亚", - "凯子哥", - "凯鸭", - "矿工", - "矿工头子", - "骑兵队长", - "凯子", - "凝冰渡海真君", - "花脸猫", - ], - 10000016: [ - "迪卢克", - "diluc", - "卢姥爷", - "姥爷", - "卢老爷", - "卢锅巴", - "正义人", - "正e人", - "正E人", - "卢本伟", - "暗夜英雄", - "卢卢伯爵", - "落魄了", - "落魄了家人们", - "哦哦哦", - "前夫哥", - "在此烧鸟真君", - "E键三连真君", - ], - 10000020: [ - "雷泽", - "razor", - "狼少年", - "狼崽子", - "狼崽", - "卢皮卡", - "小狼", - "小狼狼", - "小狼狗", - "小赛诺", - "替身使者", - "须佐狼乎", - "蒙德砍王", - "炸矿之星", - ], - 10000021: [ - "安柏", - "amber", - "安伯", - "兔兔伯爵", - "飞行冠军", - "侦查骑士", - "侦察骑士", - "点火姬", - "点火机", - "打火机", - "打火姬", - "燃炬焚棘真君", - "初代目提瓦特第一火弓", - ], - 10000022: [ - "温迪", - "venti", - "barbatos", - "温蒂", - "风神", - "卖唱的", - "巴巴托斯", - "巴巴脱丝", - "芭芭托斯", - "芭芭脱丝", - "干点正事", - "不干正事", - "吟游诗人", - "诶嘿", - "唉嘿", - "摸鱼", - "最弱最丢人的七神", - "卖唱的大哥哥", - "巴巴托斯大人", - "欸嘿聚怪真君", - "荻花洲的吹笛人", - "直升机", - ], - 10000023: [ - "香菱", - "xiangling", - "香玲", - "锅巴", - "厨师", - "万民堂厨师", - "香师傅", - "哪吒", - "锅巴发射器", - "无敌风火轮真君", - "舌尖上的璃月", - "提瓦特枪王", - ], - 10000024: ["北斗", "beidou", "大姐头", "大姐", "无冕的龙王", "稻妻人形继电石"], - 10000025: ["行秋", "xingqiu", "秋秋人", "秋妹妹", "书呆子", "飞云商会二少爷", "秋秋人", "6星水神", "枕玉老师"], - 10000026: [ - "魈", - "xiao", - "杏仁豆腐", - "打桩机", - "插秧", - "三眼五显仙人", - "三眼五显真人", - "降魔大圣", - "护法夜叉", - "快乐风男", - "无聊", - "靖妖傩舞", - "矮子仙人", - "三点五尺仙人", - "跳跳虎", - "护法夜叉大将", - "金鹏大将", - "这里无能真君", - "抬头不见低头见真君", - "跳跳虎", - "随叫随到真君", - "成天冷着脸的帅气小哥", - ], - 10000027: ["凝光", "ningguang", "富婆", "天权", "天权星", "寻山见矿真君"], - 10000029: [ - "可莉", - "klee", - "嘟嘟可", - "火花骑士", - "蹦蹦炸弹", - "炸鱼", - "放火烧山", - "放火烧山真君", - "蒙德最强战力", - "逃跑的太阳", - "啦啦啦", - "哒哒哒", - "炸弹人", - "禁闭室", - "艾莉丝的女儿", - "阿贝多的义妹", - "火化骑士", - "炸鱼禁闭真君", - "蒙德小坦克", - "骑士团团宠", - ], - 10000030: [ - "钟离", - "zhongli", - "morax", - "摩拉克斯", - "岩王爷", - "岩神", - "钟师傅", - "天动万象", - "岩王帝君", - "未来可期", - "帝君", - "契约之神", - "社会废人", - "未来可期真君", - "废人养成器", - "听书人", - ], - 10000031: [ - "菲谢尔", - "fischl", - "皇女", - "小艾米", - "小艾咪", - "奥兹", - "断罪皇女", - "中二病", - "中二少女", - "中二皇女", - "奥兹发射器", - ], - 10000032: [ - "班尼特", - "bennett", - "点赞哥", - "点赞", - "倒霉少年", - "倒霉蛋", - "霹雳闪雷真君", - "班神", - "班爷", - "倒霉", - "火神", - "六星真神", - ], - 10000033: [ - "达达利亚", - "tartaglia", - "childe", - "ajax", - "达达鸭", - "达达利鸭", - "公子", - "玩具销售员", - "玩具推销员", - "钱包", - "鸭鸭", - "愚人众末席", - "至冬国驻璃月港玩具推销员主管", - "钟离的钱包", - "近战弓兵", - "在蒙德认识的冒险家", - "永别冬都", - "汤达人", - "大貉妖处理专家", - ], - 10000034: ["诺艾尔", "noelle", "女仆", "高达", "岩王帝姬", "山吹", "冰萤术士", "岩王帝姬"], - 10000035: [ - "七七", - "qiqi", - "僵尸", - "肚饿真君", - "度厄真君", - "77", - "起死回骸童子", - "救苦度厄真君", - "椰羊创始人", - "不卜庐砍王", - "不卜庐剑圣", - ], - 10000036: ["重云", "chongyun", "纯阳之体", "冰棍", "驱邪世家", "大外甥"], - 10000037: ["甘雨", "ganyu", "椰羊", "椰奶", "鸡腿猎人", "咕噜咕噜滚下山真君", "肝雨", "走路上山真君"], - 10000038: [ - "阿贝多", - "albedo", - "可莉哥哥", - "升降机", - "升降台", - "电梯", - "白垩之子", - "贝爷", - "白垩", - "阿贝少", - "花呗多", - "阿贝夕", - "abd", - "阿师傅", - "小王子", - "调查小队队长", - "西风骑士团首席炼金术师", - "白垩老师", - "电梯人", - "蒙德岩神", - "平平无奇", - "蒙德NPC", - ], - 10000039: ["迪奥娜", "diona", "迪欧娜", "dio", "dio娜", "冰猫", "猫猫", "猫娘", "喵喵", "调酒师"], - 10000041: [ - "莫娜", - "mona", - "穷鬼", - "穷光蛋", - "穷", - "莫纳", - "占星术士", - "占星师", - "讨龙真君", - "半部讨龙真君", - "阿斯托洛吉斯·莫娜·梅姬斯图斯", - "astrologist mona megistus", - "梅姬斯图斯", - "梅姬斯图斯卿", - "梅姬", - "半部讨龙真君", - ], - 10000042: [ - "刻晴", - "keqing", - "刻情", - "氪晴", - "刻师傅", - "刻师父", - "牛杂", - "牛杂师傅", - "斩尽牛杂", - "免疫", - "免疫免疫", - "屁斜剑法", - "玉衡星", - "阿晴", - "啊晴", - "得不到的女人", - "金丝虾球真君", - "璃月雷神", - "刻猫猫", - ], - 10000043: ["砂糖", "sucrose", "雷莹术士", "雷萤术士", "雷荧术士"], - 10000044: ["辛焱", "xinyan", "辛炎", "黑妹", "摇滚"], - 10000045: [ - "罗莎莉亚", - "rosaria", - "罗莎莉娅", - "白色史莱姆", - "白史莱姆", - "修女", - "罗莎利亚", - "罗莎利娅", - "罗沙莉亚", - "罗沙莉娅", - "罗沙利亚", - "罗沙利娅", - "萝莎莉亚", - "萝莎莉娅", - "萝莎利亚", - "萝莎利娅", - "萝沙莉亚", - "萝沙莉娅", - "萝沙利亚", - "萝沙利娅", - ], - 10000046: [ - "胡桃", - "hutao", - "hu tao", - "胡淘", - "往生堂堂主", - "火化", - "抬棺的", - "蝴蝶", - "核桃", - "堂主", - "胡堂主", - "雪霁梅香", - "赤团开时", - "黑无常", - "嘘嘘鬼王", - "琪亚娜", - "薪炎之律者", - ], - 10000047: ["枫原万叶", "kazuha", "kaedehara kazuha", "万叶", "叶天帝", "天帝", "人型气象观测台", "浪人武士"], - 10000048: ["烟绯", "yanfei", "烟老师", "律师", "罗翔", "璃月港的知名律法咨询师", "璃月罗翔", "铁人三项真君"], - 10000049: [ - "宵宫", - "yoimiya", - "霄宫", - "烟花", - "肖宫", - "肖工", - "绷带女孩", - "夏祭的女王", - "地对鸽导弹", - "打火姬二代目", - "长野原加特林", - "花见坂军火商", - ], - 10000050: ["托马", "thoma", "家政官", "太郎丸", "地头蛇", "男仆", "男妈妈"], - 10000051: ["优菈", "eula", "优拉", "尤拉", "尤菈", "浪花骑士", "记仇", "喷嚏记仇真君"], - 10000052: [ - "雷电将军", - "shougun", - "raiden shogun", - "raiden", - "ei", - "raiden ei", - "baal", - "雷神", - "将军", - "雷军", - "巴尔", - "阿影", - "影", - "巴尔泽布", - "煮饭婆", - "奶香一刀", - "无想一刀", - "宅女", - "大御所大人", - "鸣神", - "永恒之神", - "姐控", - "不会做饭真君", - "宅女程序员", - "奶香一刀真君", - "雷电芽衣", - "又哭又闹真君", - "御建鸣神主尊大御所大人", - ], - 10000053: ["早柚", "sayu", "小狸猫", "狸猫", "咕噜咕噜赶路真君", "柚岩龙蜥", "善于潜行的矮子", "专业人士"], - 10000054: [ - "珊瑚宫心海", - "kokomi", - "sangonomiya kokomi", - "心海", - "我心", - "你心", - "军师", - "珊瑚宫", - "书记", - "观赏鱼", - "水母", - "鱼", - "现人神巫女", - "宅家派节能军师", - "藤原千花", - "能量管理大师", - "五星观赏鱼", - "海天后", - "深海舌鲆鱼小姐", - ], - 10000055: [ - "五郎", - "gorou", - "柴犬", - "土狗", - "希娜", - "希娜小姐", - "海祇岛的小狗大将", - "修勾", - "五郎大将的朋友", - "小狗勾", - ], - 10000056: [ - "九条裟罗", - "sara", - "kujou sara", - "九条", - "九条沙罗", - "裟罗", - "天狗", - "条家的养子", - "雷系班尼特", - "雷神单推头子", - "珊瑚宫心海的冤家", - "荒泷一斗的冤家", - "外置暴伤", - "维密天使", - ], - 10000057: [ - "荒泷一斗", - "itto", - "arataki itto", - "荒龙一斗", - "荒泷天下第一斗", - "一斗", - "一抖", - "荒泷", - "1斗", - "牛牛", - "斗子哥", - "牛子哥", - "牛子", - "孩子王", - "斗虫", - "巧乐兹", - "放牛的", - "岩丘丘萨满", - "伐伐伐伐伐木工", - "希娜小姐的榜一大哥", - ], - 10000058: [ - "八重神子", - "miko", - "yae miko", - "八重", - "神子", - "狐狸", - "想得美哦", - "巫女", - "屑狐狸", - "骚狐狸", - "八重宫司", - "婶子", - "小八", - "白辰血脉的后裔", - "兼具智慧和美貌的八重神子大人", - "稻妻老八", - "雷丘丘萨满", - "八重樱", - "嗑瓜子", - "小奥兹", - "玲珑油豆腐小姐", - ], - 10000059: [ - "鹿野院平藏", - "heizou", - "shikanoin heizou", - "heizo", - "鹿野苑", - "鹿野院", - "平藏", - "鹿野苑平藏", - "鹿野", - "小鹿", - "天领奉行侦探", - "鹿野奈奈的表弟", - "风拳前锋军", - "拳师", - "名侦探柯南", - "捕快展昭", - ], - 10000060: [ - "夜兰", - "yelan", - "夜阑", - "叶澜", - "腋兰", - "夜天后", - "自称就职于总务司的神秘人士", - "岩上茶室老板", - "夜上海", - "胸怀大痣", - ], - 10000061: ["绮良良", "kirara", "稻妻猫猫", "猫猫快递"], - 10000062: ["埃洛伊", "aloy", "异界的救世主"], - 10000063: ["申鹤", "shenhe", "神鹤", "小姨", "阿鹤", "小姨子", "审鹤", "仙家弟子", "驱邪世家旁", "药材杀手"], - 10000064: ["云堇", "yunjin", "yun jin", "云瑾", "云先生", "云锦", "神女劈观", "岩北斗", "五更琉璃"], - 10000065: [ - "久岐忍", - "kuki", - "kuki shinobu", - "shinobu", - "97忍", - "小忍", - "久歧忍", - "97", - "茄忍", - "阿忍", - "忍姐", - "鬼之副手", - "不是忍者的忍者", - "医疗忍者", - "考证专家", - ], - 10000066: [ - "神里绫人", - "ayato", - "kamisato ayato", - "绫人", - "神里凌人", - "凌人", - "0人", - "神人", - "零人", - "大舅哥", - "神里绫华的兄长", - "荒泷一斗的虫友", - "奥托", - "奥托·阿波卡利斯", - "奥托主教", - "藏镜仕男", - "袖藏奶茶真君", - "真正的甘雨", - "可莉的爷爷", - ], - 10000067: [ - "柯莱", - "collei", - "柯来", - "科莱", - "科来", - "小天使", - "须弥安柏", - "须弥飞行冠军", - "见习巡林员", - "克莱", - "草安伯", - "道成林见习巡林员", - "提纳里的学徒", - "安柏的挚友", - "兰那罗奶奶", - ], - 10000068: [ - "多莉", - "dori", - "多利", - "多力", - "多丽", - "奸商", - "须弥百货商人", - "桑歌玛哈巴依老爷", - "艾尔卡萨扎莱宫之主", - ], - 10000069: [ - "提纳里", - "tighnari", - "小提", - "提那里", - "缇娜里", - "提哪里", - "驴", - "柯莱老师", - "柯莱师傅", - "巡林官", - "提那里", - "耳朵很好摸", - "道成林巡林官", - "柯莱的师父", - ], - 10000070: ["妮露", "nilou", "尼露", "祖拜尔剧场之星", "红牛"], - 10000071: [ - "赛诺", - "cyno", - "赛洛", - "大风纪官", - "大风机关", - "胡狼头大人", - "夹击妹抖", - "游戏王", - "冷笑话爱好者", - "牌佬", - "沙漠死神", - "胡狼", - ], - 10000072: ["坎蒂丝", "candace", "坎迪斯", "水北斗", "赤王后裔", "阿如村守护者"], - 10000073: [ - "纳西妲", - "nahida", - "buer", - "草王", - "草神", - "小吉祥草王", - "草萝莉", - "艹萝莉", - "羽毛球", - "布耶尔", - "纳西坦", - "摩诃善法大吉祥智慧主", - "智慧之神", - "草木之主", - "草神大人", - ], - 10000074: ["莱依拉", "layla", "拉一拉", "莱伊拉", "莫娜的同行", "西琳", "黑塔"], - 10000075: [ - "流浪者", - "wanderer", - "散兵", - "伞兵", - "伞兵一号", - "雷电国崩", - "国崩", - "卢本伟", - "雷电大炮", - "雷大炮", - "大炮", - "sb", - "斯卡拉姆齐", - "倾奇者", - "黑主", - "崩崩小圆帽", - "七叶寂照秘密主", - "七彩阳光秘密主", - "正机之神", - "伪神", - "阿帽", - ], - 10000076: [ - "珐露珊", - "faruzan", - "法露珊", - "珐妹", - "初音", - "初音未来", - "miku", - "发露姗", - "发姐", - "法姐", - "百岁珊", - "百岁山", - "童姥", - "知论派名宿", - ], - 10000077: ["瑶瑶", "yaoyao", "遥遥", "遥遥无期", "香菱师妹", "萝卜", "四星草奶"], - 10000078: [ - "艾尔海森", - "alhaitham", - "爱尔海森", - "艾尔海参", - "艾尔", - "海森", - "海参", - "海神", - "埃尔海森", - "草刻晴", - "书记官", - "代理大贤者", - ], - 10000079: ["迪希雅", "dehya", "狮女", "狮子", "腕豪", "女拳"], - 10000080: ["米卡", "mika", "镜音连", "咪卡", "小米"], - 10000081: ["卡维", "kaveh", "夺少"], - 10000082: ["白术", "baizhuer", "白大夫", "草行秋"], - 10000083: ["琳妮特", "lynette", "登登", "锵锵", "林尼特"], - 10000084: ["林尼", "lyney", "大魔术师", "琳尼"], - 10000085: ["菲米尼", "freminet", "潜水员"], - 10000086: ["莱欧斯利", "wriothesley", "典狱长", "大狼狗", "莱欧斯利公爵", "公爵", "公爵大人"], - 10000087: ["那维莱特", "neuvillette", "水龙", "龙王", "水龙王", "那维", "大审判官"], - 10000088: ["夏洛蒂", "charlotte", "记者", "枫丹记者", "射命丸文", "大新闻", "弄个大新闻"], - 10000089: [ - "芙宁娜", - "furina", - "芙宁娜·德·枫丹", - "芙芙", - "水神", - "芙宁娜大人", - "芙宁娜女士", - "众水的颂诗", - "不休独舞", - "众水、众方、众民与众律法的女王", - ], - 10000090: ["夏沃蕾", "chevreuse"], - 10000091: ["娜维娅", "navia", "黄豆姐"], - 10000092: ["嘉明", "gaming"], - 10000093: ["闲云", "xianyun"], - 10000094: ["千织", "chiori"], - 10000095: ["希格雯", "sigewinne", "护士长", "龙女"], - 10000096: ["阿蕾奇诺", "arlecchino", "仆人", "父亲"], - 10000097: ["赛索斯", "sethos", "金沙行者"], - 10000098: ["克洛琳德", "clorinde"], + 2011: ["哲", "Wise", "wise", "哲"], + 2021: ["铃", "Belle", "belle", "铃"], + 1011: ["安比", "Anby", "anbi", "安比·德玛拉"], + 1021: ["猫又", "Nekomata", "tsubaki", "猫宫 又奈"], + 1031: ["妮可", "Nicole", "nicole", "妮可·德玛拉"], + 1041: ["「11号」", "Soldier 11", "longinus", "「11号」", "11号"], + 1061: ["可琳", "Corin", "corin", "可琳·威克斯"], + 1081: ["比利", "Billy", "billy", "比利·奇德"], + 1091: ["雅", "Miyabi", "unagi", "星见 雅"], + 1101: ["珂蕾妲", "Koleda", "koleda", "珂蕾妲·贝洛伯格"], + 1111: ["安东", "Anton", "anton", "安东·伊万诺夫"], + 1121: ["本", "Ben", "ben", "本·比格"], + 1131: ["苍角", "Soukaku", "aokaku", "苍角"], + 1141: ["莱卡恩", "Lycaon", "lycaon", "冯·莱卡恩"], + 1151: ["露西", "Lucy", "lucy", "露西亚娜·德·蒙特夫"], + 1161: ["莱特", "Lighter", "lighter", "莱特"], + 1181: ["格莉丝", "Grace", "lisa", "格莉丝·霍华德"], + 1191: ["艾莲", "Ellen", "ellen", "艾莲·乔"], + 1201: ["悠真", "Harumasa", "harumasa", "浅羽 悠真"], + 1211: ["丽娜", "Rina", "rina", "亚历山德丽娜·莎芭丝缇安"], + 1221: ["柳", "Yanagi", "yanagi", "月城 柳"], + 1241: ["朱鸢", "Zhu Yuan", "ZhuYuan", "朱鸢"], + 1251: ["青衣", "QingYi", "qingyi", "青衣"], + 1271: ["赛斯", "Seth", "seth", "赛斯·洛威尔"], + 1281: ["派派", "Piper", "clara", "派派·韦尔"], } -not_real_roles = [] +not_real_roles = [1091, 1161, 1201, 1221, 1241, 1251, 1271, 1281] weapons = { - # 1.x - "决斗之枪": ["决斗枪", "决斗", "月卡枪"], - "螭骨剑": ["螭骨", "丈育剑", "离骨剑", "月卡大剑"], - "黑剑": ["月卡剑"], - "苍翠猎弓": ["绿弓", "月卡弓"], - "匣里日月": ["日月"], - "匣里灭辰": ["灭辰"], - "匣里龙吟": ["龙吟"], - "流月针": ["针"], - "流浪乐章": ["赌狗书", "赌狗乐章", "赌狗"], - "昭心": ["糟心"], - "讨龙英杰谭": ["讨龙"], - "神射手之誓": ["脚气弓", "神射手"], - "黑缨枪": ["史莱姆枪"], - "黑岩刺枪": ["黑岩枪"], - "黑岩战弓": ["黑岩弓"], - "天空之刃": ["天空剑"], - "天空之傲": ["天空大剑"], - "天空之脊": ["天空枪", "薄荷枪", "薄荷"], - "天空之卷": ["天空书", "厕纸"], - "天空之翼": ["天空弓"], - "四风原典": ["四风", "可莉专武"], - "阿莫斯之弓": ["阿莫斯", "ams", "痛苦弓", "甘雨专武"], - "狼的末路": ["狼末"], - "和璞鸢": ["鸟枪", "绿枪", "魈专武"], - "风鹰剑": ["风鹰"], - "试作斩岩": ["斩岩"], - "试作星镰": ["星镰"], - "试作金珀": ["金珀"], - "试作古华": ["古华"], - "试作澹月": ["澹月"], - "万国诸海图谱": ["万国", "万国诸海"], - "尘世之锁": ["尘世锁", "尘世", "盾书", "锁"], - "无工之剑": ["蜈蚣", "蜈蚣大剑", "无工大剑", "盾大剑", "无工"], - "贯虹之槊": ["贯虹", "岩枪", "盾枪", "钟离专武"], - "斫峰之刃": ["斫峰", "盾剑"], - "腐殖之剑": ["腐殖", "腐殖剑"], - "雪葬的星银": ["雪葬", "星银", "雪葬星银", "雪山大剑"], - "磐岩结绿": ["绿箭", "绿剑"], - "护摩之杖": ["护摩", "护摩枪", "护膜", "胡桃专武"], - "千岩长枪": ["千岩枪"], - "千岩古剑": ["千岩剑", "千岩大剑"], - "西风长枪": ["西风枪"], - "西风猎弓": ["西风弓"], - "西风秘典": ["西风书"], - "暗巷闪光": ["暗巷剑", "暗巷小剑", "暗巷"], - "暗巷猎手": ["暗巷弓"], - "暗巷的酒与诗": ["暗巷法器", "暗巷书"], - "风花之颂": ["风花弓"], - "终末嗟叹之诗": ["终末", "终末弓", "叹气弓", "乐团弓", "温迪专武"], - "松籁响起之时": ["松籁", "乐团大剑", "松剑", "优菈专武"], - "苍古自由之誓": ["苍古", "乐团剑", "枫原万叶专武"], - "幽夜华尔兹": ["幽夜", "幽夜弓", "华尔兹", "皇女弓"], - "嘟嘟可故事集": ["嘟嘟可"], - # 2.x - "天目影打刀": ["天目刀", "天目"], - "桂木斩长正": ["桂木", "斩长正"], - "喜多院十文字": ["喜多院", "十文字"], - "破魔之弓": ["破魔弓", "破魔"], - "白辰之环": ["白辰", "白辰环"], - "雾切之回光": ["雾切", "神里绫华专武"], - "飞雷之弦振": ["飞雷", "飞雷弓", "宵宫专武"], - "薙草之稻光": ["薙草", "稻光", "薙草稻光", "马尾枪", "马尾", "薙刀", "雷电将军专武"], - "不灭月华": ["月华", "珊瑚宫心海专武"], - "「渔获」": ["鱼叉", "渔叉", "渔获"], - "衔珠海皇": ["海皇", "咸鱼剑", "咸鱼大剑"], - "冬极白星": ["冬极", "达达利亚专武"], - "曚云之月": ["曚云弓", "曚云"], - "恶王丸": ["断浪大剑"], - "断浪长鳍": ["断浪", "断浪长枪", "断浪枪"], - "辰砂之纺锤": ["辰砂", "辰砂纺锤", "纺锤", "阿贝多专武"], - "赤角石溃杵": ["赤角", "石溃杵", "荒泷一斗专武", "巧乐兹"], - "息灾": ["申鹤专武"], - "神乐之真意": ["神乐", "真意", "八重神子专武"], - "证誓之明瞳": ["证誓", "明瞳", "证誓明瞳", "大贝壳"], - "波乱月白经津": ["波乱", "月白", "波乱月白", "经津", "波波津", "神里绫人专武", "钵钵鸡"], - "若水": ["麒麟弓", "夜兰专武"], - "笼钓瓶一心": ["万叶刀", "一心传名刀", "妖刀"], - # 3.x - "猎人之径": ["草弓", "提纳里专武"], - "竭泽": ["鱼弓"], - "原木刀": ["须弥锻造单手剑"], - "森林王器": ["须弥锻造大剑", "原木大剑"], - "贯月矢": ["须弥锻造长枪", "原木枪"], - "盈满之实": ["须弥锻造法器"], - "王下近侍": ["须弥锻造弓", "原木弓"], - "赤沙之杖": ["赤沙", "赛诺专武", "船桨", "洛阳铲"], - "圣显之钥": ["圣显之钥", "圣显", "不灭剑华", "妮露专武", "板砖"], - "风信之锋": ["风信", "风信锋"], - "西福斯的月光": ["西福斯", "月光", "月光小剑", "月光剑"], - "玛海菈的水色": ["玛海菈", "水色"], - "流浪的晚星": ["晚星"], - "千夜浮梦": ["千夜", "神灯", "茶壶", "夜壶"], - "图莱杜拉的回忆": ["图莱杜拉", "铃铛", "流浪者专武"], - "东花坊时雨": ["东花坊", "时雨", "伞"], - "裁叶萃光": ["萃光", "韭菜刀", "裁叶", "菜叶"], - "饰铁之花": ["饰铁", "铁花"], - "苇海信标": ["苇海", "信标"], - "碧落之珑": ["碧落", "白术专武", "不灭绿华"], - # 4.x - "狼牙": ["狼牙"], - "海渊终曲": ["海渊"], - "灰河渡手": ["灰河"], - "聊聊棒": ["聊聊棒"], - "浪影阔剑": ["浪影阔剑"], - "峡湾长歌": ["峡湾长歌"], - "公义的酬报": ["公义的酬报"], - "遗祀玉珑": ["玉珑"], - "纯水流华": ["纯水流华"], - "烈阳之嗣": ["烈阳"], - "静谧之曲": ["静谧之曲"], - "最初的大魔术": ["魔术弓"], - "船坞长剑": ["船坞长剑"], - "便携动力锯": ["动力锯"], - "勘探钻机": ["勘探钻机"], - "无垠蔚蓝之歌": ["无垠蔚蓝之歌"], - "金流监督": ["金流监督"], - "万世流涌大典": ["万世"], - "测距规": ["测距规"], - "水仙十字之剑": ["水仙", "水仙十字剑"], - "静水流涌之辉": ["静水", "净水流涌之辉", "水神专武", "芙芙专武"], - "裁断": ["贯石斧"], - "「究极霸王超级魔剑」": ["霸王剑", "极霸剑", "全海沫村最好的剑"], - "鹤鸣余音": ["余音"], - "有乐御簾切": ["有乐"], - "沙中伟贤的对答": ["伟贤"], - "赤月之形": ["赤月"], - "赦罪": ["赦罪"], - "筑云": ["筑云"], - "白雨心弦": ["心弦"], + 12001: ["「月相」-望"], + 12002: ["「月相」-晦"], + 12003: ["「月相」-朔"], + 12004: ["「残响」-Ⅰ型"], + 12005: ["「残响」-Ⅱ型"], + 12006: ["「残响」-Ⅲ型"], + 12007: ["「湍流」-铳型"], + 12008: ["「湍流」-矢型"], + 12009: ["「湍流」-斧型"], + 12010: ["「电磁暴」-壹式"], + 12011: ["「电磁暴」-贰式"], + 12012: ["「电磁暴」-叁式"], + 12013: ["「恒等式」-本格"], + 12014: ["「恒等式」-变格"], + 13001: ["街头巨星"], + 13002: ["时光切片"], + 13003: ["雨林饕客"], + 13004: ["星徽引擎"], + 13005: ["人为刀俎"], + 13006: ["贵重骨核"], + 13007: ["正版变身器"], + 13008: ["双生泣星"], + 13009: ["触电唇彩"], + 13010: ["兔能环"], + 13011: ["春日融融"], + 13101: ["德玛拉电池Ⅱ型"], + 13103: ["聚宝箱"], + 13106: ["家政员"], + 13108: ["仿制星徽引擎"], + 13111: ["旋钻机-赤轴"], + 13112: ["比格气缸"], + 13113: ["含羞恶面"], + 13115: ["好斗的阿炮"], + 13128: ["轰鸣座驾"], + 14001: ["加农转子"], + 14002: ["逍遥游球"], + 14003: ["左轮转子"], + 14102: ["钢铁肉垫"], + 14104: ["硫磺石"], + 14110: ["燃狱齿轮"], + 14114: ["拘缚者"], + 14118: ["嵌合编译器"], + 14119: ["深海访客"], + 14121: ["啜泣摇篮"], + 14124: ["防暴者Ⅵ型"], } -elements = { - "pyro": ["火"], - "hydro": ["水"], - "anemo": ["风"], - "cryo": ["冰"], - "electro": ["雷"], - "geo": ["岩"], - "dendro": ["草"], - "physical": ["物理"], +buddy = { + 50001: ["伊埃斯"], + 53001: ["企鹅布"], + 53002: ["招财布"], + 53003: ["寻宝布"], + 53004: ["扑击布"], + 53005: ["纸壳布"], + 53006: ["纸袋布"], + 53007: ["泪眼布"], + 53008: ["果核布"], + 53009: ["飞靶布"], + 53010: ["电击布"], + 53011: ["磁力布"], + 53012: ["气压布"], + 54001: ["鲨牙布"], + 54002: ["阿全"], + 54003: ["恶魔布"], + 54004: ["巴特勒"], + 54005: ["艾米莉安"], + 54006: ["飚速布"], + 54008: ["插头布"], + 54009: ["共鸣布"], + 54013: ["左轮布"], } -elementsToColor = { - "anemo": "#65B89A", - "geo": "#F6A824", - "electro": "#9F79B5", - "dendro": "#97C12B", - "hydro": "#3FB6ED", - "pyro": "#E76429", - "cryo": "#8FCDDC", - "physical": "#15161B", -} - - -@functools.lru_cache() -def elementToName(elem: str) -> str | None: - """将元素昵称转为正式名""" - elem = str.casefold(elem) # 忽略大小写 - return elements[elem][0] if elem in elements else None # noinspection PyPep8Naming @@ -956,23 +123,56 @@ def roleToId(name: str) -> int | None: # noinspection PyPep8Naming @functools.lru_cache() -def idToName(cid: int) -> str | None: - """从角色ID获取正式名""" - return roles[cid][0] if cid in roles else None +def idToRole(aid: int) -> str | None: + """获取角色名""" + return roles.get(aid, [None])[0] # noinspection PyPep8Naming @functools.lru_cache() def weaponToName(shortname: str) -> str: """将武器昵称转为正式名""" - return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname) + shortname = str.casefold(shortname) # 忽略大小写 + return next((value[0] for value in weapons.values() for name in value if name == shortname), shortname) # noinspection PyPep8Naming @functools.lru_cache() 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) + new_name = str.casefold(name) + f1 = next((key for key, value in weapons.items() for n in value if n == new_name), None) + return f1 or next((key for key, value in weapons.items() for n in value if n == name), None) + + +# noinspection PyPep8Naming +@functools.lru_cache() +def idToWeapon(wid: int) -> str | None: + """获取武器名""" + return weapons.get(wid, [None])[0] + + +# noinspection PyPep8Naming +@functools.lru_cache() +def buddyToName(shortname: str) -> str: + """将邦布昵称转为正式名""" + shortname = str.casefold(shortname) # 忽略大小写 + return next((value[0] for value in buddy.values() for name in value if name == shortname), shortname) + + +# noinspection PyPep8Naming +@functools.lru_cache() +def buddyToId(name: str) -> int | None: + """获取邦布ID""" + name = str.casefold(name) + return next((key for key, value in buddy.items() for n in value if n == name), None) + + +# noinspection PyPep8Naming +@functools.lru_cache() +def idToBuddy(wid: int) -> str | None: + """获取邦布名""" + return buddy.get(wid, [None])[0] # noinspection PyPep8Naming @@ -981,3 +181,17 @@ def roleToTag(role_name: str) -> List[str]: """通过角色名获取TAG""" role_name = str.casefold(role_name) return next((value for value in roles.values() if value[0] == role_name), [role_name]) + + +@functools.lru_cache() +def weaponToTag(name: str) -> List[str]: + """通过光锥名获取TAG""" + name = str.casefold(name) + return next((value for value in weapons.values() if value[0] == name), [name]) + + +@functools.lru_cache() +def buddyToTag(name: str) -> List[str]: + """通过邦布名获取TAG""" + name = str.casefold(name) + return next((value for value in buddy.values() if value[0] == name), [name]) diff --git a/modules/gacha_log/const.py b/modules/gacha_log/const.py index 4635d14..2c3988c 100644 --- a/modules/gacha_log/const.py +++ b/modules/gacha_log/const.py @@ -1,15 +1,12 @@ -from simnet.models.genshin.wish import BannerType +from simnet.models.zzz.wish import ZZZBannerType -PAIMONMOE_VERSION = 3 -UIGF_VERSION = "v3.0" +ZZZGF_VERSION = "v1.0" GACHA_TYPE_LIST = { - BannerType.NOVICE: "新手祈愿", - BannerType.PERMANENT: "常驻祈愿", - BannerType.WEAPON: "武器祈愿", - BannerType.CHARACTER1: "角色祈愿", - BannerType.CHARACTER2: "角色祈愿", - BannerType.CHRONICLED: "集录祈愿", + ZZZBannerType.STANDARD: "常驻调频", + ZZZBannerType.CHARACTER: "代理人调频", + ZZZBannerType.WEAPON: "音擎调频", + ZZZBannerType.BANGBOO: "邦布调频", } GACHA_TYPE_LIST_REVERSE = {v: k for k, v in GACHA_TYPE_LIST.items()} diff --git a/modules/gacha_log/log.py b/modules/gacha_log/log.py index 3ec93af..dcf523a 100644 --- a/modules/gacha_log/log.py +++ b/modules/gacha_log/log.py @@ -3,20 +3,17 @@ import contextlib import datetime import json from concurrent.futures import ThreadPoolExecutor -from os import PathLike from pathlib import Path -from typing import Dict, IO, List, Optional, Tuple, Union, TYPE_CHECKING +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING import aiofiles -from openpyxl import load_workbook -from simnet import GenshinClient, Region +from simnet import ZZZClient, Region from simnet.errors import AuthkeyTimeout, InvalidAuthkey -from simnet.models.genshin.wish import BannerType -from simnet.utils.player import recognize_genshin_server +from simnet.models.zzz.wish import ZZZBannerType +from simnet.utils.player import recognize_zzz_server from metadata.pool.pool import get_pool_by_id -from metadata.shortname import roleToId, weaponToId -from modules.gacha_log.const import GACHA_TYPE_LIST, PAIMONMOE_VERSION +from modules.gacha_log.const import GACHA_TYPE_LIST from modules.gacha_log.error import ( GachaLogAccountNotFound, GachaLogAuthkeyTimeout, @@ -25,7 +22,6 @@ from modules.gacha_log.error import ( GachaLogInvalidAuthkey, GachaLogMixedProvider, GachaLogNotFound, - PaimonMoeGachaLogFileError, ) from modules.gacha_log.models import ( FiveStarItem, @@ -33,12 +29,10 @@ from modules.gacha_log.models import ( GachaItem, GachaLogInfo, ImportType, - ItemType, Pool, - UIGFGachaType, - UIGFInfo, - UIGFItem, - UIGFModel, + ZZZGFInfo, + ZZZGFItem, + ZZZGFModel, ) from utils.const import PROJECT_ROOT from utils.uid import mask_number @@ -47,7 +41,7 @@ if TYPE_CHECKING: from core.dependence.assets import AssetsService -GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "gacha_log") +GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "signal_log") GACHA_LOG_PATH.mkdir(parents=True, exist_ok=True) @@ -70,11 +64,11 @@ class GachaLog: async def load_history_info( self, user_id: str, uid: str, only_status: bool = False ) -> Tuple[Optional[GachaLogInfo], bool]: - """读取历史抽卡记录数据 + """读取历史调频记录数据 :param user_id: 用户id :param uid: 原神uid :param only_status: 是否只读取状态 - :return: 抽卡记录数据 + :return: 调频记录数据 """ file_path = self.gacha_log_path / f"{user_id}-{uid}.json" if only_status: @@ -87,7 +81,7 @@ class GachaLog: return GachaLogInfo(user_id=user_id, uid=uid, update_time=datetime.datetime.now()), False async def remove_history_info(self, user_id: str, uid: str) -> bool: - """删除历史抽卡记录数据 + """删除历史调频记录数据 :param user_id: 用户id :param uid: 原神uid :return: 是否删除成功 @@ -125,10 +119,10 @@ class GachaLog: return False async def save_gacha_log_info(self, user_id: str, uid: str, info: GachaLogInfo): - """保存抽卡记录数据 + """保存调频记录数据 :param user_id: 用户id :param uid: 玩家uid - :param info: 抽卡记录数据 + :param info: 调频记录数据 """ save_path = self.gacha_log_path / f"{user_id}-{uid}.json" save_path_bak = self.gacha_log_path / f"{user_id}-{uid}.json.bak" @@ -141,29 +135,31 @@ class GachaLog: # 写入数据 await self.save_json(save_path, info.json()) - async def gacha_log_to_uigf(self, user_id: str, uid: str) -> Optional[Path]: - """抽卡日记转换为 UIGF 格式 + async def gacha_log_to_zzzgf(self, user_id: str, uid: str) -> Optional[Path]: + """调频日记转换为 ZZZGF 格式 :param user_id: 用户ID :param uid: 游戏UID - :return: 转换是否成功、转换信息、UIGF文件目录 + :return: 转换是否成功、转换信息、ZZZGF 文件目录 """ data, state = await self.load_history_info(user_id, uid) if not state: raise GachaLogNotFound - save_path = self.gacha_log_path / f"{user_id}-{uid}-uigf.json" - info = UIGFModel(info=UIGFInfo(uid=uid, export_app=ImportType.PaiGram.value, export_app_version="v3"), list=[]) + save_path = self.gacha_log_path / f"{user_id}-{uid}-zzzgf.json" + info = ZZZGFModel( + info=ZZZGFInfo(uid=uid, export_app=ImportType.PaiGram.value, export_app_version="v4"), list=[] + ) for items in data.item_list.values(): for item in items: info.list.append( - UIGFItem( + ZZZGFItem( id=item.id, name=item.name, + gacha_id=item.gacha_id, gacha_type=item.gacha_type, - item_id=roleToId(item.name) if item.item_type == "角色" else weaponToId(item.name), + item_id=item.item_id, item_type=item.item_type, rank_type=item.rank_type, time=item.time.strftime("%Y-%m-%d %H:%M:%S"), - uigf_gacha_type=item.gacha_type if item.gacha_type != "400" else "301", ) ) await self.save_json(save_path, json.loads(info.json())) @@ -178,11 +174,11 @@ class GachaLog: if total > 50: if total <= five_star * 15: raise GachaLogFileError( - "检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。" + "检测到您将要导入的调频记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。" ) if four_star < five_star: raise GachaLogFileError( - "检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。" + "检测到您将要导入的调频记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。" ) return True except Exception as exc: # pylint: disable=W0703 @@ -192,7 +188,7 @@ class GachaLog: def import_data_backend(all_items: List[GachaItem], gacha_log: GachaLogInfo, temp_id_data: Dict) -> int: new_num = 0 for item_info in all_items: - pool_name = GACHA_TYPE_LIST[BannerType(int(item_info.gacha_type))] + pool_name = GACHA_TYPE_LIST[ZZZBannerType(int(item_info.gacha_type))] if pool_name not in temp_id_data: temp_id_data[pool_name] = [] if pool_name not in gacha_log.item_list: @@ -219,11 +215,6 @@ class GachaLog: all_items = [GachaItem(**i) for i in data["list"]] await self.verify_data(all_items) gacha_log, status = await self.load_history_info(str(user_id), uid) - if import_type == ImportType.PAIMONMOE: - if status and gacha_log.get_import_type != ImportType.PAIMONMOE: - raise GachaLogMixedProvider - elif status and gacha_log.get_import_type == ImportType.PAIMONMOE: - raise GachaLogMixedProvider # 将唯一 id 放入临时数据中,加快查找速度 temp_id_data = { pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items() @@ -244,20 +235,21 @@ class GachaLog: await self.save_gacha_log_info(str(user_id), uid, gacha_log) return new_num except GachaLogAccountNotFound as e: - raise GachaLogAccountNotFound("导入失败,文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同") from e + raise GachaLogAccountNotFound("导入失败,文件包含的调频记录所属 uid 与你当前绑定的 uid 不同") from e except GachaLogMixedProvider as e: raise GachaLogMixedProvider from e except Exception as exc: raise GachaLogException from exc @staticmethod - def get_game_client(player_id: int) -> GenshinClient: - if recognize_genshin_server(player_id) in ["cn_gf01", "cn_qd01"]: - return GenshinClient(player_id=player_id, region=Region.CHINESE, lang="zh-cn") - return GenshinClient(player_id=player_id, region=Region.OVERSEAS, lang="zh-cn") + def get_game_client(player_id: int) -> ZZZClient: + if recognize_zzz_server(player_id) in ["prod_gf_cn"]: + return ZZZClient(player_id=player_id, region=Region.CHINESE, lang="zh-cn") + else: + return ZZZClient(player_id=player_id, region=Region.OVERSEAS, lang="zh-cn") async def get_gacha_log_data(self, user_id: int, player_id: int, authkey: str) -> int: - """使用authkey获取抽卡记录数据,并合并旧数据 + """使用authkey获取调频记录数据,并合并旧数据 :param user_id: 用户id :param player_id: 玩家id :param authkey: authkey @@ -265,10 +257,8 @@ class GachaLog: """ new_num = 0 gacha_log, _ = await self.load_history_info(str(user_id), str(player_id)) - if gacha_log.get_import_type == ImportType.PAIMONMOE: - raise GachaLogMixedProvider # 将唯一 id 放入临时数据中,加快查找速度 - temp_id_data = {pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items()} + temp_id_data = {pool_name: {i.id: i for i in pool_data} for pool_name, pool_data in gacha_log.item_list.items()} client = self.get_game_client(player_id) try: for pool_id, pool_name in GACHA_TYPE_LIST.items(): @@ -277,7 +267,9 @@ class GachaLog: item = GachaItem( id=str(data.id), name=data.name, + gacha_id=str(data.banner_id), gacha_type=str(data.banner_type.value), + item_id=str(data.item_id), item_type=data.type, rank_type=str(data.rarity), time=datetime.datetime( @@ -291,13 +283,17 @@ class GachaLog: ) if pool_name not in temp_id_data: - temp_id_data[pool_name] = [] + temp_id_data[pool_name] = {} if pool_name not in gacha_log.item_list: gacha_log.item_list[pool_name] = [] - if item.id not in temp_id_data[pool_name]: + if item.id not in temp_id_data[pool_name].keys(): gacha_log.item_list[pool_name].append(item) - temp_id_data[pool_name].append(item.id) + temp_id_data[pool_name][item.id] = item new_num += 1 + else: + old_item: GachaItem = temp_id_data[pool_name][item.id] + old_item.gacha_id = item.gacha_id + old_item.item_id = item.item_id except AuthkeyTimeout as exc: raise GachaLogAuthkeyTimeout from exc except InvalidAuthkey as exc: @@ -307,56 +303,64 @@ class GachaLog: for i in gacha_log.item_list.values(): i.sort(key=lambda x: (x.time, x.id)) gacha_log.update_time = datetime.datetime.now() - gacha_log.import_type = ImportType.UIGF.value + gacha_log.import_type = ImportType.PaiGram.value await self.save_gacha_log_info(str(user_id), str(player_id), gacha_log) return new_num @staticmethod def check_avatar_up(name: str, gacha_time: datetime.datetime) -> bool: - if name in {"莫娜", "七七", "迪卢克", "琴", "迪希雅"}: + if name in {"莱卡恩", "猫又", "格莉丝", "丽娜", "「11号」", "珂蕾妲"}: return False - if name == "刻晴": - start_time = datetime.datetime.strptime("2021-02-17 18:00:00", "%Y-%m-%d %H:%M:%S") - end_time = datetime.datetime.strptime("2021-03-02 15:59:59", "%Y-%m-%d %H:%M:%S") - if not start_time < gacha_time < end_time: - return False - elif name == "提纳里": - start_time = datetime.datetime.strptime("2022-08-24 06:00:00", "%Y-%m-%d %H:%M:%S") - end_time = datetime.datetime.strptime("2022-09-09 17:59:59", "%Y-%m-%d %H:%M:%S") - if not start_time < gacha_time < end_time: - return False return True - async def get_all_5_star_items(self, data: List[GachaItem], assets: "AssetsService", pool_name: str = "角色祈愿"): + async def get_all_5_star_items(self, data: List[GachaItem], assets: "AssetsService", pool_name: str = "代理人调频"): """ - 获取所有5星角色 - :param data: 抽卡记录 + 获取所有5星代理人 + :param data: 调频记录 :param assets: 资源服务 :param pool_name: 池子名称 - :return: 5星角色列表 + :return: 5星代理人列表 """ count = 0 result = [] for item in data: count += 1 if item.rank_type == "5": - if item.item_type == "角色" and pool_name in {"角色祈愿", "常驻祈愿", "新手祈愿", "集录祈愿"}: + if item.item_type == "代理人" and pool_name in {"代理人调频", "常驻调频"}: + if pool_name == "代理人调频": + isUp, isBig = ( + self.check_avatar_up(item.name, item.time), + (not result[-1].isUp) if result else False, + ) + else: + isUp, isBig = False, False data = { "name": item.name, - "icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(), + "icon": assets.avatar.normal(item.name).as_uri(), "count": count, - "type": "角色", - "isUp": self.check_avatar_up(item.name, item.time) if pool_name == "角色祈愿" else False, - "isBig": (not result[-1].isUp) if result and pool_name == "角色祈愿" else False, + "type": "代理人", + "isUp": isUp, + "isBig": isBig, "time": item.time, } result.append(FiveStarItem.construct(**data)) - elif item.item_type == "武器" and pool_name in {"武器祈愿", "常驻祈愿", "新手祈愿", "集录祈愿"}: + elif item.item_type == "音擎" and pool_name in {"音擎调频", "常驻调频"}: data = { "name": item.name, - "icon": (await assets.weapon(weaponToId(item.name)).icon()).as_uri(), + "icon": assets.weapon.icon(item.name).as_uri(), "count": count, - "type": "武器", + "type": "音擎", + "isUp": False, + "isBig": False, + "time": item.time, + } + result.append(FiveStarItem.construct(**data)) + elif item.item_type == "邦布" and pool_name in {"邦布调频"}: + data = { + "name": item.name, + "icon": assets.buddy.icon(item.name).as_uri(), + "count": count, + "type": "邦布", "isUp": False, "isBig": False, "time": item.time, @@ -370,7 +374,7 @@ class GachaLog: async def get_all_4_star_items(data: List[GachaItem], assets: "AssetsService"): """ 获取 no_fout_star - :param data: 抽卡记录 + :param data: 调频记录 :param assets: 资源服务 :return: no_fout_star """ @@ -379,21 +383,30 @@ class GachaLog: for item in data: count += 1 if item.rank_type == "4": - if item.item_type == "角色": + if item.item_type == "代理人": data = { "name": item.name, - "icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(), + "icon": assets.avatar.normal(item.name).as_uri(), "count": count, - "type": "角色", + "type": "代理人", "time": item.time, } result.append(FourStarItem.construct(**data)) - elif item.item_type == "武器": + elif item.item_type == "音擎": data = { "name": item.name, - "icon": (await assets.weapon(weaponToId(item.name)).icon()).as_uri(), + "icon": assets.weapon.icon(item.name).as_uri(), "count": count, - "type": "武器", + "type": "音擎", + "time": item.time, + } + result.append(FourStarItem.construct(**data)) + elif item.item_type == "邦布": + data = { + "name": item.name, + "icon": assets.buddy.icon(item.name).as_uri(), + "count": count, + "type": "邦布", "time": item.time, } result.append(FourStarItem.construct(**data)) @@ -402,7 +415,7 @@ class GachaLog: return result, count @staticmethod - def get_301_pool_data(total: int, all_five: List[FiveStarItem], no_five_star: int, no_four_star: int): + def get_2_pool_data(total: int, all_five: List[FiveStarItem], no_five_star: int, no_four_star: int): # 总共五星 five_star = len(all_five) five_star_up = len([i for i in all_five if i.isUp]) @@ -435,20 +448,20 @@ class GachaLog: {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, {"num": five_star_const, "unit": "个", "lable": "五星常驻"}, {"num": up_avg, "unit": "抽", "lable": "UP平均"}, - {"num": up_cost, "unit": "", "lable": "UP花费原石"}, + {"num": up_cost, "unit": "", "lable": "UP花费星琼"}, ], ] @staticmethod - def get_200_pool_data( + def get_1_pool_data( total: int, all_five: List[FiveStarItem], all_four: List[FourStarItem], no_five_star: int, no_four_star: int ): # 总共五星 five_star = len(all_five) # 五星平均 five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0 - # 五星武器 - five_star_weapon = len([i for i in all_five if i.type == "武器"]) + # 五星音擎 + five_star_weapon = len([i for i in all_five if i.type == "音擎"]) # 总共四星 four_star = len(all_four) # 四星平均 @@ -462,7 +475,7 @@ class GachaLog: {"num": no_five_star, "unit": "抽", "lable": "未出五星"}, {"num": five_star, "unit": "个", "lable": "五星"}, {"num": five_star_avg, "unit": "抽", "lable": "五星平均"}, - {"num": five_star_weapon, "unit": "个", "lable": "五星武器"}, + {"num": five_star_weapon, "unit": "个", "lable": "五星音擎"}, {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, {"num": four_star, "unit": "个", "lable": "四星"}, {"num": four_star_avg, "unit": "抽", "lable": "四星平均"}, @@ -471,15 +484,15 @@ class GachaLog: ] @staticmethod - def get_302_pool_data( + def get_3_pool_data( total: int, all_five: List[FiveStarItem], all_four: List[FourStarItem], no_five_star: int, no_four_star: int ): # 总共五星 five_star = len(all_five) # 五星平均 five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0 - # 四星武器 - four_star_weapon = len([i for i in all_four if i.type == "武器"]) + # 四星音擎 + four_star_weapon = len([i for i in all_four if i.type == "音擎"]) # 总共四星 four_star = len(all_four) # 四星平均 @@ -493,38 +506,7 @@ class GachaLog: {"num": no_five_star, "unit": "抽", "lable": "未出五星"}, {"num": five_star, "unit": "个", "lable": "五星"}, {"num": five_star_avg, "unit": "抽", "lable": "五星平均"}, - {"num": four_star_weapon, "unit": "个", "lable": "四星武器"}, - {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, - {"num": four_star, "unit": "个", "lable": "四星"}, - {"num": four_star_avg, "unit": "抽", "lable": "四星平均"}, - {"num": four_star_max_count, "unit": four_star_max, "lable": "四星最多"}, - ], - ] - - @staticmethod - def get_500_pool_data( - total: int, all_five: List[FiveStarItem], all_four: List[FourStarItem], no_five_star: int, no_four_star: int - ): - # 总共五星 - five_star = len(all_five) - # 五星平均 - five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0 - # 四星角色 - four_star_character = len([i for i in all_four if i.type == "角色"]) - # 总共四星 - four_star = len(all_four) - # 四星平均 - four_star_avg = round((total - no_four_star) / four_star, 2) if four_star != 0 else 0 - # 四星最多 - four_star_name_list = [i.name for i in all_four] - four_star_max = max(four_star_name_list, key=four_star_name_list.count) if four_star_name_list else "" - four_star_max_count = four_star_name_list.count(four_star_max) - return [ - [ - {"num": no_five_star, "unit": "抽", "lable": "未出五星"}, - {"num": five_star, "unit": "个", "lable": "五星"}, - {"num": five_star_avg, "unit": "抽", "lable": "五星平均"}, - {"num": four_star_character, "unit": "个", "lable": "四星角色"}, + {"num": four_star_weapon, "unit": "个", "lable": "四星"}, {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, {"num": four_star, "unit": "个", "lable": "四星"}, {"num": four_star_avg, "unit": "抽", "lable": "四星平均"}, @@ -535,7 +517,7 @@ class GachaLog: @staticmethod def count_fortune(pool_name: str, summon_data, weapon: bool = False): """ - 角色 武器 + 代理人 音擎 欧 50以下 45以下 吉 50-60 45-55 中 60-70 55-65 @@ -557,9 +539,9 @@ class GachaLog: return f"{pool_name} · 非" return pool_name - async def get_analysis(self, user_id: int, player_id: int, pool: BannerType, assets: "AssetsService"): + async def get_analysis(self, user_id: int, player_id: int, pool: ZZZBannerType, assets: "AssetsService"): """ - 获取抽卡记录分析数据 + 获取调频记录分析数据 :param user_id: 用户id :param player_id: 玩家id :param pool: 池子类型 @@ -579,17 +561,14 @@ class GachaLog: all_five, no_five_star = await self.get_all_5_star_items(data, assets, pool_name) all_four, no_four_star = await self.get_all_4_star_items(data, assets) summon_data = None - if pool in [BannerType.CHARACTER1, BannerType.CHARACTER2, BannerType.NOVICE]: - summon_data = self.get_301_pool_data(total, all_five, no_five_star, no_four_star) + if pool == ZZZBannerType.CHARACTER: + summon_data = self.get_2_pool_data(total, all_five, no_five_star, no_four_star) pool_name = self.count_fortune(pool_name, summon_data) - elif pool == BannerType.WEAPON: - summon_data = self.get_302_pool_data(total, all_five, all_four, no_five_star, no_four_star) + elif pool in [ZZZBannerType.WEAPON, ZZZBannerType.BANGBOO]: + summon_data = self.get_3_pool_data(total, all_five, all_four, no_five_star, no_four_star) pool_name = self.count_fortune(pool_name, summon_data, True) - elif pool == BannerType.PERMANENT: - summon_data = self.get_200_pool_data(total, all_five, all_four, no_five_star, no_four_star) - pool_name = self.count_fortune(pool_name, summon_data) - elif pool == BannerType.CHRONICLED: - summon_data = self.get_500_pool_data(total, all_five, all_four, no_five_star, no_four_star) + elif pool == ZZZBannerType.STANDARD: + summon_data = self.get_1_pool_data(total, all_five, all_four, no_five_star, no_four_star) pool_name = self.count_fortune(pool_name, summon_data) last_time = data[0].time.strftime("%Y-%m-%d %H:%M") first_time = data[-1].time.strftime("%Y-%m-%d %H:%M") @@ -606,9 +585,9 @@ class GachaLog: } async def get_pool_analysis( - self, user_id: int, player_id: int, pool: BannerType, assets: "AssetsService", group: bool + self, user_id: int, player_id: int, pool: ZZZBannerType, assets: "AssetsService", group: bool ) -> dict: - """获取抽卡记录分析数据 + """获取调频记录分析数据 :param user_id: 用户id :param player_id: 玩家id :param pool: 池子类型 @@ -648,14 +627,14 @@ class GachaLog: ) pool_data = [i for i in pool_data if i["count"] > 0] return { - "uid": player_id, + "uid": mask_number(player_id), "typeName": pool_name, "pool": pool_data[:6] if group else pool_data, "hasMore": len(pool_data) > 6, } async def get_all_five_analysis(self, user_id: int, player_id: int, assets: "AssetsService") -> dict: - """获取五星抽卡记录分析数据 + """获取五星调频记录分析数据 :param user_id: 用户id :param player_id: 玩家id :param assets: 资源服务 @@ -689,133 +668,8 @@ class GachaLog: for up_pool in pools ] return { - "uid": player_id, + "uid": mask_number(player_id), "typeName": "五星列表", "pool": pool_data, "hasMore": False, } - - @staticmethod - def convert_xlsx_to_uigf(file: Union[str, PathLike, IO[bytes]], zh_dict: Dict) -> Dict: - """转换 paimone.moe 或 非小酋 导出 xlsx 数据为 UIGF 格式 - :param file: 导出的 xlsx 文件 - :param zh_dict: - :return: UIGF 格式数据 - """ - - def from_paimon_moe( - uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int - ) -> UIGFItem: - item_type = ItemType.CHARACTER if item_type == "Character" else ItemType.WEAPON - return UIGFItem( - id=str(_id), - name=zh_dict[name], - gacha_type=uigf_gacha_type, - item_type=item_type, - rank_type=str(rank_type), - time=date_string, - uigf_gacha_type=uigf_gacha_type, - ) - - def from_uigf( - uigf_gacha_type: str, - gacha__type: str, - item_type: str, - name: str, - date_string: str, - rank_type: str, - _id: str, - ) -> UIGFItem: - return UIGFItem( - id=_id, - name=name, - gacha_type=gacha__type, - item_type=item_type, - rank_type=rank_type, - time=date_string, - uigf_gacha_type=uigf_gacha_type, - ) - - def from_fxq( - uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int - ) -> UIGFItem: - item_type = ItemType.CHARACTER if item_type == "角色" else ItemType.WEAPON - return UIGFItem( - id=str(_id), - name=name, - gacha_type=uigf_gacha_type, - item_type=item_type, - rank_type=str(rank_type), - time=date_string, - uigf_gacha_type=uigf_gacha_type, - ) - - wb = load_workbook(file) - wb_len = len(wb.worksheets) - - if wb_len == 6: - import_type = ImportType.PAIMONMOE - elif wb_len == 5: - import_type = ImportType.UIGF - elif wb_len == 4: - import_type = ImportType.FXQ - else: - raise GachaLogFileError("xlsx 格式错误") - - paimonmoe_sheets = { - UIGFGachaType.BEGINNER: "Beginners' Wish", - UIGFGachaType.STANDARD: "Standard", - UIGFGachaType.CHARACTER: "Character Event", - UIGFGachaType.WEAPON: "Weapon Event", - } - fxq_sheets = { - UIGFGachaType.BEGINNER: "新手祈愿", - UIGFGachaType.STANDARD: "常驻祈愿", - UIGFGachaType.CHARACTER: "角色活动祈愿", - UIGFGachaType.WEAPON: "武器活动祈愿", - } - data = UIGFModel(info=UIGFInfo(export_app=import_type.value), list=[]) - if import_type == ImportType.PAIMONMOE: - ws = wb["Information"] - if ws["B2"].value != PAIMONMOE_VERSION: - raise PaimonMoeGachaLogFileError(file_version=ws["B2"].value, support_version=PAIMONMOE_VERSION) - count = 1 - for gacha_type in paimonmoe_sheets: - ws = wb[paimonmoe_sheets[gacha_type]] - for row in ws.iter_rows(min_row=2, values_only=True): - if row[0] is None: - break - data.list.append(from_paimon_moe(gacha_type, row[0], row[1], row[2], row[3], count)) - count += 1 - elif import_type == ImportType.UIGF: - ws = wb["原始数据"] - type_map = {} - count = 0 - for row in ws["1"]: - if row.value is None: - break - type_map[row.value] = count - count += 1 - for row in ws.iter_rows(min_row=2, values_only=True): - if row[0] is None: - break - data.list.append( - from_uigf( - row[type_map["uigf_gacha_type"]], - row[type_map["gacha_type"]], - row[type_map["item_type"]], - row[type_map["name"]], - row[type_map["time"]], - row[type_map["rank_type"]], - row[type_map["id"]], - ) - ) - else: - for gacha_type in fxq_sheets: - ws = wb[fxq_sheets[gacha_type]] - for row in ws.iter_rows(min_row=2, values_only=True): - if row[0] is None: - break - data.list.append(from_fxq(gacha_type, row[2], row[1], row[0], row[3], row[6])) - - return json.loads(data.json()) diff --git a/modules/gacha_log/models.py b/modules/gacha_log/models.py index 84b4e97..08cf338 100644 --- a/modules/gacha_log/models.py +++ b/modules/gacha_log/models.py @@ -4,15 +4,13 @@ from typing import Any, Dict, List, Union from pydantic import BaseModel, validator -from metadata.shortname import not_real_roles, roleToId, weaponToId -from modules.gacha_log.const import UIGF_VERSION +from metadata.shortname import not_real_roles, roleToId, weaponToId, buddyToId +from modules.gacha_log.const import ZZZGF_VERSION class ImportType(Enum): PaiGram = "PaiGram" - PAIMONMOE = "PAIMONMOE" - FXQ = "FXQ" - UIGF = "UIGF" + ZZZGF = "ZZZGF" UNKNOWN = "UNKNOWN" @@ -37,27 +35,29 @@ class FourStarItem(BaseModel): class GachaItem(BaseModel): id: str name: str + gacha_id: str = "" gacha_type: str + item_id: str = "" item_type: str rank_type: str time: datetime.datetime @validator("name") def name_validator(cls, v): - if item_id := (roleToId(v) or weaponToId(v)): + if item_id := (roleToId(v) or weaponToId(v) or buddyToId(v)): if item_id not in not_real_roles: return v raise ValueError(f"Invalid name {v}") @validator("gacha_type") def check_gacha_type(cls, v): - if v not in {"100", "200", "301", "302", "400", "500"}: - raise ValueError(f"gacha_type must be 200, 301, 302, 400, 500, invalid value: {v}") + if v not in {"1", "2", "3", "5"}: + raise ValueError(f"gacha_type must be 1, 2, 3 or 5, invalid value: {v}") return v @validator("item_type") def check_item_type(cls, item): - if item not in {"角色", "武器"}: + if item not in {"代理人", "音擎", "邦布"}: raise ValueError(f"error item type {item}") return item @@ -74,11 +74,10 @@ class GachaLogInfo(BaseModel): update_time: datetime.datetime import_type: str = "" item_list: Dict[str, List[GachaItem]] = { - "角色祈愿": [], - "武器祈愿": [], - "常驻祈愿": [], - "新手祈愿": [], - "集录祈愿": [], + "代理人调频": [], + "音擎调频": [], + "常驻调频": [], + "邦布调频": [], } @property @@ -131,57 +130,47 @@ class Pool: class ItemType(Enum): - CHARACTER = "角色" - WEAPON = "武器" + CHARACTER = "代理人" + WEAPON = "音擎" + BANGBOO = "邦布" -class UIGFGachaType(Enum): - BEGINNER = "100" - STANDARD = "200" - CHARACTER = "301" - WEAPON = "302" - CHARACTER2 = "400" - CHRONICLED = "500" +class ZZZGFGachaType(Enum): + STANDARD = "1" + CHARACTER = "2" + WEAPON = "3" + BANGBOO = "5" -class UIGFItem(BaseModel): +class ZZZGFItem(BaseModel): id: str name: str count: str = "1" - gacha_type: UIGFGachaType + gacha_id: str = "" + gacha_type: ZZZGFGachaType item_id: str = "" item_type: ItemType rank_type: str time: str - uigf_gacha_type: UIGFGachaType -class UIGFInfo(BaseModel): +class ZZZGFInfo(BaseModel): uid: str = "0" lang: str = "zh-cn" + region_time_zone: int = 8 export_time: str = "" export_timestamp: int = 0 export_app: str = "" export_app_version: str = "" - uigf_version: str = UIGF_VERSION - region_time_zone: int = 8 + zzzgf_version: str = ZZZGF_VERSION def __init__(self, **data: Any): - data["region_time_zone"] = data.get("region_time_zone", UIGFInfo.get_region_time_zone(data.get("uid", "0"))) super().__init__(**data) if not self.export_time: self.export_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.export_timestamp = int(datetime.datetime.now().timestamp()) - @staticmethod - def get_region_time_zone(uid: str) -> int: - if uid.startswith("6"): - return -5 - if uid.startswith("7"): - return 1 - return 8 - -class UIGFModel(BaseModel): - info: UIGFInfo - list: List[UIGFItem] +class ZZZGFModel(BaseModel): + info: ZZZGFInfo + list: List[ZZZGFItem] diff --git a/modules/wiki/base.py b/modules/wiki/base.py index 133a5a4..7b69a6a 100644 --- a/modules/wiki/base.py +++ b/modules/wiki/base.py @@ -1,254 +1,39 @@ -import asyncio -import re -from abc import abstractmethod -from asyncio import Queue -from multiprocessing import Value -from ssl import SSLZeroReturnError -from typing import AsyncIterator, ClassVar, List, Optional, Tuple, Union +from pathlib import Path +from typing import List, Dict -import anyio -from bs4 import BeautifulSoup -from httpx import URL, AsyncClient, HTTPError, Response -from pydantic import BaseConfig as PydanticBaseConfig -from pydantic import BaseModel as PydanticBaseModel - -from utils.log import logger - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - -__all__ = ["Model", "WikiModel", "HONEY_HOST"] - -HONEY_HOST = URL("https://gensh.honeyhunterworld.com/") +import aiofiles +import ujson as jsonlib +from httpx import AsyncClient -class Model(PydanticBaseModel): - """基类""" +class WikiModel: + BASE_URL = "https://zzz-res.paimon.vip/data/" + BASE_PATH = Path("data/wiki-zzz") + BASE_PATH.mkdir(parents=True, exist_ok=True) - def __new__(cls, *args, **kwargs): - # 让每次new的时候都解析 - cls.update_forward_refs() - return super(Model, cls).__new__(cls) # pylint: disable=E1120 + def __init__(self): + self.client = AsyncClient(timeout=120.0) - class Config(PydanticBaseConfig): - # 使用 ujson 作为解析库 - json_dumps = jsonlib.dumps - json_loads = jsonlib.loads - - -class WikiModel(Model): - # noinspection PyUnresolvedReferences - """wiki所用到的基类 - - Attributes: - id (:obj:`int`): ID - name (:obj:`str`): 名称 - rarity (:obj:`int`): 星级 - - _client (:class:`httpx.AsyncClient`): 发起 http 请求的 client - """ - _client: ClassVar[AsyncClient] = AsyncClient() - - id: str - name: str - rarity: int + async def remote_get(self, url: str): + return await self.client.get(url) @staticmethod - @abstractmethod - def scrape_urls() -> List[URL]: - """爬取的目标网页集合 - - 例如有关武器的页面有: - [单手剑](https://genshin.honeyhunterworld.com/fam_sword/?lang=CHS) - [双手剑](https://genshin.honeyhunterworld.com/fam_claymore/?lang=CHS) - [长柄武器](https://genshin.honeyhunterworld.com/fam_polearm/?lang=CHS) - 。。。 - 这个函数就是返回这些页面的网址所组成的 List - - """ - - @classmethod - async def _client_get(cls, url: Union[URL, str], retry_times: int = 5, sleep: float = 1) -> Response: - """用自己的 client 发起 get 请求的快捷函数 - - Args: - url: 发起请求的 url - retry_times: 发生错误时的重复次数。不能小于 0 . - sleep: 发生错误后等待重试的时间,单位为秒。 - Returns: - 返回对应的请求 - Raises: - 请求所需要的异常 - """ - for _ in range(retry_times): - try: - return await cls._client.get(url, follow_redirects=True) - except (HTTPError, SSLZeroReturnError): - await anyio.sleep(sleep) - return await cls._client.get(url, follow_redirects=True) # 防止 retry_times 等于 0 的时候无法发生请求 - - @classmethod - @abstractmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> "WikiModel": - """解析 soup 生成对应 WikiModel - - Args: - soup: 需要解析的 soup - Returns: - 返回对应的 WikiModel - """ - - @classmethod - async def _scrape(cls, url: Union[URL, str]) -> "WikiModel": - """从 url 中爬取数据,并返回对应的 Model - - Args: - url: 目标 url. 可以为字符串 str , 也可以为 httpx.URL - Returns: - 返回对应的 WikiModel - """ - response = await cls._client_get(url) - return await cls._parse_soup(BeautifulSoup(response.text, "lxml")) - - @classmethod - async def get_by_id(cls, id_: str) -> "WikiModel": - """通过ID获取Model - - Args: - id_: 目标 ID - Returns: - 返回对应的 WikiModel - """ - return await cls._scrape(await cls.get_url_by_id(id_)) - - @classmethod - async def get_by_name(cls, name: str) -> Optional["WikiModel"]: - """通过名称获取Model - - Args: - name: 目标名 - Returns: - 返回对应的 WikiModel - """ - url = await cls.get_url_by_name(name) - return None if url is None else await cls._scrape(url) - - @classmethod - async def get_full_data(cls) -> List["WikiModel"]: - """获取全部数据的 Model - - Returns: - 返回能爬到的所有的 Model 所组成的 List - """ - return [i async for i in cls.full_data_generator()] - - @classmethod - async def full_data_generator(cls) -> AsyncIterator["WikiModel"]: - """Model 生成器 - - 这是一个异步生成器,该函数在使用时会爬取所有数据,并将其转为对应的 Model,然后存至一个队列中 - 当有需要时,再一个一个地迭代取出 - - Returns: - 返回能爬到的所有的 WikiModel 所组成的 List - """ - queue: Queue["WikiModel"] = Queue() # 存放 Model 的队列 - signal = Value("i", 0) # 一个用于异步任务同步的信号 - - async def task(u): - # 包装的爬虫任务 - try: - await queue.put(await cls._scrape(u)) # 爬取一条数据,并将其放入队列中 - except Exception as exc: # pylint: disable=W0703 - logger.error("爬取数据出现异常 %s", str(exc)) - logger.debug("异常信息", exc_info=exc) - finally: - signal.value -= 1 # 信号量减少 1 ,说明该爬虫任务已经完成 - - for _, url in await cls.get_name_list(with_url=True): # 遍历爬取所有需要爬取的页面 - signal.value += 1 # 信号量增加 1 ,说明有一个爬虫任务被添加 - asyncio.create_task(task(url)) # 创建一个爬虫任务 - - while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时 - yield await queue.get() # 取出并返回一个存放的 Model - - def __str__(self) -> str: - return f"<{self.__class__.__name__} {super(WikiModel, self).__str__()}>" - - def __repr__(self) -> str: - return self.__str__() + async def dump(datas, path: Path): + async with aiofiles.open(path, "w", encoding="utf-8") as f: + await f.write(jsonlib.dumps(datas, indent=4, ensure_ascii=False)) @staticmethod - async def get_url_by_id(id_: str) -> URL: - """根据 id 获取对应的 url + async def read(path: Path) -> List[Dict]: + async with aiofiles.open(path, "r", encoding="utf-8") as f: + datas = jsonlib.loads(await f.read()) + return datas - 例如神里绫华的ID为 ayaka_002,对应的数据页url为 https://genshin.honeyhunterworld.com/ayaka_002/?lang=CHS + @staticmethod + async def save_file(data, path: Path): + async with aiofiles.open(path, "wb") as f: + await f.write(data) - Args: - id_ : 实列ID - Returns: - 返回对应的 url - """ - 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]]]: - """一个 Model 的名称 和 其对应 url 的异步生成器 - - Args: - with_url: 是否返回相应的 url - Returns: - 返回对应的名称列表 或者 名称与url 的列表 - """ - urls = cls.scrape_urls() - queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列 - signal = Value("i", len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数 - - async def task(page: URL): - """包装的爬虫任务""" - response = await cls._client_get(page) - # 从页面中获取对应的 chaos data (未处理的json格式字符串) - chaos_data = re.findall(r"sortable_data\.push\((.*?)\);\s*sortable_cur_page", response.text)[0] - json_data = jsonlib.loads(chaos_data) # 转为 json - for data in json_data: # 遍历 json - data_name = re.findall(r">(.*)<", data[1])[0].strip() # 获取 Model 的名称 - if with_url: # 如果需要返回对应的 url - data_url = HONEY_HOST.join(re.findall(r"\"(.*?)\"", data[0])[0]) - await queue.put((data_name, data_url)) - else: - await queue.put(data_name) - signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成 - - for url in urls: # 遍历需要爬出的页面 - asyncio.create_task(task(url)) # 添加爬虫任务 - while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时 - yield await queue.get() # 取出并返回一个存放的 Model - - @classmethod - async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]: - """获取全部 Model 的 名称 - - Returns: - 返回能爬到的所有的 Model 的名称所组成的 List - """ - return [i async for i in cls._name_list_generator(with_url=with_url)] - - @classmethod - async def get_url_by_name(cls, name: str) -> Optional[URL]: - """通过 Model 的名称获取对应的 url - - Args: - name: 实列名 - Returns: - 若有对应的实列,则返回对应的 url; 若没有, 则返回 None - """ - async for n, url in cls._name_list_generator(with_url=True): - if name == n: - return url - - @property - @abstractmethod - def icon(self): - """返回此 Model 的图标链接""" + @staticmethod + async def read_file(path: Path): + async with aiofiles.open(path, "rb") as f: + return await f.read() diff --git a/modules/wiki/buddy.py b/modules/wiki/buddy.py new file mode 100644 index 0000000..ce7a866 --- /dev/null +++ b/modules/wiki/buddy.py @@ -0,0 +1,46 @@ +from typing import List, Dict, Optional + +from modules.wiki.base import WikiModel +from modules.wiki.models.buddy import Buddy as BuddyModel + + +class Buddy(WikiModel): + buddy_url = WikiModel.BASE_URL + "buddy.json" + buddy_path = WikiModel.BASE_PATH / "buddy.json" + + def __init__(self): + super().__init__() + self.all_buddys: List[BuddyModel] = [] + self.all_buddys_map: Dict[int, BuddyModel] = {} + self.all_buddys_name: Dict[str, BuddyModel] = {} + + def clear_class_data(self) -> None: + self.all_buddys.clear() + self.all_buddys_map.clear() + self.all_buddys_name.clear() + + async def refresh(self): + datas = await self.remote_get(self.buddy_url) + await self.dump(datas.json(), self.buddy_path) + await self.read() + + async def read(self): + if not self.buddy_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.buddy_path) + self.clear_class_data() + for data in datas: + m = BuddyModel(**data) + self.all_buddys.append(m) + self.all_buddys_map[m.id] = m + self.all_buddys_name[m.name] = m + + def get_by_id(self, cid: int) -> Optional[BuddyModel]: + return self.all_buddys_map.get(cid) + + def get_by_name(self, name: str) -> Optional[BuddyModel]: + return self.all_buddys_name.get(name) + + def get_name_list(self) -> List[str]: + return list(self.all_buddys_name.keys()) diff --git a/modules/wiki/character.py b/modules/wiki/character.py index 6820c77..53ccbc8 100644 --- a/modules/wiki/character.py +++ b/modules/wiki/character.py @@ -1,199 +1,46 @@ -import re -from typing import List, Optional +from typing import List, Dict, Optional -from bs4 import BeautifulSoup -from httpx import URL - -from modules.wiki.base import HONEY_HOST, Model, WikiModel -from modules.wiki.other import Association, Element, WeaponType - - -class Birth(Model): - """生日 - Attributes: - day: 天 - month: 月 - """ - - day: int - month: int - - -class CharacterAscension(Model): - """角色的突破材料 - - Attributes: - level: 等级突破材料 - skill: 技能/天赋培养材料 - """ - - level: List[str] = [] - skill: List[str] = [] - - -class CharacterState(Model): - """角色属性值 - - Attributes: - level: 等级 - HP: 生命 - ATK: 攻击力 - DEF: 防御力 - CR: 暴击率 - CD: 暴击伤害 - bonus: 突破属性 - """ - - level: str - HP: int - ATK: float - DEF: float - CR: str - CD: str - bonus: str - - -class CharacterIcon(Model): - icon: str - side: str - gacha: str - splash: Optional[str] +from modules.wiki.base import WikiModel +from modules.wiki.models.avatar import Avatar class Character(WikiModel): - """角色 - Attributes: - title: 称号 - occupation: 所属 - association: 地区 - weapon_type: 武器类型 - element: 元素 - birth: 生日 - constellation: 命之座 - cn_cv: 中配 - jp_cv: 日配 - en_cv: 英配 - kr_cv: 韩配 - description: 描述 - """ + avatar_url = WikiModel.BASE_URL + "avatars.json" + avatar_path = WikiModel.BASE_PATH / "avatars.json" - id: str - title: str - occupation: str - association: Association - weapon_type: WeaponType - element: Element - birth: Optional[Birth] - constellation: str - cn_cv: str - jp_cv: str - en_cv: str - kr_cv: str - description: str - ascension: CharacterAscension + def __init__(self): + super().__init__() + self.all_avatars: List[Avatar] = [] + self.all_avatars_map: Dict[int, Avatar] = {} + self.all_avatars_name: Dict[str, Avatar] = {} - stats: List[CharacterState] + def clear_class_data(self) -> None: + self.all_avatars.clear() + self.all_avatars_map.clear() + self.all_avatars_name.clear() - @classmethod - def scrape_urls(cls) -> List[URL]: - return [HONEY_HOST.join("fam_chars/?lang=CHS")] + async def refresh(self): + datas = await self.remote_get(self.avatar_url) + await self.dump(datas.json(), self.avatar_path) + await self.read() - @classmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> "Character": - """解析角色页""" - soup = soup.select(".wp-block-post-content")[0] - tables = soup.find_all("table") - table_rows = tables[0].find_all("tr") + async def read(self): + if not self.avatar_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.avatar_path) + self.clear_class_data() + for data in datas: + m = Avatar(**data) + self.all_avatars.append(m) + self.all_avatars_map[m.id] = m + self.all_avatars_name[m.name] = m - def get_table_text(row_num: int) -> str: - """一个快捷函数,用于返回表格对应行的最后一个单元格中的文本""" - return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "") + def get_by_id(self, cid: int) -> Optional[Avatar]: + return self.all_avatars_map.get(cid) - id_ = re.findall(r"img/(.*?_\d+)_.*", table_rows[0].find("img").attrs["src"])[0] - name = get_table_text(0) - if name != "旅行者": # 如果角色名不是 旅行者 - title = get_table_text(1) - occupation = get_table_text(2) - association = Association.convert(get_table_text(3).lower().title()) - rarity = len(table_rows[4].find_all("img")) - weapon_type = WeaponType[get_table_text(5)] - element = Element[get_table_text(6)] - birth = Birth(day=int(get_table_text(7)), month=int(get_table_text(8))) - constellation = get_table_text(10) - cn_cv = get_table_text(11) - jp_cv = get_table_text(12) - en_cv = get_table_text(13) - kr_cv = get_table_text(14) - else: - name = "空" if id_.endswith("5") else "荧" - title = get_table_text(0) - occupation = get_table_text(1) - association = Association.convert(get_table_text(2).lower().title()) - rarity = len(table_rows[3].find_all("img")) - weapon_type = WeaponType[get_table_text(4)] - element = Element[get_table_text(5)] - birth = None - constellation = get_table_text(7) - cn_cv = get_table_text(8) - jp_cv = get_table_text(9) - en_cv = get_table_text(10) - kr_cv = get_table_text(11) - description = get_table_text(-3) - ascension = CharacterAscension( - level=[ - target[0] - for i in table_rows[-2].find_all("a") - if (target := re.findall(r"/(.*)/", i.attrs["href"])) # 过滤掉错误的材料(honey网页的bug) - ], - skill=[re.findall(r"/(.*)/", i.attrs["href"])[0] for i in table_rows[-1].find_all("a")], - ) - stats = [] - for row in tables[2].find_all("tr")[1:]: - cells = row.find_all("td") - stats.append( - CharacterState( - level=cells[0].text, - HP=cells[1].text, - ATK=cells[2].text, - DEF=cells[3].text, - CR=cells[4].text, - CD=cells[5].text, - bonus=cells[6].text, - ) - ) - return Character( - id=id_, - name=name, - title=title, - occupation=occupation, - association=association, - weapon_type=weapon_type, - element=element, - birth=birth, - constellation=constellation, - cn_cv=cn_cv, - jp_cv=jp_cv, - rarity=rarity, - en_cv=en_cv, - kr_cv=kr_cv, - description=description, - ascension=ascension, - stats=stats, - ) + def get_by_name(self, name: str) -> Optional[Avatar]: + return self.all_avatars_name.get(name) - @classmethod - async def get_url_by_name(cls, name: str) -> Optional[URL]: - # 重写此函数的目的是处理主角名字的 ID - _map = {"荧": "playergirl_007", "空": "playerboy_005"} - if (id_ := _map.get(name)) is not None: - return await cls.get_url_by_id(id_) - return await super(Character, cls).get_url_by_name(name) - - @property - def icon(self) -> CharacterIcon: - return CharacterIcon( - 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")), - ) + def get_name_list(self) -> List[str]: + return list(self.all_avatars_name.keys()) diff --git a/modules/wiki/equipment_suit.py b/modules/wiki/equipment_suit.py new file mode 100644 index 0000000..dd3fc7c --- /dev/null +++ b/modules/wiki/equipment_suit.py @@ -0,0 +1,46 @@ +from typing import List, Dict, Optional + +from modules.wiki.base import WikiModel +from modules.wiki.models.equipment_suit import EquipmentSuit as EquipmentSuitModel + + +class EquipmentSuit(WikiModel): + equipment_suit_url = WikiModel.BASE_URL + "equipment_suits.json" + equipment_suit_path = WikiModel.BASE_PATH / "equipment_suits.json" + + def __init__(self): + super().__init__() + self.all_equipment_suits: List[EquipmentSuitModel] = [] + self.all_equipment_suits_map: Dict[int, EquipmentSuitModel] = {} + self.all_equipment_suits_name: Dict[str, EquipmentSuitModel] = {} + + def clear_class_data(self) -> None: + self.all_equipment_suits.clear() + self.all_equipment_suits_map.clear() + self.all_equipment_suits_name.clear() + + async def refresh(self): + datas = await self.remote_get(self.equipment_suit_url) + await self.dump(datas.json(), self.equipment_suit_path) + await self.read() + + async def read(self): + if not self.equipment_suit_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.equipment_suit_path) + self.clear_class_data() + for data in datas: + m = EquipmentSuitModel(**data) + self.all_equipment_suits.append(m) + self.all_equipment_suits_map[m.id] = m + self.all_equipment_suits_name[m.name] = m + + def get_by_id(self, cid: int) -> Optional[EquipmentSuitModel]: + return self.all_equipment_suits_map.get(cid) + + def get_by_name(self, name: str) -> Optional[EquipmentSuitModel]: + return self.all_equipment_suits_name.get(name) + + def get_name_list(self) -> List[str]: + return list(self.all_equipment_suits_name.keys()) diff --git a/modules/wiki/material.py b/modules/wiki/material.py deleted file mode 100644 index ce9b193..0000000 --- a/modules/wiki/material.py +++ /dev/null @@ -1,81 +0,0 @@ -import re -from typing import List, Optional, Tuple, Union - -from bs4 import BeautifulSoup -from httpx import URL - -from modules.wiki.base import HONEY_HOST, WikiModel - -__all__ = ["Material"] - -WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] - - -class Material(WikiModel): - # noinspection PyUnresolvedReferences - """武器、角色培养素材 - - Attributes: - type: 类型 - weekdays: 每周开放的时间 - source: 获取方式 - description: 描述 - """ - type: str - source: Optional[List[str]] = None - weekdays: Optional[List[int]] = None - description: str - - @staticmethod - def scrape_urls() -> List[URL]: - 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 - async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]: - return list(sorted(set(await super(Material, cls).get_name_list(with_url=with_url)), key=lambda x: x[0])) - - @classmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> "Material": - """解析突破素材页""" - soup = soup.select(".wp-block-post-content")[0] - tables = soup.find_all("table") - table_rows = tables[0].find_all("tr") - - def get_table_row(target: str): - """一个便捷函数,用于返回对应表格头的对应行的最后一个单元格中的文本""" - for row in table_rows: - if target in row.find("td").text: - return row.find_all("td")[-1] - return None - - def get_table_text(row_num: int) -> str: - """一个便捷函数,用于返回表格对应行的最后一个单元格中的文本""" - return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "") - - id_ = re.findall(r"/img/(.*?)\.webp", str(table_rows[0]))[0] - name = get_table_text(0) - rarity = len(table_rows[3].find_all("img")) - type_ = get_table_text(1) - if (item_source := get_table_row("Item Source")) is not None: - item_source = list( - # filter 在这里的作用是过滤掉为空的数据 - filter(lambda x: x, item_source.encode_contents().decode().split("
")) - ) - if (alter_source := get_table_row("Alternative Item")) is not None: - alter_source = list( - # filter 在这里的作用是过滤掉为空的数据 - filter(lambda x: x, alter_source.encode_contents().decode().split("
")) - ) - source = list(sorted(set((item_source or []) + (alter_source or [])))) - if (weekdays := get_table_row("Weekday")) is not None: - weekdays = [*(WEEKDAYS.index(weekdays.text.replace("\xa0", "").split(",")[0]) + 3 * i for i in range(2)), 6] - description = get_table_text(-1) - return Material( - id=id_, name=name, rarity=rarity, type=type_, description=description, source=source, weekdays=weekdays - ) - - @property - def icon(self) -> str: - return str(HONEY_HOST.join(f"/img/{self.id}.webp")) diff --git a/modules/wiki/models/__init__.py b/modules/wiki/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/wiki/models/avatar.py b/modules/wiki/models/avatar.py new file mode 100644 index 0000000..c9a4956 --- /dev/null +++ b/modules/wiki/models/avatar.py @@ -0,0 +1,38 @@ +from typing import List + +from pydantic import BaseModel + +from .enums import ZZZElementType, ZZZSpeciality, ZZZRank + + +class Avatar(BaseModel, frozen=False): + id: int + """ 角色ID """ + name: str + """ 中文名称 """ + name_en: str + """ 英文名称 """ + name_full: str + """ 中文全称 """ + name_short: str + """ 英文简称 """ + rank: ZZZRank = ZZZRank.NULL + """ 星级 """ + element: ZZZElementType + """ 元素 """ + speciality: ZZZSpeciality + """ 特性 """ + icon: List[str] = ["", "", ""] + """ 图标 """ + + @property + def icon_(self) -> str: + return self.icon[0] + + @property + def normal(self) -> str: + return self.icon[1] + + @property + def gacha(self) -> str: + return self.icon[2] diff --git a/modules/wiki/models/buddy.py b/modules/wiki/models/buddy.py new file mode 100644 index 0000000..52f11c6 --- /dev/null +++ b/modules/wiki/models/buddy.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel + +from .enums import ZZZRank + + +class Buddy(BaseModel): + id: int + """"邦布ID""" + name: str + """名称""" + name_en: str + """英文名称""" + icon: str = "" + """图标""" + rank: ZZZRank = ZZZRank.NULL + """ 星级 """ + + @property + def webp(self) -> str: + return self.icon if self.icon.endswith("webp") else "" + + @property + def png(self) -> str: + return self.icon if self.icon.endswith("png") else "" diff --git a/modules/wiki/models/enums.py b/modules/wiki/models/enums.py new file mode 100644 index 0000000..fdfc43b --- /dev/null +++ b/modules/wiki/models/enums.py @@ -0,0 +1,49 @@ +from enum import Enum, IntEnum + + +class ZZZElementType(IntEnum): + """ZZZ element type.""" + + NULL = 1 + """ 空 """ + PHYSICAL = 200 + """ 物理 """ + FIRE = 201 + """ 火 """ + ICE = 202 + """ 冰 """ + ELECTRIC = 203 + """ 电 """ + ETHER = 205 + """ 以太 """ + + +class ZZZSpeciality(IntEnum): + """ZZZ agent compatible speciality.""" + + ATTACK = 1 + """ 强攻 """ + STUN = 2 + """ 击破 """ + ANOMALY = 3 + """ 异常 """ + SUPPORT = 4 + """ 支援 """ + DEFENSE = 5 + """ 防护 """ + + +class ZZZRank(str, Enum): + """ZZZ Rank""" + + S = "S" + A = "A" + B = "B" + C = "C" + D = "D" + NULL = "NULL" + + @property + def int(self): + value_map = {"S": 5, "A": 4, "B": 3, "C": 2, "D": 1, "NULL": 0} + return value_map[self.value] diff --git a/modules/wiki/models/equipment_suit.py b/modules/wiki/models/equipment_suit.py new file mode 100644 index 0000000..3e21299 --- /dev/null +++ b/modules/wiki/models/equipment_suit.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel + +from .enums import ZZZRank + + +class EquipmentSuit(BaseModel): + id: int + """驱动盘套装ID""" + name: str + """套装名称""" + name_en: str + """英文套装名称""" + icon: str = "" + """套装图标""" + desc_2: str + """2套描述""" + desc_4: str + """4套描述""" + story: str + """套装故事""" + rank: ZZZRank = ZZZRank.NULL + """ 星级 """ diff --git a/modules/wiki/models/weapon.py b/modules/wiki/models/weapon.py new file mode 100644 index 0000000..9103fd9 --- /dev/null +++ b/modules/wiki/models/weapon.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +from .enums import ZZZRank + + +class Weapon(BaseModel): + id: int + """"武器ID""" + name: str + """名称""" + name_en: str + """英文名称""" + description: str + """描述""" + icon: str = "" + """图标""" + rank: ZZZRank + """稀有度""" diff --git a/modules/wiki/other.py b/modules/wiki/other.py index 5f8ca22..5fa76bf 100644 --- a/modules/wiki/other.py +++ b/modules/wiki/other.py @@ -124,7 +124,6 @@ class Association(Enum): Inazuma = "稻妻" Liyue = "璃月" Mondstadt = "蒙德" - Fontaine = "枫丹" @classmethod def convert(cls, string: str) -> Optional[Self]: diff --git a/modules/wiki/raider.py b/modules/wiki/raider.py new file mode 100644 index 0000000..65d7363 --- /dev/null +++ b/modules/wiki/raider.py @@ -0,0 +1,69 @@ +import asyncio +from typing import List, Dict + +from metadata.shortname import roleToName, weaponToName +from modules.wiki.base import WikiModel + + +class Raider(WikiModel): + raider_url = "https://raw.githubusercontent.com/PaiGramTeam/zzz-atlas/master" + raider_path = WikiModel.BASE_PATH / "raiders" + raider_role_path = WikiModel.BASE_PATH / "raiders" / "role" + raider_guide_for_role_path = WikiModel.BASE_PATH / "raiders" / "guide_for_role" + raider_light_cone_path = WikiModel.BASE_PATH / "raiders" / "weapon" + raider_relic_path = WikiModel.BASE_PATH / "raiders" / "relic" + raider_info_path = WikiModel.BASE_PATH / "raiders" / "path.json" + raider_role_path.mkdir(parents=True, exist_ok=True) + raider_guide_for_role_path.mkdir(parents=True, exist_ok=True) + raider_light_cone_path.mkdir(parents=True, exist_ok=True) + raider_relic_path.mkdir(parents=True, exist_ok=True) + name_map = { + "角色": "role", + "音擎": "weapon", + "角色攻略": "guide_for_role", + } + + def __init__(self): + super().__init__() + self.all_role_raiders: List[str] = [] + self.all_guide_for_role_raiders: List[str] = [] + self.all_light_cone_raiders: List[str] = [] + self.all_relic_raiders: List[str] = [] + + def clear_class_data(self) -> None: + self.all_role_raiders.clear() + self.all_guide_for_role_raiders.clear() + self.all_light_cone_raiders.clear() + self.all_relic_raiders.clear() + + async def refresh_task(self, name: str, path: str = "", start: str = ""): + photo = await self.remote_get(f"{self.raider_url}{path}") + await self.save_file(photo.content, self.raider_path / start / f"{name}.png") + + async def refresh(self): + datas = await self.remote_get(self.raider_url + "/path.json") + data = datas.json() + new_data = {} + for key, start in self.name_map.items(): + new_data[start] = list(data[key].keys()) + tasks = [] + for name, path in data[key].items(): + if key in {"角色", "角色攻略"}: + name = roleToName(name) + else: + name = weaponToName(name) + tasks.append(self.refresh_task(name, path, start)) + await asyncio.gather(*tasks) + await self.dump(new_data, self.raider_info_path) + await self.read() + + async def read(self): + if not self.raider_info_path.exists(): + await self.refresh() + return + datas: Dict[str, List] = await WikiModel.read(self.raider_info_path) # noqa + self.clear_class_data() + self.all_role_raiders.extend(datas["role"]) + self.all_guide_for_role_raiders.extend(datas["guide_for_role"]) + self.all_light_cone_raiders.extend(datas["weapon"]) + # self.all_relic_raiders.extend(datas["relic"]) diff --git a/modules/wiki/weapon.py b/modules/wiki/weapon.py index 1623f6d..cb8490d 100644 --- a/modules/wiki/weapon.py +++ b/modules/wiki/weapon.py @@ -1,146 +1,46 @@ -import itertools -import re -from typing import List, Optional, Tuple, Union +from typing import List, Dict, Optional -from bs4 import BeautifulSoup -from httpx import URL - -from modules.wiki.base import HONEY_HOST, Model, WikiModel -from modules.wiki.other import AttributeType, WeaponType - -__all__ = ["Weapon", "WeaponAffix", "WeaponAttribute"] - - -class WeaponAttribute(Model): - """武器词条""" - - type: AttributeType - value: str - - -class WeaponAffix(Model): - """武器技能 - - Attributes: - name: 技能名 - description: 技能描述 - - """ - - name: str - description: List[str] - - -class WeaponState(Model): - level: str - ATK: float - bonus: Optional[str] - - -class WeaponIcon(Model): - icon: str - awakened: str - gacha: str +from modules.wiki.base import WikiModel +from modules.wiki.models.weapon import Weapon as WeaponModel class Weapon(WikiModel): - """武器 + weapon_url = WikiModel.BASE_URL + "weapons.json" + weapon_path = WikiModel.BASE_PATH / "weapons.json" - Attributes: - weapon_type: 武器类型 - attack: 基础攻击力 - attribute: - affix: 武器技能 - description: 描述 - ascension: 突破材料 - story: 武器故事 - """ + def __init__(self): + super().__init__() + self.all_weapons: List[WeaponModel] = [] + self.all_weapons_map: Dict[int, WeaponModel] = {} + self.all_weapons_name: Dict[str, WeaponModel] = {} - weapon_type: WeaponType - attack: float - attribute: Optional[WeaponAttribute] - affix: Optional[WeaponAffix] - description: str - ascension: List[str] - story: Optional[str] + def clear_class_data(self) -> None: + self.all_weapons.clear() + self.all_weapons_map.clear() + self.all_weapons_name.clear() - stats: List[WeaponState] + async def refresh(self): + datas = await self.remote_get(self.weapon_url) + await self.dump(datas.json(), self.weapon_path) + await self.read() - @staticmethod - def scrape_urls() -> List[URL]: - return [HONEY_HOST.join(f"fam_{i.lower()}/?lang=CHS") for i in WeaponType.__members__] + async def read(self): + if not self.weapon_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.weapon_path) + self.clear_class_data() + for data in datas: + m = WeaponModel(**data) + self.all_weapons.append(m) + self.all_weapons_map[m.id] = m + self.all_weapons_name[m.name] = m - @classmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> "Weapon": - """解析武器页""" - soup = soup.select(".wp-block-post-content")[0] - tables = soup.find_all("table") - table_rows = tables[0].find_all("tr") + def get_by_id(self, cid: int) -> Optional[WeaponModel]: + return self.all_weapons_map.get(cid) - def get_table_text(row_num: int) -> str: - """一个快捷函数,用于返回表格对应行的最后一个单元格中的文本""" - return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "") + def get_by_name(self, name: str) -> Optional[WeaponModel]: + return self.all_weapons_name.get(name) - def find_table(select: str): - """一个快捷函数,用于寻找对应表格头的表格""" - return list(filter(lambda x: select in " ".join(x.attrs["class"]), tables)) - - id_ = re.findall(r"/img/(.*?)_gacha", str(table_rows[0]))[0] - weapon_type = WeaponType[get_table_text(1).split(",")[-1].strip()] - name = get_table_text(0) - rarity = len(table_rows[2].find_all("img")) - attack = float(get_table_text(4)) - ascension = [re.findall(r"/(.*)/", tag.attrs["href"])[0] for tag in table_rows[-1].find_all("a")] - if rarity > 2: # 如果是 3 星及其以上的武器 - attribute = WeaponAttribute( - type=AttributeType.convert(tables[2].find("thead").find("tr").find_all("td")[2].text.split(" ")[1]), - value=get_table_text(6), - ) - affix = WeaponAffix( - name=get_table_text(7), description=[i.find_all("td")[1].text for i in tables[3].find_all("tr")[1:]] - ) - description = get_table_text(9) - if story_table := find_table("quotes"): - story = story_table[0].text.strip() - else: - story = None - else: # 如果是 2 星及其以下的武器 - attribute = affix = None - description = get_table_text(5) - story = tables[-1].text.strip() - stats = [] - for row in tables[2].find_all("tr")[1:]: - cells = row.find_all("td") - if rarity > 2: - stats.append(WeaponState(level=cells[0].text, ATK=cells[1].text, bonus=cells[2].text)) - else: - stats.append(WeaponState(level=cells[0].text, ATK=cells[1].text)) - return Weapon( - id=id_, - name=name, - rarity=rarity, - attack=attack, - attribute=attribute, - affix=affix, - weapon_type=weapon_type, - story=story, - stats=stats, - description=description, - ascension=ascension, - ) - - @classmethod - async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]: - # 重写此函数的目的是名字去重,例如单手剑页面中有三个 “「一心传」名刀” - name_list = [i async for i in cls._name_list_generator(with_url=with_url)] - if with_url: - return [(i[0], list(i[1])[0][1]) for i in itertools.groupby(name_list, lambda x: x[0])] - return [i[0] for i in itertools.groupby(name_list, lambda x: x)] - - @property - def icon(self) -> WeaponIcon: - return WeaponIcon( - 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")), - ) + def get_name_list(self) -> List[str]: + return list(self.all_weapons_name.keys()) diff --git a/pdm.lock b/pdm.lock index fea3e37..1b40f6d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2212,11 +2212,11 @@ name = "simnet" version = "0.1.22" requires_python = "<4.0,>=3.8" git = "https://github.com/PaiGramTeam/SIMNet" -revision = "05fcb568d6c1fe44a4f917c996198bfe62a00053" +revision = "074939d8818e6073be4a918b25d7deadd43a5b7b" summary = "Modern API wrapper for Genshin Impact & Honkai: Star Rail built on asyncio and pydantic." groups = ["default"] dependencies = [ - "httpx<1.0.0,>=0.25.0", + "httpx>=0.25.0", "pydantic<2.0.0,>=1.10.7", ] diff --git a/plugins/account/account.py b/plugins/account/account.py index 62d052d..71db797 100644 --- a/plugins/account/account.py +++ b/plugins/account/account.py @@ -212,7 +212,7 @@ class BindAccountPlugin(Plugin.Conversation): return ConversationHandler.END try: async with self.helper.public_genshin(user.id, region=region, uid=player_id) as client: - player_stats = await client.get_genshin_user(player_id) + player_stats = await client.get_zzz_user(player_id) except TooManyRequestPublicCookies: await message.reply_text("用户查询次数过多,请稍后重试", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END diff --git a/plugins/admin/set_command.py b/plugins/admin/set_command.py index bc80101..ed87bd4 100644 --- a/plugins/admin/set_command.py +++ b/plugins/admin/set_command.py @@ -21,6 +21,11 @@ class SetCommandPlugin(Plugin): user_command = [ BotCommand("cancel", "取消操作(解决一切玄学问题)"), BotCommand("help_raw", "查看文本帮助"), + # gacha_log 相关 + BotCommand("signal_log", "查看调频记录"), + BotCommand("signal_log_import", "导入调频记录"), + BotCommand("signal_log_export", "导出调频记录"), + BotCommand("signal_log_delete", "删除调频记录"), # Cookie 查询类 BotCommand("sign", "米游社绝区零每日签到"), BotCommand("dailynote_tasks", "自动便笺提醒"), @@ -33,6 +38,9 @@ class SetCommandPlugin(Plugin): group_command = [ # 通用 BotCommand("help", "帮助"), + # Wiki 类 + BotCommand("weapon", "查看音擎图鉴"), + BotCommand("avatar", "查询角色攻略"), # UID 查询类 BotCommand("stats", "玩家统计查询"), # Cookie 查询类 diff --git a/plugins/admin/wiki.py b/plugins/admin/wiki.py new file mode 100644 index 0000000..8946df1 --- /dev/null +++ b/plugins/admin/wiki.py @@ -0,0 +1,19 @@ +from telegram import Update +from telegram.ext import CallbackContext + +from core.plugin import Plugin, handler +from core.services.wiki.services import WikiService + + +class WikiPlugin(Plugin): + """有关WIKI操作""" + + def __init__(self, wiki_service: WikiService): + self.wiki_service = wiki_service + + @handler.command("refresh_wiki", block=False, admin=True) + async def refresh_wiki(self, update: Update, _: CallbackContext): + message = update.effective_message + msg = await message.reply_text("正在刷新Wiki缓存,请稍等") + await self.wiki_service.refresh_wiki() + await msg.edit_text("刷新Wiki缓存成功") diff --git a/plugins/app/inline.py b/plugins/app/inline.py index 1c7c1f6..6cdf4c4 100644 --- a/plugins/app/inline.py +++ b/plugins/app/inline.py @@ -1,27 +1,31 @@ +import asyncio from typing import Awaitable, Dict, List, cast, Tuple from uuid import uuid4 from telegram import ( InlineQuery, InlineQueryResultArticle, - InlineQueryResultPhoto, InlineQueryResultCachedPhoto, + InlineQueryResultCachedDocument, InputTextMessageContent, Update, InlineQueryResultsButton, - InlineKeyboardButton, InlineKeyboardMarkup, + InlineKeyboardButton, + InlineQueryResultPhoto, ) from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.ext import CallbackContext, ContextTypes +from core.dependence.assets import AssetsService from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.players import PlayersService from core.services.search.services import SearchServices +from core.services.wiki.services import WikiService from gram_core.config import config from gram_core.plugin.methods.inline_use_data import IInlineUseData +from gram_core.services.cookies import CookiesService +from gram_core.services.players import PlayersService from utils.log import logger @@ -30,12 +34,20 @@ class Inline(Plugin): def __init__( self, + asset_service: AssetsService, search_service: SearchServices, + wiki_service: WikiService, cookies_service: CookiesService, players_service: PlayersService, ): + self.asset_service = asset_service + self.wiki_service = wiki_service self.weapons_list: List[Dict[str, str]] = [] self.characters_list: List[Dict[str, str]] = [] + self.characters_material_list: List[Dict[str, str]] = [] + self.characters_guide_list: List[Dict[str, str]] = [] + self.light_cone_list: List[Dict[str, str]] = [] + self.relics_list: List[Dict[str, str]] = [] self.refresh_task: List[Awaitable] = [] self.search_service = search_service self.cookies_service = cookies_service @@ -44,6 +56,84 @@ class Inline(Plugin): self.inline_use_data_map: Dict[str, IInlineUseData] = {} self.img_url = "https://i.dawnlab.me/b1bdf9cc3061d254f038e557557694bc.jpg" + async def initialize(self): + async def task_light_cone(): + logger.info("Inline 模块正在获取武器列表") + light_cone_datas: Dict[str, str] = {} + light_cone_datas_name: Dict[str, str] = {} + for light_cone in self.asset_service.weapon.data: + light_cone_datas[light_cone.name] = light_cone.icon + light_cone_datas_name[str(light_cone.id)] = light_cone.name + # 武器列表 + for lid in self.wiki_service.raider.all_light_cone_raiders: + if lid not in light_cone_datas_name: + continue + light_cone = light_cone_datas_name[lid] + if light_cone in light_cone_datas: + self.light_cone_list.append({"name": light_cone, "icon": light_cone_datas[light_cone]}) + else: + logger.warning(f"未找到武器 {light_cone} 的图标,inline 不显示此武器") + logger.success("Inline 模块获取武器列表完成") + + async def task_relics(): + logger.info("Inline 模块正在获取驱动盘列表") + relics_datas: Dict[str, str] = {} + relics_datas_name: Dict[str, str] = {} + for relics in self.wiki_service.equipment_suit.all_equipment_suits: + relics_datas[relics.name] = relics.icon + relics_datas_name[str(relics.id)] = relics.name + for rid in self.wiki_service.raider.all_relic_raiders: + if rid not in relics_datas_name: + continue + relics = relics_datas_name[rid] + if relics in relics_datas: + self.relics_list.append({"name": relics, "icon": relics_datas[relics]}) + else: + logger.warning(f"未找到驱动盘 {relics} 的图标,inline 不显示此驱动盘") + logger.success("Inline 模块获取驱动盘列表完成") + + async def task_characters(): + logger.info("Inline 模块正在获取角色列表") + datas: Dict[str, str] = {} + datas_name: Dict[str, str] = {} + for character in self.asset_service.avatar.data: + datas[character.name] = character.normal + datas_name[str(character.id)] = character.name + + def get_character(_cid: str) -> str: + if _cid in datas_name: + return datas_name[_cid] + + # 角色攻略 + for cid in self.wiki_service.raider.all_role_raiders: + character = get_character(cid) + if not character: + continue + if character in datas: + self.characters_list.append({"name": character, "icon": datas[character]}) + else: + for key, value in datas.items(): + if character.startswith(key) or character.endswith(key): + self.characters_list.append({"name": character, "icon": value}) + break + # 角色攻略 + for cid in self.wiki_service.raider.all_guide_for_role_raiders: + character = get_character(cid) + if not character: + continue + if character in datas: + self.characters_guide_list.append({"name": character, "icon": datas[character]}) + else: + for key, value in datas.items(): + if character.startswith(key) or character.endswith(key): + self.characters_guide_list.append({"name": character, "icon": value}) + break + logger.success("Inline 模块获取角色列表成功") + + self.refresh_task.append(asyncio.create_task(task_characters())) + self.refresh_task.append(asyncio.create_task(task_light_cone())) + self.refresh_task.append(asyncio.create_task(task_relics())) + async def init_inline_use_data(self): if self.inline_use_data: return @@ -159,22 +249,22 @@ class Inline(Plugin): results_list = [] args = query.split(" ") if args[0] == "": - results_list.append( - InlineQueryResultArticle( - id=str(uuid4()), - title="武器图鉴查询", - description="输入武器名称即可查询武器图鉴", - input_message_content=InputTextMessageContent("武器图鉴查询"), + temp_data = [ + ("武器图鉴查询", "输入武器名称即可查询武器图鉴"), + ("角色攻略查询", "输入角色名即可查询角色攻略图鉴"), + # ("角色图鉴查询", "输入角色名即可查询角色图鉴"), + # ("角色培养素材查询", "输入角色名即可查询角色培养素材图鉴"), + # ("驱动盘套装查询", "输入驱动盘套装名称即可查询驱动盘套装图鉴"), + ] + for i in temp_data: + results_list.append( + InlineQueryResultArticle( + id=str(uuid4()), + title=i[0], + description=i[1], + input_message_content=InputTextMessageContent(i[0]), + ) ) - ) - results_list.append( - InlineQueryResultArticle( - id=str(uuid4()), - title="角色攻略查询", - description="输入角色名即可查询角色攻略", - input_message_content=InputTextMessageContent("角色攻略查询"), - ) - ) results_list.append( InlineQueryResultArticle( id=str(uuid4()), @@ -188,33 +278,31 @@ class Inline(Plugin): elif args[0] == "功能": return else: - if args[0] == "查看武器列表并查询": - for weapon in self.weapons_list: - name = weapon["name"] - icon = weapon["icon"] - results_list.append( - InlineQueryResultArticle( - id=str(uuid4()), - title=name, - description=f"查看武器列表并查询 {name}", - thumbnail_url=icon, - input_message_content=InputTextMessageContent( - f"武器查询{name}", parse_mode=ParseMode.MARKDOWN_V2 - ), - ) - ) - elif args[0] == "查看角色攻略列表并查询": - for character in self.characters_list: + if args[0] in [ + # "查看角色攻略列表并查询", + "查看角色图鉴列表并查询", + "查看音擎列表并查询", + # "查看驱动盘套装列表并查询", + # "查看角色培养素材列表并查询", + ]: + temp_data = { + # "查看角色攻略列表并查询": (self.characters_list, "角色攻略查询"), + "查看角色图鉴列表并查询": (self.characters_guide_list, "角色图鉴查询"), + # "查看角色培养素材列表并查询": (self.characters_material_list, "角色培养素材查询"), + "查看音擎列表并查询": (self.light_cone_list, "音擎图鉴查询"), + # "查看驱动盘套装列表并查询": (self.relics_list, "驱动盘套装查询"), + }[args[0]] + for character in temp_data[0]: name = character["name"] icon = character["icon"] results_list.append( InlineQueryResultArticle( id=str(uuid4()), title=name, - description=f"查看角色攻略列表并查询 {name}", + description=f"{args[0]} {name}", thumbnail_url=icon, input_message_content=InputTextMessageContent( - f"角色攻略查询{name}", parse_mode=ParseMode.MARKDOWN_V2 + f"{temp_data[1]}{name}", parse_mode=ParseMode.MARKDOWN_V2 ), ) ) @@ -226,35 +314,44 @@ class Inline(Plugin): id=str(uuid4()), title=f"当前查询内容为 {args[0]}", description="如果无查看图片描述 这是正常的 客户端问题", - thumbnail_url="https://www.miyoushe.com/_nuxt/img/game-ys.dfc535b.jpg", + thumbnail_url="https://www.miyoushe.com/_nuxt/img/game-sr.4f80911.jpg", input_message_content=InputTextMessageContent( f"当前查询内容为 {args[0]}\n如果无查看图片描述 这是正常的 客户端问题" ), ) ) for simple_search_result in simple_search_results: + description = simple_search_result.description + if len(description) >= 10: + description = description[:10] + item = None if simple_search_result.photo_file_id: - description = simple_search_result.description - if len(description) >= 10: - description = description[:10] - results_list.append( - InlineQueryResultCachedPhoto( - id=str(uuid4()), - title=simple_search_result.title, - photo_file_id=simple_search_result.photo_file_id, - description=description, - caption=simple_search_result.caption, - parse_mode=simple_search_result.parse_mode, - ) + item = InlineQueryResultCachedPhoto( + id=str(uuid4()), + title=simple_search_result.title, + photo_file_id=simple_search_result.photo_file_id, + description=description, + caption=simple_search_result.caption, + parse_mode=simple_search_result.parse_mode, ) - + elif simple_search_result.document_file_id: + item = InlineQueryResultCachedDocument( + id=str(uuid4()), + title=simple_search_result.title, + document_file_id=simple_search_result.document_file_id, + description=description, + caption=simple_search_result.caption, + parse_mode=simple_search_result.parse_mode, + ) + if item: + results_list.append(item) if not results_list: results_list.append( InlineQueryResultArticle( id=str(uuid4()), title="好像找不到问题呢", - description="这个问题我也不知道,因为我就是个应急食品。", - input_message_content=InputTextMessageContent("这个问题我也不知道,因为我就是个应急食品。"), + description="这个问题我也不知道。", + input_message_content=InputTextMessageContent("这个问题我也不知道。"), ) ) try: diff --git a/plugins/tools/sign.py b/plugins/tools/sign.py index 1e54294..dcbfebe 100644 --- a/plugins/tools/sign.py +++ b/plugins/tools/sign.py @@ -8,7 +8,6 @@ from typing import Optional, Tuple, List, TYPE_CHECKING from httpx import TimeoutException from simnet import Game from simnet.errors import BadRequest as SimnetBadRequest, AlreadyClaimed, InvalidCookies, TimedOut as SimnetTimedOut -from simnet.utils.player import recognize_genshin_server from sqlalchemy.orm.exc import StaleDataError from telegram import InlineKeyboardButton, InlineKeyboardMarkup from telegram.constants import ParseMode @@ -114,10 +113,7 @@ class SignSystem(Plugin): title: Optional[str] = "签到结果", ) -> str: if is_sleep: - if recognize_genshin_server(client.player_id) in ("cn_gf01", "cn_qd01"): - await asyncio.sleep(random.randint(10, 300)) # nosec - else: - await asyncio.sleep(random.randint(0, 3)) # nosec + await asyncio.sleep(random.randint(0, 3)) # nosec try: rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn") except SimnetBadRequest as error: @@ -246,7 +242,7 @@ class SignSystem(Plugin): logger.warning("UID[%s] 已经签到", client.player_id) if is_raise: raise error - result = "今天旅行者已经签到过了~" + result = "今天绳匠已经签到过了~" except SimnetBadRequest as error: logger.warning("UID %s 签到失败,API返回信息为 %s", client.player_id, str(error)) if is_raise: @@ -256,7 +252,7 @@ class SignSystem(Plugin): result = "OK" else: logger.info("UID[%s] 已经签到", client.player_id) - result = "今天旅行者已经签到过了~" + result = "今天绳匠已经签到过了~" logger.info("UID[%s] 签到结果 %s", client.player_id, result) reward = rewards[daily_reward_info.claimed_rewards - (1 if daily_reward_info.signed_in else 0)] today = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) @@ -300,7 +296,7 @@ class SignSystem(Plugin): text = "自动签到执行失败,Cookie无效" sign_db.status = TaskStatusEnum.INVALID_COOKIES except AlreadyClaimed: - text = "今天旅行者已经签到过了~" + text = "今天绳匠已经签到过了~" sign_db.status = TaskStatusEnum.ALREADY_CLAIMED except SimnetBadRequest as exc: text = f"自动签到执行失败,API返回信息为 {str(exc)}" diff --git a/plugins/zzz/avatars.py b/plugins/zzz/avatars.py new file mode 100644 index 0000000..4392180 --- /dev/null +++ b/plugins/zzz/avatars.py @@ -0,0 +1,86 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.constants import ChatAction, ParseMode +from telegram.ext import CallbackContext, filters + +from core.plugin import Plugin, handler +from core.services.game.services import GameCacheService +from core.services.search.models import StrategyEntry +from core.services.search.services import SearchServices +from core.services.wiki.services import WikiService +from metadata.shortname import roleToName, roleToTag +from utils.log import logger + + +class AvatarsPlugin(Plugin): + """角色图鉴查询""" + + KEYBOARD = [ + [InlineKeyboardButton(text="查看角色图鉴列表并查询", switch_inline_query_current_chat="查看角色图鉴列表并查询")] + ] + + def __init__( + self, + cache_service: GameCacheService = None, + wiki_service: WikiService = None, + search_service: SearchServices = None, + ): + self.cache_service = cache_service + self.wiki_service = wiki_service + self.search_service = search_service + + @handler.command(command="avatar", block=False) + @handler.message(filters=filters.Regex("^角色图鉴查询(.*)"), block=False) + async def command_start(self, update: Update, context: CallbackContext) -> None: + message = update.effective_message + args = self.get_args(context) + if len(args) >= 1: + character_name = args[0] + else: + reply_message = await message.reply_text( + "请回复你要查询的图鉴的角色名", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) + ) + if filters.ChatType.GROUPS.filter(reply_message): + self.add_delete_message_job(message) + self.add_delete_message_job(reply_message) + return + character_name = roleToName(character_name) + file_path = self.wiki_service.raider.raider_guide_for_role_path / f"{character_name}.png" + if not file_path.exists(): + reply_message = await message.reply_text( + f"没有找到 {character_name} 的图鉴", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) + ) + if filters.ChatType.GROUPS.filter(reply_message): + self.add_delete_message_job(message) + self.add_delete_message_job(reply_message) + return + self.log_user(update, logger.info, "查询角色图鉴命令请求 || 参数 %s", character_name) + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + caption = "From 米游社@听语惊花" + if file_id := await self.cache_service.get_avatar_cache(character_name): + await message.reply_document( + document=file_id, + caption=caption, + filename=f"{character_name}.png", + parse_mode=ParseMode.HTML, + ) + else: + reply_photo = await message.reply_document( + document=open(file_path, "rb"), + caption=caption, + filename=f"{character_name}.png", + parse_mode=ParseMode.HTML, + ) + if reply_photo.document: + tags = roleToTag(character_name) + photo_file_id = reply_photo.document.file_id + await self.cache_service.set_avatar_cache(character_name, photo_file_id) + entry = StrategyEntry( + key=f"plugin:avatar:{character_name}", + title=character_name, + description=f"{character_name} 角色图鉴", + tags=tags, + caption=caption, + parse_mode="HTML", + document_file_id=photo_file_id, + ) + await self.search_service.add_entry(entry) diff --git a/plugins/zzz/signal_log.py b/plugins/zzz/signal_log.py new file mode 100644 index 0000000..3548b65 --- /dev/null +++ b/plugins/zzz/signal_log.py @@ -0,0 +1,596 @@ +from functools import partial +from io import BytesIO +from typing import Optional, TYPE_CHECKING, List, Union, Tuple, Dict + +from simnet import ZZZClient, Region +from simnet.models.zzz.wish import ZZZBannerType +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove +from telegram.constants import ChatAction +from telegram.ext import ConversationHandler, filters +from telegram.helpers import create_deep_linked_url + +from core.dependence.assets import AssetsService +from core.plugin import Plugin, conversation, handler +from core.services.cookies import CookiesService +from core.services.players import PlayersService +from core.services.template.models import FileType +from core.services.template.services import TemplateService +from gram_core.basemodel import RegionEnum +from gram_core.config import config +from gram_core.plugin.methods.inline_use_data import IInlineUseData +from modules.gacha_log.const import ZZZGF_VERSION, GACHA_TYPE_LIST_REVERSE +from modules.gacha_log.error import ( + GachaLogAccountNotFound, + GachaLogAuthkeyTimeout, + GachaLogFileError, + GachaLogInvalidAuthkey, + GachaLogMixedProvider, + GachaLogNotFound, +) +from modules.gacha_log.helpers import from_url_get_authkey +from modules.gacha_log.log import GachaLog +from modules.gacha_log.migrate import GachaLogMigrate +from modules.gacha_log.models import GachaLogInfo +from plugins.tools.genshin import PlayerNotFoundError +from utils.const import RESOURCE_DIR +from utils.log import logger + +try: + import ujson as jsonlib + +except ImportError: + import json as jsonlib + + +if TYPE_CHECKING: + from telegram import Update, Message, User, Document + from telegram.ext import ContextTypes + from gram_core.services.players.models import Player + from gram_core.services.template.models import RenderResult + +INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103) +WAITING = f"小{config.notice.bot_name}正在从服务器获取数据,请稍后" +WISHLOG_NOT_FOUND = f"{config.notice.bot_name}没有找到你的调频记录,快来私聊{config.notice.bot_name}导入吧~" + + +class WishLogPlugin(Plugin.Conversation): + """调频记录导入/导出/分析""" + + IMPORT_HINT = ( + "开始导入祈愿历史记录:请通过 https://zzz.rng.moe/en/tracker/import 获取调频记录链接后发送给我" + "(非 zzz.rng.moe 导出的文件数据)\n\n" + f"> 你还可以向{config.notice.bot_name}发送从其他工具导出的 ZZZGF {ZZZGF_VERSION} 标准的记录文件\n" + "> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n" + "注意:导入的数据将会与旧数据进行合并。" + ) + + def __init__( + self, + template_service: TemplateService, + players_service: PlayersService, + assets: AssetsService, + cookie_service: CookiesService, + ): + self.template_service = template_service + self.players_service = players_service + self.assets_service = assets + self.cookie_service = cookie_service + self.gacha_log = GachaLog() + self.wish_photo = None + + async def get_player_id(self, user_id: int, player_id: int, offset: int) -> int: + """获取绑定的游戏ID""" + logger.debug("尝试获取已绑定的绝区零账号") + player = await self.players_service.get_player(user_id, player_id=player_id, offset=offset) + if player is None: + raise PlayerNotFoundError(user_id) + return player.player_id + + async def _refresh_user_data( + self, user: "User", player_id: int, data: dict = None, authkey: str = None, verify_uid: bool = True + ) -> str: + """刷新用户数据 + :param user: 用户 + :param data: 数据 + :param authkey: 认证密钥 + :return: 返回信息 + """ + try: + logger.debug("尝试获取已绑定的绝区零账号") + if authkey: + new_num = await self.gacha_log.get_gacha_log_data(user.id, player_id, authkey) + return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条调频记录" + if data: + new_num = await self.gacha_log.import_gacha_log_data(user.id, player_id, data, verify_uid) + return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条调频记录" + except GachaLogNotFound: + return WISHLOG_NOT_FOUND + except GachaLogAccountNotFound: + return "导入失败,可能文件包含的调频记录所属 uid 与你当前绑定的 uid 不同" + except GachaLogFileError: + return "导入失败,数据格式错误" + except GachaLogInvalidAuthkey: + return "更新数据失败,authkey 无效" + except GachaLogAuthkeyTimeout: + return "更新数据失败,authkey 已经过期" + except GachaLogMixedProvider: + return "导入失败,你已经通过其他方式导入过调频记录了,本次无法导入" + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + return config.notice.user_not_found + + async def import_from_file( + self, user: "User", player_id: int, message: "Message", document: "Document" = None + ) -> None: + if not document: + document = message.document + # TODO: 使用 mimetype 判断文件类型 + if document.file_name.endswith(".json"): + file_type = "json" + else: + await message.reply_text("文件格式错误,请发送符合 ZZZGF 标准的调频记录文件") + return + if document.file_size > 5 * 1024 * 1024: + await message.reply_text("文件过大,请发送小于 5 MB 的文件") + return + try: + out = BytesIO() + await (await document.get_file()).download_to_memory(out=out) + if file_type == "json": + # bytesio to json + data = jsonlib.loads(out.getvalue().decode("utf-8")) + else: + await message.reply_text("文件解析失败,请检查文件") + return + except GachaLogFileError: + await message.reply_text("文件解析失败,请检查文件是否符合 ZZZGF 标准") + return + except (KeyError, IndexError, ValueError): + await message.reply_text("文件解析失败,请检查文件编码是否正确或符合 ZZZGF 标准") + return + except Exception as exc: + logger.error("文件解析失败 %s", repr(exc)) + await message.reply_text("文件解析失败,请检查文件是否符合 ZZZGF 标准") + return + await message.reply_chat_action(ChatAction.TYPING) + reply = await message.reply_text("文件解析成功,正在导入数据", reply_markup=ReplyKeyboardRemove()) + await message.reply_chat_action(ChatAction.TYPING) + try: + text = await self._refresh_user_data(user, player_id, data=data, verify_uid=file_type == "json") + except Exception as exc: # pylint: disable=W0703 + logger.error("文件解析失败 %s", repr(exc)) + text = "文件解析失败,请检查文件是否符合 ZZZGF 标准" + await reply.edit_text(text) + + async def can_gen_authkey(self, user_id: int, player_id: int) -> bool: + player_info = await self.players_service.get_player(user_id, region=RegionEnum.HYPERION, player_id=player_id) + if player_info is not None: + cookies = await self.cookie_service.get(user_id, account_id=player_info.account_id) + if ( + cookies is not None + and cookies.data + and "stoken" in cookies.data + and next((value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None) + ): + return True + return False + + async def gen_authkey(self, uid: int) -> Optional[str]: + player_info = await self.players_service.get_player(uid, region=RegionEnum.HYPERION) + if player_info is not None: + cookies = await self.cookie_service.get(uid, account_id=player_info.account_id) + if cookies is not None and cookies.data and "stoken" in cookies.data: + if stuid := next((value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None): + cookies.data["stuid"] = stuid + async with ZZZClient( + cookies=cookies.data, region=Region.CHINESE, lang="zh-cn", player_id=player_info.player_id + ) as client: + return await client.get_authkey_by_stoken("webview_gacha") + + @conversation.entry_point + @handler.command(command="signal_log_import", filters=filters.ChatType.PRIVATE, block=False) + @handler.message(filters=filters.Regex("^导入调频记录(.*)") & filters.ChatType.PRIVATE, block=False) + @handler.command(command="start", filters=filters.Regex("signal_log_import$"), block=False) + async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: + uid, offset = self.get_real_uid_or_offset(update) + message = update.effective_message + user = update.effective_user + player_id = await self.get_player_id(user.id, uid, offset) + context.chat_data["uid"] = player_id + logger.info("用户 %s[%s] 导入调频记录命令请求", user.full_name, user.id) + keyboard = None + if await self.can_gen_authkey(user.id, player_id): + keyboard = ReplyKeyboardMarkup([["自动导入"], ["退出"]], one_time_keyboard=True) + await message.reply_text(self.IMPORT_HINT, parse_mode="html", reply_markup=keyboard) + return INPUT_URL + + @conversation.state(state=INPUT_URL) + @handler.message(filters=~filters.COMMAND, block=False) + async def import_data_from_message(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: + message = update.effective_message + user = update.effective_user + player_id = context.chat_data["uid"] + if message.document: + await self.import_from_file(user, player_id, message) + return ConversationHandler.END + if not message.text: + await message.reply_text("请发送文件或链接") + return INPUT_URL + if message.text == "自动导入": + authkey = await self.gen_authkey(user.id) + if not authkey: + await message.reply_text( + "自动生成 authkey 失败,请尝试通过其他方式导入。", reply_markup=ReplyKeyboardRemove() + ) + return ConversationHandler.END + elif message.text == "退出": + await message.reply_text("取消导入跃迁记录", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + else: + authkey = from_url_get_authkey(message.text) + reply = await message.reply_text(WAITING, reply_markup=ReplyKeyboardRemove()) + await message.reply_chat_action(ChatAction.TYPING) + text = await self._refresh_user_data(user, player_id, authkey=authkey) + self.add_delete_message_job(reply, delay=1) + await message.reply_text(text, reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + + @conversation.entry_point + @handler.command(command="signal_log_delete", filters=filters.ChatType.PRIVATE, block=False) + @handler.message(filters=filters.Regex("^删除调频记录(.*)") & filters.ChatType.PRIVATE, block=False) + async def command_start_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: + uid, offset = self.get_real_uid_or_offset(update) + message = update.effective_message + user = update.effective_user + logger.info("用户 %s[%s] 删除调频记录命令请求", user.full_name, user.id) + try: + player_id = await self.get_player_id(user.id, uid, offset) + context.chat_data["uid"] = player_id + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + await message.reply_text(config.notice.user_not_found) + return ConversationHandler.END + _, status = await self.gacha_log.load_history_info(str(user.id), str(player_id), only_status=True) + if not status: + await message.reply_text("你还没有导入调频记录哦~") + return ConversationHandler.END + await message.reply_text( + "你确定要删除调频记录吗?(此项操作无法恢复),如果确定请发送 ”确定“,发送其他内容取消" + ) + return CONFIRM_DELETE + + @conversation.state(state=CONFIRM_DELETE) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) + async def command_confirm_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: + message = update.effective_message + user = update.effective_user + if message.text == "确定": + status = await self.gacha_log.remove_history_info(str(user.id), str(context.chat_data["uid"])) + await message.reply_text("调频记录已删除" if status else "调频记录删除失败") + return ConversationHandler.END + await message.reply_text("已取消") + return ConversationHandler.END + + @handler.command(command="signal_log_force_delete", block=False, admin=True) + async def command_signal_log_force_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): + uid, offset = self.get_real_uid_or_offset(update) + message = update.effective_message + args = self.get_args(context) + if not args: + await message.reply_text("请指定用户ID") + return + try: + cid = int(args[0]) + if cid < 0: + raise ValueError("Invalid cid") + player_id = await self.get_player_id(cid, uid, offset) + _, status = await self.gacha_log.load_history_info(str(cid), str(player_id), only_status=True) + if not status: + await message.reply_text("该用户还没有导入调频记录") + return + status = await self.gacha_log.remove_history_info(str(cid), str(player_id)) + await message.reply_text("调频记录已强制删除" if status else "调频记录删除失败") + except GachaLogNotFound: + await message.reply_text("该用户还没有导入调频记录") + except PlayerNotFoundError: + await message.reply_text("该用户暂未绑定账号") + except (ValueError, IndexError): + await message.reply_text("用户ID 不合法") + + @handler.command(command="signal_log_export", filters=filters.ChatType.PRIVATE, block=False) + @handler.message(filters=filters.Regex("^导出调频记录(.*)") & filters.ChatType.PRIVATE, block=False) + async def command_start_export(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: + uid, offset = self.get_real_uid_or_offset(update) + message = update.effective_message + user = update.effective_user + logger.info("用户 %s[%s] 导出调频记录命令请求", user.full_name, user.id) + try: + await message.reply_chat_action(ChatAction.TYPING) + player_id = await self.get_player_id(user.id, uid, offset) + path = await self.gacha_log.gacha_log_to_zzzgf(str(user.id), str(player_id)) + await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT) + await message.reply_document( + document=open(path, "rb+"), caption=f"调频记录导出文件 - ZZZGF {ZZZGF_VERSION}" + ) + except GachaLogNotFound: + logger.info("未找到用户 %s[%s] 的调频记录", user.full_name, user.id) + buttons = [ + [ + InlineKeyboardButton( + "点我导入", url=create_deep_linked_url(context.bot.username, "signal_log_import") + ) + ] + ] + await message.reply_text(WISHLOG_NOT_FOUND, reply_markup=InlineKeyboardMarkup(buttons)) + except GachaLogAccountNotFound: + await message.reply_text("导入失败,可能文件包含的调频记录所属 uid 与你当前绑定的 uid 不同") + except GachaLogFileError: + await message.reply_text("导入失败,数据格式错误") + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + await message.reply_text(config.notice.user_not_found) + + async def rander_wish_log_analysis( + self, user_id: int, player_id: int, pool_type: ZZZBannerType + ) -> Union[str, "RenderResult"]: + data = await self.gacha_log.get_analysis(user_id, player_id, pool_type, self.assets_service) + if isinstance(data, str): + return data + await self.add_theme_data(data, player_id) + png_data = await self.template_service.render( + "zzz/gacha_log/gacha_log.html", + data, + full_page=True, + file_type=FileType.DOCUMENT if len(data.get("fiveLog")) > 300 else FileType.PHOTO, + query_selector=".body_box", + ) + return png_data + + @staticmethod + def gen_button(user_id: int, uid: int, info: "GachaLogInfo") -> List[List[InlineKeyboardButton]]: + buttons = [] + pools = [] + skip_pools = [] + for k, v in info.item_list.items(): + if k in skip_pools: + continue + if not v: + continue + pools.append(k) + # 2 个一组 + for i in range(0, len(pools), 2): + row = [] + for pool in pools[i : i + 2]: + for k, v in {"log": "", "count": "(按卡池)"}.items(): + row.append( + InlineKeyboardButton( + f"{pool.replace('祈愿', '')}{v}", + callback_data=f"get_wish_log|{user_id}|{uid}|{k}|{pool}", + ) + ) + buttons.append(row) + buttons.append([InlineKeyboardButton("五星调频统计", callback_data=f"get_wish_log|{user_id}|{uid}|count|five")]) + return buttons + + async def wish_log_pool_choose(self, user_id: int, player_id: int, message: "Message"): + await message.reply_chat_action(ChatAction.TYPING) + gacha_log, status = await self.gacha_log.load_history_info(str(user_id), str(player_id)) + if not status: + raise GachaLogNotFound + buttons = self.gen_button(user_id, player_id, gacha_log) + if isinstance(self.wish_photo, str): + photo = self.wish_photo + else: + photo = open("resources/img/wish.jpg", "rb") + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + reply_message = await message.reply_photo( + photo=photo, + caption="请选择你要查询的卡池", + reply_markup=InlineKeyboardMarkup(buttons), + ) + if reply_message.photo: + self.wish_photo = reply_message.photo[-1].file_id + + async def wish_log_pool_send(self, user_id: int, uid: int, pool_type: "ZZZBannerType", message: "Message"): + await message.reply_chat_action(ChatAction.TYPING) + png_data = await self.rander_wish_log_analysis(user_id, uid, pool_type) + if isinstance(png_data, str): + reply = await message.reply_text(png_data) + if filters.ChatType.GROUPS.filter(message): + self.add_delete_message_job(reply) + self.add_delete_message_job(message) + else: + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + if png_data.file_type == FileType.DOCUMENT: + await png_data.reply_document(message, filename="调频统计.png") + else: + await png_data.reply_photo(message) + + @handler.command(command="signal_log", block=False) + @handler.message(filters=filters.Regex("^调频记录?(光锥|角色|常驻|新手)$"), block=False) + async def command_start_analysis(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: + user_id = await self.get_real_user_id(update) + uid, offset = self.get_real_uid_or_offset(update) + message = update.effective_message + pool_type = None + if args := self.get_args(context): + if "角色" in args: + pool_type = ZZZBannerType.CHARACTER + elif "武器" in args: + pool_type = ZZZBannerType.WEAPON + elif "常驻" in args: + pool_type = ZZZBannerType.STANDARD + elif "邦布" in args: + pool_type = ZZZBannerType.BANGBOO + self.log_user(update, logger.info, "调频记录命令请求 || 参数 %s", pool_type.name if pool_type else None) + try: + player_id = await self.get_player_id(user_id, uid, offset) + if pool_type is None: + await self.wish_log_pool_choose(user_id, player_id, message) + else: + await self.wish_log_pool_send(user_id, player_id, pool_type, message) + except GachaLogNotFound: + self.log_user(update, logger.info, "未找到调频记录") + buttons = [ + [ + InlineKeyboardButton( + "点我导入", url=create_deep_linked_url(context.bot.username, "signal_log_import") + ) + ] + ] + await message.reply_text( + WISHLOG_NOT_FOUND, + reply_markup=InlineKeyboardMarkup(buttons), + ) + + @handler.callback_query(pattern=r"^get_wish_log\|", block=False) + async def get_wish_log(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: + callback_query = update.callback_query + user = callback_query.from_user + message = callback_query.message + + async def get_wish_log_callback( + callback_query_data: str, + ) -> Tuple[str, str, int, int]: + _data = callback_query_data.split("|") + _user_id = int(_data[1]) + _uid = int(_data[2]) + _t = _data[3] + _result = _data[4] + logger.debug( + "callback_query_data函数返回 result[%s] user_id[%s] uid[%s] show_type[%s]", + _result, + _user_id, + _uid, + _t, + ) + return _result, _t, _user_id, _uid + + try: + pool, show_type, user_id, uid = await get_wish_log_callback(callback_query.data) + except IndexError: + await callback_query.answer("按钮数据已过期,请重新获取。", show_alert=True) + self.add_delete_message_job(message, delay=1) + return + if user.id != user_id: + await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) + return + if show_type == "count": + await self.get_wish_log_count(update, user_id, uid, pool) + else: + await self.get_wish_log_log(update, user_id, uid, pool) + + async def get_wish_log_log(self, update: "Update", user_id: int, uid: int, pool: str): + callback_query = update.callback_query + message = callback_query.message + + pool_type = GACHA_TYPE_LIST_REVERSE.get(pool) + await message.reply_chat_action(ChatAction.TYPING) + try: + png_data = await self.rander_wish_log_analysis(user_id, uid, pool_type) + except GachaLogNotFound: + png_data = "未找到调频记录" + if isinstance(png_data, str): + await callback_query.answer(png_data, show_alert=True) + self.add_delete_message_job(message, delay=1) + else: + await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + if png_data.file_type == FileType.DOCUMENT: + await png_data.reply_document(message, filename="调频统计.png") + self.add_delete_message_job(message, delay=1) + else: + await png_data.edit_media(message) + + async def get_wish_log_count(self, update: "Update", user_id: int, uid: int, pool: str): + callback_query = update.callback_query + message = callback_query.message + + all_five = pool == "five" + group = filters.ChatType.GROUPS.filter(message) + pool_type = GACHA_TYPE_LIST_REVERSE.get(pool) + await message.reply_chat_action(ChatAction.TYPING) + try: + if all_five: + png_data = await self.gacha_log.get_all_five_analysis(user_id, uid, self.assets_service) + else: + png_data = await self.gacha_log.get_pool_analysis(user_id, uid, pool_type, self.assets_service, group) + except GachaLogNotFound: + png_data = "未找到调频记录" + if isinstance(png_data, str): + await callback_query.answer(png_data, show_alert=True) + self.add_delete_message_job(message, delay=1) + else: + await self.add_theme_data(png_data, uid) + await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) + document = False + if png_data["hasMore"] and not group: + document = True + png_data["hasMore"] = False + await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if document else ChatAction.UPLOAD_PHOTO) + png = await self.template_service.render( + "zzz/gacha_count/gacha_count.html", + png_data, + full_page=True, + query_selector=".body_box", + file_type=FileType.DOCUMENT if document else FileType.PHOTO, + ) + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + if document: + await png.reply_document(message, filename="调频统计.png") + self.add_delete_message_job(message, delay=1) + else: + await png.edit_media(message) + + async def add_theme_data(self, data: Dict, player_id: int): + res = RESOURCE_DIR / "img" + data["avatar"] = (res / "avatar.png").as_uri() + data["background"] = (res / "home.png").as_uri() + return data + + @staticmethod + async def get_migrate_data( + old_user_id: int, new_user_id: int, old_players: List["Player"] + ) -> Optional[GachaLogMigrate]: + return await GachaLogMigrate.create(old_user_id, new_user_id, old_players) + + async def wish_log_use_by_inline( + self, update: "Update", context: "ContextTypes.DEFAULT_TYPE", pool_type: "ZZZBannerType" + ): + callback_query = update.callback_query + user = update.effective_user + user_id = user.id + uid = IInlineUseData.get_uid_from_context(context) + + self.log_user(update, logger.info, "调频记录命令请求 || 参数 %s", pool_type.name if pool_type else None) + notice = None + try: + render_result = await self.rander_wish_log_analysis(user_id, uid, pool_type) + if isinstance(render_result, str): + notice = render_result + else: + await render_result.edit_inline_media(callback_query, filename="调频统计.png") + except GachaLogNotFound: + self.log_user(update, logger.info, "未找到调频记录") + notice = "未找到调频记录" + if notice: + await callback_query.answer(notice, show_alert=True) + + async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: + types = { + "代理人": ZZZBannerType.CHARACTER, + "音擎": ZZZBannerType.WEAPON, + "邦布": ZZZBannerType.BANGBOO, + "常驻": ZZZBannerType.STANDARD, + } + data = [] + for k, v in types.items(): + data.append( + IInlineUseData( + text=f"{k}调频", + hash=f"signal_log_{v.value}", + callback=partial(self.wish_log_use_by_inline, pool_type=v), + player=True, + ) + ) + return data diff --git a/plugins/zzz/stats.py b/plugins/zzz/stats.py index 1a52be2..c9e1fe7 100644 --- a/plugins/zzz/stats.py +++ b/plugins/zzz/stats.py @@ -10,6 +10,7 @@ from core.services.template.models import RenderResult from core.services.template.services import TemplateService from gram_core.plugin.methods.inline_use_data import IInlineUseData from plugins.tools.genshin import GenshinHelper +from utils.const import RESOURCE_DIR from utils.log import logger from utils.uid import mask_number @@ -60,9 +61,6 @@ class PlayerStatsPlugins(Plugin): uid = client.player_id user_info = await client.get_zzz_user(uid) - # 因为需要替换线上图片地址为本地地址,先克隆数据,避免修改原数据 - user_info = user_info.copy(deep=True) - data = { "uid": mask_number(uid), "stats": user_info.stats, @@ -74,11 +72,11 @@ class PlayerStatsPlugins(Plugin): ("式舆防卫战防线", "cur_period_zone_layer_count"), ("获得邦布数", "buddy_num"), ], - "style": random.choice(["mondstadt", "liyue"]), # nosec + "style": "main", # nosec } - + await self.add_theme_data(data, uid) return await self.template_service.render( - "zzz/stats/stats.jinja2", + "zzz/stats/stats.html", data, {"width": 650, "height": 400}, full_page=True, @@ -113,6 +111,12 @@ class PlayerStatsPlugins(Plugin): return await render_result.edit_inline_media(callback_query) + async def add_theme_data(self, data, player_id: int): + res = RESOURCE_DIR / "img" + data["avatar"] = (res / "avatar.png").as_uri() + data["background"] = (res / "home.png").as_uri() + return data + async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: return [ IInlineUseData( diff --git a/plugins/zzz/weapon.py b/plugins/zzz/weapon.py new file mode 100644 index 0000000..a526571 --- /dev/null +++ b/plugins/zzz/weapon.py @@ -0,0 +1,86 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.constants import ChatAction, ParseMode +from telegram.ext import CallbackContext, filters + +from core.plugin import Plugin, handler +from core.services.game.services import GameCacheService +from core.services.search.models import StrategyEntry +from core.services.search.services import SearchServices +from core.services.wiki.services import WikiService +from metadata.shortname import weaponToTag, weaponToName +from utils.log import logger + + +class WeaponPlugin(Plugin): + """音擎图鉴查询""" + + KEYBOARD = [ + [InlineKeyboardButton(text="查看音擎列表并查询", switch_inline_query_current_chat="查看音擎列表并查询")] + ] + + def __init__( + self, + cache_service: GameCacheService = None, + wiki_service: WikiService = None, + search_service: SearchServices = None, + ): + self.cache_service = cache_service + self.wiki_service = wiki_service + self.search_service = search_service + + @handler.command(command="weapon", block=False) + @handler.message(filters=filters.Regex("^音擎图鉴查询(.*)"), block=False) + async def command_start(self, update: Update, context: CallbackContext) -> None: + message = update.effective_message + args = self.get_args(context) + if len(args) >= 1: + weapon_name = args[0] + else: + reply_message = await message.reply_text( + "请回复你要查询的音擎名称", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) + ) + if filters.ChatType.GROUPS.filter(reply_message): + self.add_delete_message_job(message) + self.add_delete_message_job(reply_message) + return + weapon_name = weaponToName(weapon_name) + file_path = self.wiki_service.raider.raider_light_cone_path / f"{weapon_name}.png" + if not file_path.exists(): + reply_message = await message.reply_text( + f"没有找到 {weapon_name} 的图鉴", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) + ) + if filters.ChatType.GROUPS.filter(reply_message): + self.add_delete_message_job(message) + self.add_delete_message_job(reply_message) + return + self.log_user(update, logger.info, "查询音擎图鉴命令请求 || 参数 %s", weapon_name) + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + caption = "From 米游社@听语惊花" + if file_id := await self.cache_service.get_weapon_cache(weapon_name): + await message.reply_photo( + photo=file_id, + caption=caption, + filename=f"{weapon_name}.png", + parse_mode=ParseMode.HTML, + ) + else: + reply_photo = await message.reply_photo( + photo=open(file_path, "rb"), + caption=caption, + filename=f"{weapon_name}.png", + parse_mode=ParseMode.HTML, + ) + if reply_photo.photo: + tags = weaponToTag(weapon_name) + photo_file_id = reply_photo.photo[0].file_id + await self.cache_service.set_weapon_cache(weapon_name, photo_file_id) + entry = StrategyEntry( + key=f"plugin:strategy:{weapon_name}", + title=weapon_name, + description=f"{weapon_name} 音擎图鉴", + tags=tags, + caption=caption, + parse_mode="HTML", + photo_file_id=photo_file_id, + ) + await self.search_service.add_entry(entry) diff --git a/requirements.txt b/requirements.txt index 2f393ca..7f0a38f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -88,7 +88,7 @@ rich==13.7.1 sentry-sdk==2.7.1 setuptools==70.2.0 shellingham==1.5.4 -simnet @ git+https://github.com/PaiGramTeam/SIMNet@05fcb568d6c1fe44a4f917c996198bfe62a00053 +simnet @ git+https://github.com/PaiGramTeam/SIMNet@074939d8818e6073be4a918b25d7deadd43a5b7b six==1.16.0 smmap==5.0.1 sniffio==1.3.1 diff --git a/resources/img/avatar.png b/resources/img/avatar.png new file mode 100644 index 0000000..7d57469 Binary files /dev/null and b/resources/img/avatar.png differ diff --git a/resources/img/home.png b/resources/img/home.png new file mode 100644 index 0000000..8119065 Binary files /dev/null and b/resources/img/home.png differ diff --git a/resources/img/wish.jpg b/resources/img/wish.jpg index 949adfe..2b3c505 100644 Binary files a/resources/img/wish.jpg and b/resources/img/wish.jpg differ diff --git a/resources/zzz/gacha_count/example.html b/resources/zzz/gacha_count/example.html new file mode 100644 index 0000000..9fddf2c --- /dev/null +++ b/resources/zzz/gacha_count/example.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + +
+
+
ID: 10001
+

抽卡统计-角色祈愿

+ +
+
+
+
+

「枫原万叶、可莉」

+ 98抽 +
+ 2022-08-02 - 2022-08-02 +
+
+
+
+ 20 + +
+
+
+
*完整数据请私聊查看
+ +
+ + diff --git a/resources/zzz/gacha_count/gacha_count.css b/resources/zzz/gacha_count/gacha_count.css new file mode 100644 index 0000000..fab8f33 --- /dev/null +++ b/resources/zzz/gacha_count/gacha_count.css @@ -0,0 +1,226 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("./../../fonts/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "HYWenHei-55W"; + src: url("../../fonts/HYWenHei-85W.ttf"); + font-weight: normal; + font-style: normal; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + user-select: none; +} + +.header { + /*background: #e0dad3 url(../gacha_log/img/starrail.png) no-repeat right;*/ + box-shadow: 0 0 8px #72a2ae79; + background-size: cover; + background-position: top; +} + +.frame { + border-color: #cdbea8; + color: white; +} + +body { + font-size: 16px; + width: 1286px; + color: #1E1F20; + transform: scale(1.5); + transform-origin: 0 0; +} + +.container { + width: 1286px; + padding: 20px 15px 10px 15px; + background-color: #F5F6FB; +} + +.head_box { + border-radius: 15px; + font-family: tttgbnumber, serif; + padding: 10px 20px; + position: relative; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + +} + +.head_box .id_text { + font-size: 24px; +} + +.head_box .day_text { + font-size: 20px; +} + +.head_box .starrail_logo { + position: absolute; + top: 5px; + right: 30px; + width: 120px; +} + +.base_info { + position: relative; + padding-left: 10px; +} + +.uid { + font-family: tttgbnumber, serif; +} + +.pool_box { + font-family: HYWenHei-55W, serif; + border-radius: 12px; + margin-top: 20px; + margin-bottom: 20px; + padding: 10px 5px 5px 5px; + background: #FFF; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} + +.title_box { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.title { + white-space: nowrap; + max-width: 210px; + overflow: hidden; +} + +.name_box { + display: flex; + align-items: center; + flex: 1; +} + +.title_box .date { + margin-right: 10px; +} + +.list_box { + display: flex; + flex-wrap: wrap; +} + +.item { + margin: 0 0 10px 10px; + border-radius: 7px; + overflow: hidden; + box-shadow: 0 2px 6px 0 rgb(132 93 90 / 30%); + height: 70px; + width: 70px; + background: #E9E5DC; + position: relative; +} + +.item .role_img { + width: 100%; + overflow: hidden; + background-size: 100%; + background-repeat: no-repeat; + position: absolute; + top: 0; + /* filter: contrast(95%); */ +} + +.item .num { + position: absolute; + top: 0; + right: 0; + z-index: 9; + font-size: 18px; + text-align: center; + color: #FFF; + border-radius: 3px; + padding: 1px 5px; + background: rgb(0 0 0 / 50%); + font-family: "tttgbnumber", serif; +} + +.label_301 { + background-color: rgb(235 106 75); +} + +.label_302 { + background-color: #E69449; +} + +.label_200 { + background-color: #757CC8; +} + +.label { + color: #FFF; + border-radius: 10px; + font-size: 16px; + padding: 2px 7px; + vertical-align: 2px; +} + +.bg5 { + background-image: url(./../../genshin/abyss/background/roleStarBg5.png); + width: 100%; + height: 70px; + /* filter: brightness(1.1); */ + background-size: 100%; + background-repeat: no-repeat; +} + +.bg4 { + width: 100%; + height: 70px; + background-image: url(./../../genshin/abyss/background/roleStarBg4.png); + background-size: 100%; + background-repeat: no-repeat; +} + +.list_box .item .life1 { + background-color: #62A8EA; +} + +.list_box .item .life2 { + background-color: #62A8EA; +} + +.list_box .item .life3 { + background-color: #45B97C; +} + +.list_box .item .life4 { + background-color: #45B97C; +} + +.list_box .item .life5 { + background-color: #FF5722; +} + +.list_box .item .life6 { + background-color: #FF5722; +} + +.logo { + font-size: 14px; + font-family: "tttgbnumber", serif; + text-align: center; + color: #7994A7; +} + +.hasMore { + font-size: 12px; + margin: -6px 0 10px 6px; + color: #7F858A; +} diff --git a/resources/zzz/gacha_count/gacha_count.html b/resources/zzz/gacha_count/gacha_count.html new file mode 100644 index 0000000..0e045af --- /dev/null +++ b/resources/zzz/gacha_count/gacha_count.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + +
+
+
+
+ Avatar +
+

ID: {{ uid }}

+

+ 跃迁统计-{{ typeName }} +

+
+
+
+
+ {% for val in pool %} +
+
+
+

「{{ val.name }}」

+ {{ val.count }}抽 +
+ {% if typeName != "常驻跃迁" %} + {{ val.start }} - {{ val.end }} + {% endif %} +
+
+ {% for v in val.list %} +
+
+ {{ v.count }} + +
+ {% endfor %} +
+
+ {% endfor %} + {% if hasMore %} +
*完整数据请私聊查看
+ {% endif %} + +
+ + diff --git a/resources/zzz/gacha_log/example.html b/resources/zzz/gacha_log/example.html new file mode 100644 index 0000000..a353278 --- /dev/null +++ b/resources/zzz/gacha_log/example.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + + +
+
+ +
+
+ ID: 10001 +
+

+ 81抽 + 角色祈愿池 · 欧 +

+ +
+ +
+
数据总览
+
+
+
1
+
未出五星
+
+
+ +
+ + 五星历史 2022-10-07 01:10 ~ 2022-10-07 23:10 + +
+ + +
+
+ UP + + +
80
+
+
+ +
+ + 四星最近历史 + +
+ +
+
+ + +
10
+
+
+
+ +
+
+ + \ No newline at end of file diff --git a/resources/zzz/gacha_log/gacha_log.css b/resources/zzz/gacha_log/gacha_log.css new file mode 100644 index 0000000..cb0cf69 --- /dev/null +++ b/resources/zzz/gacha_log/gacha_log.css @@ -0,0 +1,345 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("./../../fonts/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} + +.header { + /*background: #e0dad3 url(./img/starrail.png) no-repeat right;*/ + box-shadow: 0 0 8px #72a2ae79; + background-size: cover; + background-position: top; +} + +.frame { + border-color: #cdbea8; + color: white; +} + +body { + font-size: 18px; + color: #1e1f20; + font-family: PingFangSC-Medium, PingFang SC, sans-serif; + transform: scale(1.5); + transform-origin: 0 0; + width: 980px +} + +.container { + width: 980px; + padding: 20px 15px 10px 15px; + background-color: #f5f6fb; +} + +.head_box { + border-radius: 9999px; + font-family: tttgbnumber, sans-serif; + padding: 10px 20px; + position: relative; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); +} + +.head_box .id_text { + font-size: 24px; +} + +.head_box .day_text { + font-size: 20px; +} + +.head_box .starrail_logo { + position: absolute; + top: 10px; + right: 30px; + width: 120px; +} + +.logo { + font-size: 12px; + font-family: "tttgbnumber", serif; + text-align: center; + color: #7994a7; + position: relative; + padding-left: 10px; +} + +.data_box { + border-radius: 15px; + margin-top: 20px; + margin-bottom: 10px; + padding: 20px 0 5px 10px; + background: #fff; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} + +.tab_lable { + position: absolute; + top: -10px; + left: -8px; + background: #d4b98c; + color: #fff; + font-size: 14px; + padding: 3px 10px; + border-radius: 15px 0 15px 15px; + z-index: 20; +} + +.data_line { + display: flex; + justify-content: space-around; + margin-bottom: 14px; + padding-right: 10px; +} + +.data_line_item { + width: 100px; + text-align: center; + + /* margin: 0 20px; */ +} + +.num { + font-family: tttgbnumber, serif; + font-size: 24px; +} + +.num .unit { + font-size: 12px; +} + +.data_box .lable { + font-size: 14px; + color: #7f858a; + line-height: 1; + margin-top: 3px; +} + +.info_box_border { + border-radius: 15px; + + /* margin-top: 20px; */ + margin-bottom: 20px; + padding: 6px 0 5px 10px; + background: #fff; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} + +.card_list { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +.card_list .item { + margin: 0 8px 10px 0; + border-radius: 7px; + box-shadow: 0 2px 6px 0 rgb(132 93 90 / 30%); + height: 90px; + position: relative; + overflow: hidden; + background: #e7e5d9; +} + +.card_list .item img { + width: 70px; + height: 70px; + border-radius: 7px 7px 20px 0; +} + +.card_list .item.star5 img { + background-image: url(./../../genshin/abyss/background/roleStarBg5.png); + width: 100%; + height: 70px; + /* filter: brightness(1.1); */ + background-size: 100%; + background-repeat: no-repeat; +} + +.card_list .item.star4 img { + width: 100%; + height: 70px; + background-image: url(./../../genshin/abyss/background/roleStarBg4.png); + background-size: 100%; + background-repeat: no-repeat; +} + +.card_list .item .num { + position: absolute; + top: 0; + right: 0; + z-index: 9; + font-size: 18px; + text-align: center; + color: #fff; + border-radius: 3px; + padding: 1px 5px; + background: rgb(0 0 0 / 50%); + font-family: "tttgbnumber", serif; +} + +.card_list .item .name, +.card_list .item .num_name { + position: absolute; + top: 71px; + left: 0; + z-index: 9; + font-size: 12px; + text-align: center; + width: 100%; + height: 16px; + line-height: 18px; +} + +.card_list .item .num_name { + font-family: "tttgbnumber", serif; + font-size: 16px; +} + +.base_info { + position: relative; + padding-left: 10px; + margin: 5px 10px; +} + +.uid::before { + content: " "; + position: absolute; + width: 5px; + height: 24px; + border-radius: 1px; + left: 0; + top: 0; + background: #d3bc8d; +} + +.label_301 { + background-color: rgb(235 106 75); +} + +.label_302 { + background-color: #e69449; +} + +.label_200 { + background-color: #757cc8; +} + +.label { + color: #fff!important; + border-radius: 10px; + font-size: 12px; + padding: 2px 7px; + vertical-align: 2px; +} + +.ritem { + display: flex; + font-size: 12px; + margin-bottom: 5px; +} + +.info_role { + display: flex; + flex-wrap: wrap; + padding: 0 0 5px 9px; +} + +.ritem .role { + width: 20px; + height: 20px; + background-color: #ffb285; + border-radius: 100%; +} + +.ritem .weapon_box { + overflow: hidden; + width: 20px; + height: 20px; + border-radius: 100%; +} + +.ritem .weapon { + width: 20px; + height: 20px; + background-color: #ffb285; + border-radius: 100%; + transform: scale(1.5); + -webkit-transform: scale(1.5); +} + +.ritem .role_text { + margin: 2px 3px 0 2px; + display: flex; + align-items: baseline; +} + +.ritem .role_name { + width: 24px; + white-space: nowrap; + overflow: hidden; +} + +.ritem .role_num { + width: 24px; +} + +.line_box { + height: 32px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #7d7d7d; + padding-bottom: 5px; +} + +.line_box .line { + height: 2px; + flex-grow: 1; + background-color: #ebebeb; + margin: 0 10px; +} + +.red { + color: #f21000; +} + +.orange { + color: #ff8d00; +} + +.green { + color: #12d88c; +} + +.blue { + color: #4169e1; +} + +.purple { + color: #7500ff; +} + +.minimum { + position: absolute; + top: 0; + right: 0; + z-index: 9; + font-size: 12px; + text-align: center; + color: #fff; + border-radius: 3px; + padding: 1px 3px; + background-color: rgb(0 0 0 / 80%); + font-family: "tttgbnumber", serif; +} + +.hasMore { + font-size: 12px; + margin: 6px 0; + color: #7f858a; +} diff --git a/resources/zzz/gacha_log/gacha_log.html b/resources/zzz/gacha_log/gacha_log.html new file mode 100644 index 0000000..0cc4c39 --- /dev/null +++ b/resources/zzz/gacha_log/gacha_log.html @@ -0,0 +1,83 @@ + + + + + + + + + Title + + +
+
+
+
+ Avatar +
+

ID: {{ uid }}

+

+ {{ allNum }}抽 + {{ typeName }} +

+
+
+
+ +
+
数据总览
+ {% for val in line %} +
+ {% for item in val %} +
+
+ {{item.num}}{{item.unit}} +
+
{{item.lable}}
+
+ {% endfor %} +
+ {% endfor %} + +
+ + 五星历史 {{firstTime}} ~ {{lastTime}} + +
+ +
+ {% for val in fiveLog %} +
+ {% if val.isUp %} + UP + {% endif %} + + +
{{ val.count }}
+
+ {% endfor %} +
+ +
+ + 四星最近历史 + +
+ +
+ {% for val in fourLog %} +
+ + +
{{ val.count }}
+
+ {% endfor %} +
+
+ +
+
+ + diff --git a/resources/zzz/gacha_log/img/starrail.png b/resources/zzz/gacha_log/img/starrail.png new file mode 100644 index 0000000..b7451eb Binary files /dev/null and b/resources/zzz/gacha_log/img/starrail.png differ diff --git a/resources/zzz/gacha_log/img/提纳里.png b/resources/zzz/gacha_log/img/提纳里.png new file mode 100644 index 0000000..fb923ac Binary files /dev/null and b/resources/zzz/gacha_log/img/提纳里.png differ diff --git a/resources/zzz/stats/background/liyue.png b/resources/zzz/stats/background/liyue.png deleted file mode 100644 index 1a16959..0000000 Binary files a/resources/zzz/stats/background/liyue.png and /dev/null differ diff --git a/resources/zzz/stats/background/mondstadt.png b/resources/zzz/stats/background/mondstadt.png deleted file mode 100644 index d36b7df..0000000 Binary files a/resources/zzz/stats/background/mondstadt.png and /dev/null differ diff --git a/resources/zzz/stats/example/liyue.css b/resources/zzz/stats/example/liyue.css deleted file mode 100644 index 4cca478..0000000 --- a/resources/zzz/stats/example/liyue.css +++ /dev/null @@ -1,62 +0,0 @@ -:root { - --primary: #ffeabd; -} - -body { - background-color: #f5f6fb; -} - -.header { - background-image: url(../background/liyue.png); - box-shadow: 0 0 16px rgb(255 233 144 / 50%); -} - -.box { - background-color: #9c433d; - box-shadow: 0 0 16px rgb(255 233 144 / 50%); -} - -.box-title { - background-color: rgb(255, 200, 122, 0.1); - --tw-ring-color: #ff9966; -} - -.pointer-bar { - width: 95%; - height: 8px; - display: inline-block; - background-color: rgb(0, 0, 0, 0.1); - border-radius: 0.25rem; -} - -.pointer-progress-bar { - border-radius: 0.25rem; - height: 100%; - background: linear-gradient(to bottom, #f5efcd, #f8eabd, #ffdf90); -} - -.name { - background: linear-gradient(to bottom, #ffffff, #ffeabd, #ffdf90); - background-clip: text; - -webkit-background-clip: text; - color: transparent; -} - -.uid { - color: var(--primary); - background: linear-gradient(to right, rgb(0, 0, 0, 0), #cc6666, rgb(0, 0, 0, 0)); -} - -.about { - background-color: #e0dad3; - color: #8a4d30; -} - -.box-stats { - color: var(--primary); -} - -.box-stats-label { - color: var(--primary) !important; - opacity: 0.65; -} diff --git a/resources/zzz/stats/example/liyue.html b/resources/zzz/stats/example/liyue.html deleted file mode 100644 index 1abd585..0000000 --- a/resources/zzz/stats/example/liyue.html +++ /dev/null @@ -1,256 +0,0 @@ - - - - Title - - - - - - -
-
-
-

- 小何 - lv.58 -

-

UID - 125324176

-
-
-
-
-

- 数据总览 -

-
-
-
493
-
活跃天数
-
- -
-
536
-
成就达成数
-
- -
-
38
-
获取角色数
-
- -
-
12-3
-
深境螺旋
-
- -
-
227
-
解锁传送点
-
- -
-
41
-
解锁秘境
-
- -
-
58
-
奇馈宝箱数
-
- -
-
128
-
华丽宝箱数
-
- -
-
316
-
珍贵宝箱数
-
- -
-
1184
-
精致宝箱数
-
- -
-
1594
-
普通宝箱数
-
- -
-
65
-
风神瞳
-
- -
-
131
-
岩神瞳
-
- -
-
180
-
雷神瞳
-
- -
-
80
-
草神瞳
-
-
-
- -
-

- 世界探索 -

-
-
- -
须弥
-
28.0%
-
-
-
-
- -
- -
层岩巨渊·地下矿区
-
98.7%
-
-
-
-
- -
- -
层岩巨渊
-
92.9%
-
-
-
-
- -
- -
渊下宫
-
98.3%
-
-
-
-
- -
- -
稻妻
-
100.0%
-
-
-
-
- -
- -
龙脊雪山
-
83.6%
-
-
-
-
- -
- -
璃月
-
95.9%
-
-
-
-
- -
- -
蒙德
-
100.0%
-
-
-
-
-
-
- -
- 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 -
-
-
- - diff --git a/resources/zzz/stats/example/mondstadt.css b/resources/zzz/stats/example/mondstadt.css deleted file mode 100644 index 17979cd..0000000 --- a/resources/zzz/stats/example/mondstadt.css +++ /dev/null @@ -1,49 +0,0 @@ -body { - background-color: #f5f6fb; -} - -.header { - background-image: url(../background/mondstadt.png); - box-shadow: 0 0 8px rgb(123 242 248 / 50%); -} - -.box { - background-color: #fdfdf3; - box-shadow: 0 0 8px rgb(123 242 248 / 50%); -} - -.box-title { - background-color: #43849abb; - --tw-ring-color: #43849a; -} - -.pointer-bar { - width: 95%; - height: 8px; - display: inline-block; - background-color: rgb(0, 0, 0, 0.2); - border-radius: 0.25rem; -} - -.pointer-progress-bar { - border-radius: 0.25rem; - height: 100%; - background: #fff6e2; -} - -.name { - background: linear-gradient(to bottom, #66bbee, #5ddddd, #55dddd); - background-clip: text; - -webkit-background-clip: text; - color: transparent; -} - -.uid { - color: #fff; - background: linear-gradient(to right, rgb(0, 0, 0, 0), #5ddddd, rgb(0, 0, 0, 0)); -} - -.about { - background-color: #e0dad3; - color: #8a4d30; -} \ No newline at end of file diff --git a/resources/zzz/stats/example/mondstadt.html b/resources/zzz/stats/example/mondstadt.html deleted file mode 100644 index 1d9c14e..0000000 --- a/resources/zzz/stats/example/mondstadt.html +++ /dev/null @@ -1,261 +0,0 @@ - - - - - Title - - - - - - -
-
-
-

- 小何 - lv.58 -

-

UID - 125324176

-
-
-
-
-

- 数据总览 -

-
- -
-
491
-
活跃天数
-
- -
-
536
-
成就达成数
-
- -
-
38
-
获取角色数
-
- -
-
12-3
-
深境螺旋
-
- -
-
227
-
解锁传送点
-
- -
-
41
-
解锁秘境
-
- -
-
58
-
奇馈宝箱数
-
- -
-
127
-
华丽宝箱数
-
- -
-
316
-
珍贵宝箱数
-
- -
-
1180
-
精致宝箱数
-
- -
-
1591
-
普通宝箱数
-
- -
-
65
-
风神瞳
-
- -
-
131
-
岩神瞳
-
- -
-
180
-
雷神瞳
-
- -
-
79
-
草神瞳
-
- -
-
- -
-

- 世界探索 -

-
- -
- -
须弥
-
26.0%
-
-
-
-
- -
- -
层岩巨渊·地下矿区
-
98.7%
-
-
-
-
- -
- -
层岩巨渊
-
92.9%
-
-
-
-
- -
- -
渊下宫
-
98.3%
-
-
-
-
- -
- -
稻妻
-
100.0%
-
-
-
-
- -
- -
龙脊雪山
-
83.6%
-
-
-
-
- -
- -
璃月
-
95.9%
-
-
-
-
- -
- -
蒙德
-
100.0%
-
-
-
-
- -
-
- -
- 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 -
-
-
- - \ No newline at end of file diff --git a/resources/zzz/stats/items/star.png b/resources/zzz/stats/items/star.png new file mode 100644 index 0000000..1c7498b Binary files /dev/null and b/resources/zzz/stats/items/star.png differ diff --git a/resources/zzz/stats/liyue.css b/resources/zzz/stats/liyue.css deleted file mode 100644 index 5e73809..0000000 --- a/resources/zzz/stats/liyue.css +++ /dev/null @@ -1,62 +0,0 @@ -:root { - --primary: #ffeabd; -} - -body { - background-color: #f5f6fb; -} - -.header { - background-image: url(./background/liyue.png); - box-shadow: 0 0 16px rgb(255 233 144 / 50%); -} - -.box { - background-color: #9c433d; - box-shadow: 0 0 16px rgb(255 233 144 / 50%); -} - -.box-title { - background-color: rgb(255, 200, 122, 0.1); - --tw-ring-color: #ff9966; -} - -.pointer-bar { - width: 95%; - height: 8px; - display: inline-block; - background-color: rgb(0, 0, 0, 0.1); - border-radius: 0.25rem; -} - -.pointer-progress-bar { - border-radius: 0.25rem; - height: 100%; - background: linear-gradient(to bottom, #f5efcd, #f8eabd, #ffdf90); -} - -.name { - background: linear-gradient(to bottom, #ffffff, #ffeabd, #ffdf90); - background-clip: text; - -webkit-background-clip: text; - color: transparent; -} - -.uid { - color: var(--primary); - background: linear-gradient(to right, rgb(0, 0, 0, 0), #cc6666, rgb(0, 0, 0, 0)); -} - -.about { - background-color: #e0dad3; - color: #8a4d30; -} - -.box-stats { - color: var(--primary); -} - -.box-stats-label { - color: var(--primary) !important; - opacity: 0.65; -} diff --git a/resources/zzz/stats/main.css b/resources/zzz/stats/main.css new file mode 100644 index 0000000..184d4a0 --- /dev/null +++ b/resources/zzz/stats/main.css @@ -0,0 +1,56 @@ +body { + background-color: #f5f6fb; +} + +.header { + /*background-image: url(../../bot/help/background/header.png);*/ + box-shadow: 0 0 8px #72a2ae79; +} + +.box { + background-color: #f4f2e4; + box-shadow: 0 0 8px #72a2ae79; +} + +.pointer-bar { + width: 95%; + height: 8px; + display: inline-block; + background-color: rgb(0, 0, 0, 0.2); + border-radius: 0.25rem; +} + +.pointer-progress-bar { + border-radius: 0.25rem; + height: 100%; + background: #fff6e2; +} + +.name { + color: #ffffee; + text-shadow: 0 0.08em 0.1em #00000093, 0 0.1em 0.3em rgba(0, 0, 0, 0.4); +} + +.uid { + color: #fff; + background: linear-gradient( + to right, + rgb(0, 0, 0, 0), + #3f7587 25%, + #3f7587 75%, + rgb(0, 0, 0, 0) + ); +} + +.about { + background-color: #e0dad3; + color: #8a4d30; +} + +.frame-pic { + border-color: #fdfdf356; +} + +.frame { + border-color: #cdbea8; +} diff --git a/resources/zzz/stats/mondstadt.css b/resources/zzz/stats/mondstadt.css deleted file mode 100644 index 4122e18..0000000 --- a/resources/zzz/stats/mondstadt.css +++ /dev/null @@ -1,49 +0,0 @@ -body { - background-color: #f5f6fb; -} - -.header { - background-image: url(background/mondstadt.png); - box-shadow: 0 0 8px rgb(123 242 248 / 50%); -} - -.box { - background-color: #fdfdf3; - box-shadow: 0 0 8px rgb(123 242 248 / 50%); -} - -.box-title { - background-color: #43849abb; - --tw-ring-color: #43849a; -} - -.pointer-bar { - width: 95%; - height: 8px; - display: inline-block; - background-color: rgb(0, 0, 0, 0.2); - border-radius: 0.25rem; -} - -.pointer-progress-bar { - border-radius: 0.25rem; - height: 100%; - background: #fff6e2; -} - -.name { - background: linear-gradient(to bottom, #66bbee, #5ddddd, #55dddd); - background-clip: text; - -webkit-background-clip: text; - color: transparent; -} - -.uid { - color: #fff; - background: linear-gradient(to right, rgb(0, 0, 0, 0), #5ddddd, rgb(0, 0, 0, 0)); -} - -.about { - background-color: #e0dad3; - color: #8a4d30; -} \ No newline at end of file diff --git a/resources/zzz/stats/old/info.css b/resources/zzz/stats/old/info.css deleted file mode 100644 index 2b88125..0000000 --- a/resources/zzz/stats/old/info.css +++ /dev/null @@ -1,92 +0,0 @@ -body { - background-repeat: no-repeat; - background-size: cover; - background-position: center; - background-attachment: fixed; -} - - -#container { - -} - -.account-center-header { - padding: 10px 8px; - background-color: rgba(225, 225, 225, 0.5); - /*background-image: url("./0.jpg"); - background-size: cover;*/ -} - -.user-avatar { - width: 100%; - height: 100%; -} - -.user-info-1 { - padding: 10px 8px; - background-color: rgba(225, 225, 225, 0.5); -} - -.world-exploration { - padding: 10px 8px; - background-color: rgba(225, 225, 225, 0.5); -} - -.teapot { - padding: 10px 8px; - background-color: rgba(225, 225, 225, 0.5); -} - -.account-center-header-avatar { - width: 120px; - height: 120px; -} - -.teapot-info-icon { - height: 96px; - overflow: hidden; -} - -.teapot-info-img { - width: 100%; -} - -.world-exploration-info { - border: 2px solid rgb(0, 0, 0, 0.2); -} - -.world-exploration-info-icon { - height: 96px; - overflow: hidden; -} - -.world-exploration-info-img { - filter: brightness(0); - height: 100%; -} - -.world-exploration-info-hr-1 { - width: 95%; -} - -.world-exploration-info-pointer-bar-body { - width: 95%; - height: 4px; - display: inline-block; - border-radius: 2px; - background-color: rgb(0, 0, 0, 0.3); -} - -.world-exploration-info-pointer-progress-bar { - height: 100%; - background-color: #000000; - border-radius: 2px; -} - -.background-color { - background-color: rgb(225, 225, 225, 0.75); -} - -.teapot-info-name { - background-color: rgba(225, 225, 225, 0.5); -} \ No newline at end of file diff --git a/resources/zzz/stats/old/info.html b/resources/zzz/stats/old/info.html deleted file mode 100644 index ca027e5..0000000 --- a/resources/zzz/stats/old/info.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - Title - - - - - - -
-
- - - -
-
- - \ No newline at end of file diff --git a/resources/zzz/stats/stats.html b/resources/zzz/stats/stats.html new file mode 100644 index 0000000..a2b74fc --- /dev/null +++ b/resources/zzz/stats/stats.html @@ -0,0 +1,73 @@ + + + + + Title + + + + + + +
+
+
+ Avatar +
+

+ {{ nickname }} +

+

UID - {{ uid }}

+
+
+
+
+
+

+ + 数据总览 +

+
+ {% for label, key in stats_labels %} +
+ {% set value = stats[key] %} + {% if value == "" %} + {% set value = "-" %} + {% endif %} +
{{ value }}
+
{{ label }}
+
+ {% endfor %} + {% if rogue %} + {% for label, key in rogue_labels %} +
+
{{ rogue[key] }}
+
{{ label }}
+
+ {% endfor %} + {% endif %} + {% if ledger %} + {% for label, key in ledger_labels %} +
+
{{ ledger[key] }}
+
{{ label }}
+
+ {% endfor %} + {% endif %} +
+ +
+ 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 +
+
+
+
+ + diff --git a/resources/zzz/stats/stats.jinja2 b/resources/zzz/stats/stats.jinja2 deleted file mode 100644 index 5e688ac..0000000 --- a/resources/zzz/stats/stats.jinja2 +++ /dev/null @@ -1,45 +0,0 @@ - - - - - Title - - - - - - -
-
-
-

- {{ nickname }} -

-

UID - {{ uid }}

-
-
-
-
-

- 数据总览 -

-
- {% for label, key in stats_labels %} -
-
{{ stats[key] }}
-
{{ label }}
-
- {% endfor %} -
-
- -
- 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 -
-
-
- -