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