+
diff --git a/core/assets/__init__.py b/core/assets/__init__.py
new file mode 100644
index 0000000..2ff27ec
--- /dev/null
+++ b/core/assets/__init__.py
@@ -0,0 +1,8 @@
+from core.service import init_service
+from .service import AssetsService
+
+
+@init_service
+def create_wiki_service():
+ _service = AssetsService()
+ return _service
diff --git a/core/assets/service.py b/core/assets/service.py
new file mode 100644
index 0000000..acf9052
--- /dev/null
+++ b/core/assets/service.py
@@ -0,0 +1,164 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from ssl import SSLZeroReturnError
+from typing import ClassVar, Optional, Union
+
+from aiofiles import open as async_open
+from httpx import AsyncClient, HTTPError
+
+from core.service import Service
+from metadata.honey import HONEY_RESERVED_ID_MAP
+from metadata.shortname import roleToId, roles
+from modules.wiki.base import SCRAPE_HOST
+from utils.const import PROJECT_ROOT
+from utils.helpers import mkdir
+from utils.typedefs import StrOrURL
+
+ASSETS_PATH = PROJECT_ROOT.joinpath('resources/assets')
+ASSETS_PATH.mkdir(exist_ok=True)
+
+
+class _AssetsService(ABC):
+ _dir: ClassVar[Path]
+
+ id: str
+ type: str
+
+ @property
+ def path(self) -> Path:
+ return mkdir(self._dir.joinpath(self.id))
+
+ def __init__(self, client: AsyncClient):
+ self._client = client
+
+ @abstractmethod
+ def __call__(self, target):
+ pass
+
+ def __init_subclass__(cls, **kwargs):
+ cls.type = cls.__name__.lstrip('_').split('Assets')[0].lower()
+ cls._dir = ASSETS_PATH.joinpath(cls.type)
+ cls._dir.mkdir(exist_ok=True)
+ return cls
+
+ async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Optional[Path]:
+ import asyncio
+ for _ in range(retry):
+ try:
+ response = await self._client.get(url, follow_redirects=False)
+ except (HTTPError, SSLZeroReturnError):
+ await asyncio.sleep(1)
+ continue
+ if response.status_code != 200:
+ return None
+ async with async_open(path, 'wb') as file:
+ await file.write(response.content)
+ return path
+
+ @abstractmethod
+ async def icon(self) -> Path:
+ pass
+
+
+class _CharacterAssets(_AssetsService):
+ # noinspection SpellCheckingInspection
+ def __call__(self, target: Union[str, int]) -> "_CharacterAssets":
+ if isinstance(target, int):
+ if target == 10000005:
+ self.id = 'playerboy_005'
+ elif target == 10000007:
+ self.id = 'playergirl_007'
+ else:
+ self.id = f"{roles[target][2]}_{str(target)[-3:]}"
+ elif not target[-1].isdigit():
+ target = roleToId(target)
+ self.id = f"{roles[target][2]}_{str(target)[-3:]}"
+ else:
+ self.id = target
+ return self
+
+ async def icon(self) -> Path:
+ if (path := self.path.joinpath('icon.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_icon.webp')), path)
+
+ async def side(self) -> Path:
+ if (path := self.path.joinpath('side.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_side_icon.webp')), path)
+
+ async def gacha(self) -> Path:
+ if (path := self.path.joinpath('gacha.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_card.webp')), path)
+
+ async def splash(self) -> Optional[Path]:
+ if (path := self.path.joinpath('splash.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_splash.webp')), path)
+
+
+class _WeaponAssets(_AssetsService):
+ def __call__(self, target: str) -> '_WeaponAssets':
+ if not target[-1].isdigit():
+ self.id = HONEY_RESERVED_ID_MAP['weapon'][target][0]
+ else:
+ self.id = target
+ return self
+
+ async def icon(self) -> Path:
+ if (path := self.path.joinpath('icon.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}.webp')), path)
+
+ async def awakened(self) -> Path:
+ if (path := self.path.joinpath('awakened.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_awaken_icon.webp')), path)
+
+ async def gacha(self) -> Path:
+ if (path := self.path.joinpath('gacha.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_icon.webp')), path)
+
+
+class _MaterialAssets(_AssetsService):
+
+ def __call__(self, target) -> "_MaterialAssets":
+ if not target[-1].isdigit():
+ self.id = HONEY_RESERVED_ID_MAP['material'][target][0]
+ else:
+ self.id = target
+ return self
+
+ async def icon(self) -> Path:
+ if (path := self.path.joinpath('icon.webp')).exists():
+ return path
+
+ return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}.webp')), path)
+
+
+class AssetsService(Service):
+ """asset服务
+
+ 用于储存和管理 asset :
+ 当对应的 asset (如某角色图标)不存在时,该服务会先查找本地。
+ 若本地不存在,则从网络上下载;若存在,则返回其路径
+ """
+
+ character: _CharacterAssets
+ weapon: _WeaponAssets
+ material: _MaterialAssets
+
+ def __init__(self):
+ self.client = AsyncClient()
+ self.character = _CharacterAssets(self.client)
+ self.weapon = _WeaponAssets(self.client)
+ self.material = _MaterialAssets(self.client)
diff --git a/core/bot.py b/core/bot.py
index 20a7b11..5a0c11f 100644
--- a/core/bot.py
+++ b/core/bot.py
@@ -1,14 +1,23 @@
import asyncio
import inspect
import os
+from asyncio import CancelledError
from importlib import import_module
from multiprocessing import RLock as Lock
from pathlib import Path
from typing import Any, Callable, ClassVar, Dict, Iterator, List, NoReturn, Optional, TYPE_CHECKING, Type, TypeVar
import pytz
+from async_timeout import timeout
from telegram.error import NetworkError, TimedOut
-from telegram.ext import AIORateLimiter, Application as TgApplication, Defaults, JobQueue, MessageHandler
+from telegram.ext import (
+ AIORateLimiter,
+ Application as TgApplication,
+ CallbackContext,
+ Defaults,
+ JobQueue,
+ MessageHandler,
+)
from telegram.ext.filters import StatusUpdate
from core.config import BotConfig, config # pylint: disable=W0611
@@ -21,7 +30,6 @@ from utils.log import logger
if TYPE_CHECKING:
from telegram import Update
- from telegram.ext import CallbackContext
__all__ = ['bot']
@@ -45,12 +53,7 @@ class Bot:
_services: Dict[Type[T], T] = {}
_running: bool = False
- def init_inject(self, target: Callable[..., T]) -> T:
- """用于实例化Plugin的方法。用于给插件传入一些必要组件,如 MySQL、Redis等"""
- if isinstance(target, type):
- signature = inspect.signature(target.__init__)
- else:
- signature = inspect.signature(target)
+ def _inject(self, signature: inspect.Signature, target: Callable[..., T]) -> T:
kwargs = {}
for name, parameter in signature.parameters.items():
if name != 'self' and parameter.annotation != inspect.Parameter.empty:
@@ -58,6 +61,17 @@ class Bot:
kwargs[name] = value
return target(**kwargs)
+ def init_inject(self, target: Callable[..., T]) -> T:
+ """用于实例化Plugin的方法。用于给插件传入一些必要组件,如 MySQL、Redis等"""
+ if isinstance(target, type):
+ signature = inspect.signature(target.__init__)
+ else:
+ signature = inspect.signature(target)
+ return self._inject(signature, target)
+
+ async def async_inject(self, target: Callable[..., T]) -> T:
+ return await self._inject(inspect.signature(target), target)
+
def _gen_pkg(self, root: Path) -> Iterator[str]:
"""生成可以用于 import_module 导入的字符串"""
for path in root.iterdir():
@@ -84,7 +98,7 @@ class Bot:
try:
plugin: PluginType = self.init_inject(plugin_cls)
if hasattr(plugin, '__async_init__'):
- await plugin.__async_init__()
+ await self.async_inject(plugin.__async_init__)
handlers = plugin.handlers
self.app.add_handlers(handlers)
if handlers:
@@ -170,18 +184,22 @@ class Bot:
if not self._services:
return
logger.info('正在关闭服务')
- for _, service in self._services.items():
- try:
- if hasattr(service, 'stop'):
- if inspect.iscoroutinefunction(service.stop):
- await service.stop()
- else:
- service.stop()
- logger.success(f'服务 "{service.__class__.__name__}" 关闭成功')
- except Exception as e: # pylint: disable=W0703
- logger.exception(f"服务 \"{service.__class__.__name__}\" 关闭失败: \n{type(e).__name__}: {e}")
+ for _, service in filter(lambda x: not isinstance(x[1], TgApplication), self._services.items()):
+ async with timeout(5):
+ try:
+ if hasattr(service, 'stop'):
+ if inspect.iscoroutinefunction(service.stop):
+ await service.stop()
+ else:
+ service.stop()
+ logger.success(f'服务 "{service.__class__.__name__}" 关闭成功')
+ except CancelledError:
+ logger.warning(f'服务 "{service.__class__.__name__}" 关闭超时')
+ except Exception as e: # pylint: disable=W0703
+ logger.exception(f"服务 \"{service.__class__.__name__}\" 关闭失败: \n{type(e).__name__}: {e}")
- async def _post_init(self, _) -> NoReturn:
+ async def _post_init(self, context: CallbackContext) -> NoReturn:
+ self._services.update({CallbackContext: context})
logger.info('开始初始化服务')
await self.start_services()
logger.info('开始安装插件')
diff --git a/metadata/honey.py b/metadata/honey.py
new file mode 100644
index 0000000..3608f28
--- /dev/null
+++ b/metadata/honey.py
@@ -0,0 +1,464 @@
+"""此文件用于储存 honey impact 中的部分基础数据"""
+__all__ = [
+ 'HONEY_ID_MAP', 'HONEY_RESERVED_ID_MAP',
+ 'HONEY_ROLE_NAME_MAP'
+]
+
+# noinspection SpellCheckingInspection
+HONEY_ID_MAP = {
+ 'character': {
+ 'ayaka_002': ['神里绫华', 5],
+ 'xiangling_023': ['香菱', 4],
+ 'xingqiu_025': ['行秋', 4],
+ 'albedo_038': ['阿贝多', 5],
+ 'lisa_006': ['丽莎', 4],
+ 'sucrose_043': ['砂糖', 4],
+ 'mona_041': ['莫娜', 5],
+ 'diona_039': ['迪奥娜', 4],
+ 'venti_022': ['温迪', 5],
+ 'xinyan_044': ['辛焱', 4],
+ 'rosaria_045': ['罗莎莉亚', 4],
+ 'hutao_046': ['胡桃', 5],
+ 'zhongli_030': ['钟离', 5],
+ 'ningguang_027': ['凝光', 4],
+ 'eula_051': ['优菈', 5],
+ 'shougun_052': ['雷电将军', 5],
+ 'sayu_053': ['早柚', 4],
+ 'keqing_042': ['刻晴', 5],
+ 'ganyu_037': ['甘雨', 5],
+ 'gorou_055': ['五郎', 4],
+ 'tartaglia_033': ['达达利亚', 5],
+ 'beidou_024': ['北斗', 4],
+ 'itto_057': ['荒泷一斗', 5],
+ 'ambor_021': ['安柏', 4],
+ 'diluc_016': ['迪卢克', 5],
+ 'chongyun_036': ['重云', 4],
+ 'kaeya_015': ['凯亚', 4],
+ 'aloy_062': ['埃洛伊', 4],
+ 'yunjin_064': ['云堇', 4],
+ 'shinobu_065': ['久岐忍', 4],
+ 'ayato_066': ['神里绫人', 5],
+ 'collei_067': ['柯莱', 4],
+ 'feiyan_048': ['烟绯', 4],
+ 'razor_020': ['雷泽', 4],
+ 'barbara_014': ['芭芭拉', 4],
+ 'dori_068': ['多莉', 4],
+ 'noel_034': ['诺艾尔', 4],
+ 'tighnari_069': ['提纳里', 5],
+ 'kazuha_047': ['枫原万叶', 5],
+ 'qiqi_035': ['七七', 5],
+ 'bennett_032': ['班尼特', 4],
+ 'nilou_070': ['妮露', 5],
+ 'fischl_031': ['菲谢尔', 4],
+ 'klee_029': ['可莉', 5],
+ 'cyno_071': ['赛诺', 5],
+ 'candace_072': ['坎蒂丝', 4],
+ 'qin_003': ['琴', 5],
+ 'xiao_026': ['魈', 5],
+ 'playergirl_007': ['荧', 5],
+ 'heizo_059': ['鹿野院平藏', 4],
+ 'yoimiya_049': ['宵宫', 5],
+ 'playerboy_005': ['空', 5],
+ 'sara_056': ['九条裟罗', 4],
+ 'tohma_050': ['托马', 4],
+ 'kokomi_054': ['珊瑚宫心海', 5],
+ 'shenhe_063': ['申鹤', 5],
+ 'yae_058': ['八重神子', 5],
+ 'yelan_060': ['夜兰', 5]
+ },
+ 'weapon': {
+ 'i_n11401': ['西风剑', 4],
+ 'i_n11305': ['吃虎鱼刀', 3],
+ 'i_n11101': ['无锋剑', 1],
+ 'i_n11303': ['旅行剑', 3],
+ 'i_n11410': ['暗巷闪光', 4],
+ 'i_n11301': ['冷刃', 3],
+ 'i_n11416': ['笼钓瓶一心', 4],
+ 'i_n11407': ['铁蜂刺', 4],
+ 'i_n11501': ['风鹰剑', 5],
+ 'i_n11419': ['「一心传」名刀', 4],
+ 'i_n13409': ['龙脊长枪', 4],
+ 'i_n13406': ['千岩长枪', 4],
+ 'i_n11412': ['降临之剑', 4],
+ 'i_n13505': ['和璞鸢', 5],
+ 'i_n11504': ['斫峰之刃', 5],
+ 'i_n11417': ['原木刀', 4],
+ 'i_n13101': ['新手长枪', 1],
+ 'i_n11509': ['雾切之回光', 5],
+ 'i_n11502': ['天空之刃', 5],
+ 'i_n14304': ['翡玉法球', 3],
+ 'i_n13401': ['匣里灭辰', 4],
+ 'i_n11413': ['腐殖之剑', 4],
+ 'i_n11404': ['宗室长剑', 4],
+ 'i_n11418': ['西福斯的月光', 4],
+ 'i_n11415': ['辰砂之纺锤', 4],
+ 'i_n11503': ['苍古自由之誓', 5],
+ 'i_n13402': ['试作星镰', 4],
+ 'i_n11511': ['圣显之钥', 5],
+ 'i_n11409': ['黑剑', 4],
+ 'i_n11414': ['天目影打刀', 4],
+ 'i_n11405': ['匣里龙吟', 4],
+ 'i_n11510': ['波乱月白经津', 5],
+ 'i_n13405': ['决斗之枪', 4],
+ 'i_n13407': ['西风长枪', 4],
+ 'i_n11408': ['黑岩长剑', 4],
+ 'i_n14306': ['琥珀玥', 3],
+ 'i_n11505': ['磐岩结绿', 5],
+ 'i_n14408': ['黑岩绯玉', 4],
+ 'i_n14417': ['盈满之实', 4],
+ 'i_n14416': ['流浪的晚星', 4],
+ 'i_n12301': ['铁影阔剑', 3],
+ 'i_n14506': ['不灭月华', 5],
+ 'i_n14305': ['甲级宝珏', 3],
+ 'i_n13415': ['「渔获」', 4],
+ 'i_n14402': ['流浪乐章', 4],
+ 'i_n12402': ['钟剑', 4],
+ 'i_n12201': ['佣兵重剑', 2],
+ 'i_n14403': ['祭礼残章', 4],
+ 'i_n14405': ['匣里日月', 4],
+ 'i_n12101': ['训练大剑', 1],
+ 'i_n14501': ['天空之卷', 5],
+ 'i_n14413': ['嘟嘟可故事集', 4],
+ 'i_n14302': ['讨龙英杰谭', 3],
+ 'i_n14303': ['异世界行记', 3],
+ 'i_n12303': ['白铁大剑', 3],
+ 'i_n14409': ['昭心', 4],
+ 'i_n13502': ['天空之脊', 5],
+ 'i_n14404': ['宗室秘法录', 4],
+ 'i_n14401': ['西风秘典', 4],
+ 'i_n14415': ['证誓之明瞳', 4],
+ 'i_n13301': ['白缨枪', 3],
+ 'i_n13404': ['黑岩刺枪', 4],
+ 'i_n13408': ['宗室猎枪', 4],
+ 'i_n13201': ['铁尖枪', 2],
+ 'i_n13511': ['赤沙之杖', 5],
+ 'i_n13416': ['断浪长鳍', 4],
+ 'i_n13509': ['薙草之稻光', 5],
+ 'i_n13403': ['流月针', 4],
+ 'i_n13417': ['贯月矢', 4],
+ 'i_n13419': ['风信之锋', 4],
+ 'i_n13302': ['钺矛', 3],
+ 'i_n11406': ['试作斩岩', 4],
+ 'i_n13414': ['喜多院十文字', 4],
+ 'i_n14201': ['口袋魔导书', 2],
+ 'i_n13501': ['护摩之杖', 5],
+ 'i_n13303': ['黑缨枪', 3],
+ 'i_n14101': ['学徒笔记', 1],
+ 'i_n12401': ['西风大剑', 4],
+ 'i_n12304': ['石英大剑', 3],
+ 'i_n14412': ['忍冬之果', 4],
+ 'i_n14414': ['白辰之环', 4],
+ 'i_n14509': ['神乐之真意', 5],
+ 'i_n14406': ['试作金珀', 4],
+ 'i_n14502': ['四风原典', 5],
+ 'i_n12305': ['以理服人', 3],
+ 'i_n14504': ['尘世之锁', 5],
+ 'i_n12306': ['飞天大御剑', 3],
+ 'i_n14301': ['魔导绪论', 3],
+ 'i_n12302': ['沐浴龙血的剑', 3],
+ 'i_n14407': ['万国诸海图谱', 4],
+ 'i_n13504': ['贯虹之槊', 5],
+ 'i_n12416': ['恶王丸', 4],
+ 'i_n12409': ['螭骨剑', 4],
+ 'i_n12404': ['宗室大剑', 4],
+ 'i_n12405': ['雨裁', 4],
+ 'i_n12414': ['桂木斩长正', 4],
+ 'i_n12408': ['黑岩斩刀', 4],
+ 'i_n12410': ['千岩古剑', 4],
+ 'i_n12406': ['试作古华', 4],
+ 'i_n12415': ['玛海菈的水色', 4],
+ 'i_n12403': ['祭礼大剑', 4],
+ 'i_n12411': ['雪葬的星银', 4],
+ 'i_n12412': ['衔珠海皇', 4],
+ 'i_n12407': ['白影剑', 4],
+ 'i_n11201': ['银剑', 2],
+ 'i_n12504': ['无工之剑', 5],
+ 'i_n15305': ['信使', 3],
+ 'i_n15411': ['落霞', 4],
+ 'i_n15413': ['风花之颂', 4],
+ 'i_n15401': ['西风猎弓', 4],
+ 'i_n12510': ['赤角石溃杵', 5],
+ 'i_n15405': ['弓藏', 4],
+ 'i_n15403': ['祭礼弓', 4],
+ 'i_n15201': ['历练的猎弓', 2],
+ 'i_n15404': ['宗室长弓', 4],
+ 'i_n15302': ['神射手之誓', 3],
+ 'i_n12501': ['天空之傲', 5],
+ 'i_n15402': ['绝弦', 4],
+ 'i_n12417': ['森林王器', 4],
+ 'i_n11306': ['飞天御剑', 3],
+ 'i_n15410': ['暗巷猎手', 4],
+ 'i_n15414': ['破魔之弓', 4],
+ 'i_n15101': ['猎弓', 1],
+ 'i_n15415': ['掠食者', 4],
+ 'i_n15301': ['鸦羽弓', 3],
+ 'i_n11304': ['暗铁剑', 3],
+ 'i_n15303': ['反曲弓', 3],
+ 'i_n15306': ['黑檀弓', 3],
+ 'i_n15408': ['黑岩战弓', 4],
+ 'i_n15304': ['弹弓', 3],
+ 'i_n15409': ['苍翠猎弓', 4],
+ 'i_n15412': ['幽夜华尔兹', 4],
+ 'i_n15406': ['试作澹月', 4],
+ 'i_n15417': ['王下近侍', 4],
+ 'i_n15501': ['天空之翼', 5],
+ 'i_n15418': ['竭泽', 4],
+ 'i_n15507': ['冬极白星', 5],
+ 'i_n15508': ['若水', 5],
+ 'i_n15503': ['终末嗟叹之诗', 5],
+ 'i_n15509': ['飞雷之弦振', 5],
+ 'i_n15511': ['猎人之径', 5],
+ 'i_n12502': ['狼的末路', 5],
+ 'i_n15407': ['钢轮弓', 4],
+ 'i_n12503': ['松籁响起之时', 5],
+ 'i_n15416': ['曚云之月', 4],
+ 'i_n11402': ['笛剑', 4],
+ 'i_n15502': ['阿莫斯之弓', 5],
+ 'i_n11302': ['黎明神剑', 3],
+ 'i_n13507': ['息灾', 5],
+ 'i_n11403': ['祭礼剑', 4],
+ 'i_n14410': ['暗巷的酒与诗', 4]
+ },
+ 'material': {
+ 'i_413': ['「勤劳」的哲学', 4],
+ 'i_411': ['「勤劳」的教导', 2],
+ 'i_n104333': ['「巧思」的指引', 3],
+ 'i_584': ['今昔剧画之鬼人', 5],
+ 'i_427': ['「天光」的指引', 3],
+ 'i_453': ['「抗争」的哲学', 4],
+ 'i_n112063': ['休眠菌核', 3],
+ 'i_408': ['「浮世」的哲学', 4],
+ 'i_n104336': ['「笃行」的指引', 3],
+ 'i_n104337': ['「笃行」的哲学', 4],
+ 'i_421': ['「自由」的教导', 2],
+ 'i_n112068': ['混沌容器', 2],
+ 'i_n104331': ['「诤言」的哲学', 4],
+ 'i_n104330': ['「诤言」的指引', 3],
+ 'i_402': ['「诗文」的指引', 3],
+ 'i_416': ['「风雅」的教导', 2],
+ 'i_423': ['「自由」的哲学', 4],
+ 'i_407': ['「浮世」的指引', 3],
+ 'i_581': ['今昔剧画之恶尉', 2],
+ 'i_401': ['「诗文」的教导', 2],
+ 'i_441': ['「繁荣」的教导', 2],
+ 'i_n104335': ['「笃行」的教导', 2],
+ 'i_422': ['「自由」的指引', 3],
+ 'i_432': ['「黄金」的指引', 3],
+ 'i_n104332': ['「巧思」的教导', 2],
+ 'i_53': ['历战的箭簇', 3],
+ 'i_417': ['「风雅」的指引', 3],
+ 'i_431': ['「黄金」的教导', 2],
+ 'i_403': ['「诗文」的哲学', 4],
+ 'i_451': ['「抗争」的教导', 2],
+ 'i_462': ['东风之爪', 5],
+ 'i_461': ['东风之翎', 5],
+ 'i_524': ['凛风奔狼的怀乡', 5],
+ 'i_433': ['「黄金」的哲学', 4],
+ 'i_406': ['「浮世」的教导', 2],
+ 'i_452': ['「抗争」的指引', 3],
+ 'i_n104334': ['「巧思」的哲学', 4],
+ 'i_133': ['原素花蜜', 3],
+ 'i_582': ['今昔剧画之虎啮', 3],
+ 'i_442': ['「繁荣」的指引', 3],
+ 'i_483': ['凶将之手眼', 5],
+ 'i_583': ['今昔剧画之一角', 4],
+ 'i_418': ['「风雅」的哲学', 4],
+ 'i_463': ['东风的吐息', 5],
+ 'i_443': ['「繁荣」的哲学', 4],
+ 'i_521': ['凛风奔狼的始龀', 2],
+ 'i_n104329': ['「诤言」的教导', 2],
+ 'i_464': ['北风之尾', 5],
+ 'i_61': ['沉重号角', 2],
+ 'i_523': ['凛风奔狼的断牙', 4],
+ 'i_485': ['万劫之真意', 5],
+ 'i_33': ['不祥的面具', 3],
+ 'i_467': ['吞天之鲸·只角', 5],
+ 'i_465': ['北风之环', 5],
+ 'i_21': ['史莱姆凝液', 1],
+ 'i_513': ['孤云寒林的圣骸', 4],
+ 'i_185': ['浮游干核', 1],
+ 'i_511': ['孤云寒林的光砂', 2],
+ 'i_522': ['凛风奔狼的裂齿', 3],
+ 'i_514': ['孤云寒林的神体', 5],
+ 'i_412': ['「勤劳」的指引', 3],
+ 'i_466': ['北风的魂匣', 5],
+ 'i_173': ['混沌真眼', 4],
+ 'i_73': ['地脉的新芽', 4],
+ 'i_183': ['偏光棱镜', 4],
+ 'i_n112061': ['孢囊晶尘', 3],
+ 'i_83': ['混沌炉心', 4],
+ 'i_142': ['结实的骨片', 3],
+ 'i_22': ['史莱姆清', 2],
+ 'i_112': ['士官的徽记', 2],
+ 'i_23': ['史莱姆原浆', 3],
+ 'i_163': ['名刀镡', 3],
+ 'i_n112062': ['失活菌核', 2],
+ 'i_n112072': ['混浊棱晶', 3],
+ 'i_428': ['「天光」的哲学', 4],
+ 'i_552': ['漆黑陨铁的一片', 3],
+ 'i_172': ['混沌枢纽', 3],
+ 'i_72': ['地脉的枯叶', 3],
+ 'i_426': ['「天光」的教导', 2],
+ 'i_n112070': ['混沌锚栓', 4],
+ 'i_491': ['智识之冕', 5],
+ 'i_123': ['攫金鸦印', 3],
+ 'i_121': ['寻宝鸦印', 1],
+ 'i_153': ['幽邃刻像', 4],
+ 'i_41': ['导能绘卷', 1],
+ 'i_187': ['浮游晶化核', 3],
+ 'i_32': ['污秽的面具', 2],
+ 'i_469': ['武炼之魂·孤影', 5],
+ 'i_553': ['漆黑陨铁的一角', 4],
+ 'i_151': ['晦暗刻像', 2],
+ 'i_182': ['水晶棱镜', 3],
+ 'i_71': ['地脉的旧枝', 2],
+ 'i_512': ['孤云寒林的辉岩', 3],
+ 'i_42': ['封魔绘卷', 2],
+ 'i_132': ['微光花蜜', 2],
+ 'i_152': ['夤夜刻像', 3],
+ 'i_186': ['浮游幽核', 2],
+ 'i_482': ['灰烬之心', 5],
+ 'i_81': ['混沌装置', 2],
+ 'i_82': ['混沌回路', 3],
+ 'i_n114045': ['烈日威权的残响', 2],
+ 'i_541': ['狮牙斗士的枷锁', 2],
+ 'i_n114046': ['烈日威权的余光', 3],
+ 'i_171': ['混沌机关', 2],
+ 'i_51': ['牢固的箭簇', 1],
+ 'i_542': ['狮牙斗士的铁链', 3],
+ 'i_111': ['新兵的徽记', 1],
+ 'i_n112069': ['混沌模块', 3],
+ 'i_n114048': ['烈日威权的旧日', 5],
+ 'i_162': ['影打刀镡', 2],
+ 'i_481': ['狱火之蝶', 5],
+ 'i_554': ['漆黑陨铁的一块', 5],
+ 'i_n112071': ['破缺棱晶', 2],
+ 'i_143': ['石化的骨片', 4],
+ 'i_103': ['督察长祭刀', 4],
+ 'i_31': ['破损的面具', 1],
+ 'i_43': ['禁咒绘卷', 3],
+ 'i_n114043': ['绿洲花园的哀思', 4],
+ 'i_n112067': ['织金红绸', 3],
+ 'i_n114042': ['绿洲花园的恩惠', 3],
+ 'i_n114044': ['绿洲花园的真谛', 5],
+ 'i_n112060': ['荧光孢粉', 2],
+ 'i_122': ['藏银鸦印', 2],
+ 'i_n112065': ['褪色红绸', 1],
+ 'i_141': ['脆弱的骨片', 2],
+ 'i_n114040': ['谧林涓露的金符', 5],
+ 'i_n114039': ['谧林涓露的银符', 4],
+ 'i_562': ['远海夷地的玉枝', 3],
+ 'i_563': ['远海夷地的琼枝', 4],
+ 'i_564': ['远海夷地的金枝', 5],
+ 'i_n114038': ['谧林涓露的铁符', 3],
+ 'i_n114037': ['谧林涓露的铜符', 2],
+ 'i_52': ['锐利的箭簇', 2],
+ 'i_n112066': ['镶边红绸', 2],
+ 'i_174': ['隐兽指爪', 2],
+ 'i_176': ['隐兽鬼爪', 4],
+ 'i_534': ['雾海云间的转还', 5],
+ 'i_531': ['雾海云间的铅丹', 2],
+ 'i_503': ['高塔孤王的断片', 4],
+ 'i_91': ['雾虚花粉', 2],
+ 'i_92': ['雾虚草囊', 3],
+ 'i_501': ['高塔孤王的破瓦', 2],
+ 'i_504': ['高塔孤王的碎梦', 5],
+ 'i_468': ['魔王之刃·残片', 5],
+ 'i_544': ['狮牙斗士的理想', 5],
+ 'i_470': ['龙王之冕', 5],
+ 'i_572': ['鸣神御灵的欢喜', 3],
+ 'i_574': ['鸣神御灵的勇武', 5],
+ 'i_573': ['鸣神御灵的亲爱', 4],
+ 'i_480': ['熔毁之刻', 5],
+ 'i_62': ['黑铜号角', 3],
+ 'i_101': ['猎兵祭刀', 2],
+ 'i_551': ['漆黑陨铁的一粒', 2],
+ 'i_484': ['祸神之禊泪', 5],
+ 'i_n114041': ['绿洲花园的追忆', 2],
+ 'i_n112059': ['蕈兽孢子', 1],
+ 'i_561': ['远海夷地的瑚枝', 2],
+ 'i_n112073': ['辉光棱晶', 4],
+ 'i_175': ['隐兽利爪', 3],
+ 'i_532': ['雾海云间的汞丹', 3],
+ 'i_93': ['雾虚灯芯', 4],
+ 'i_131': ['骗骗花蜜', 1],
+ 'i_571': ['鸣神御灵的明惠', 2],
+ 'i_n114047': ['烈日威权的梦想', 4],
+ 'i_63': ['黑晶号角', 4],
+ 'i_543': ['狮牙斗士的镣铐', 4],
+ 'i_n112064': ['茁壮菌核', 4],
+ 'i_161': ['破旧的刀镡', 1],
+ 'i_472': ['鎏金之鳞', 5],
+ 'i_113': ['尉官的徽记', 3],
+ 'i_502': ['高塔孤王的残垣', 3],
+ 'i_181': ['黯淡棱镜', 2],
+ 'i_102': ['特工祭刀', 3],
+ 'i_471': ['血玉之枝', 5],
+ 'i_533': ['雾海云间的金丹', 4]
+ }
+}
+
+HONEY_RESERVED_ID_MAP = {
+ k: {j[0]: [i, j[1]] for i, j in v.items()} for k, v in HONEY_ID_MAP.items()
+}
+# noinspection SpellCheckingInspection
+HONEY_ROLE_NAME_MAP = {
+ 10000002: ['ayaka_002', '神里绫华', 'ayaka'],
+ 10000042: ['keqing_042', '刻晴', 'keqing'],
+ 10000030: ['zhongli_030', '钟离', 'zhongli'],
+ 10000026: ['xiao_026', '魈', 'xiao'],
+ 10000020: ['razor_020', '雷泽', 'razor'],
+ 10000015: ['kaeya_015', '凯亚', 'kaeya'],
+ 10000037: ['ganyu_037', '甘雨', 'ganyu'],
+ 10000041: ['mona_041', '莫娜', 'mona'],
+ 10000038: ['albedo_038', '阿贝多', 'albedo'],
+ 10000014: ['barbara_014', '芭芭拉', 'barbara'],
+ 10000027: ['ningguang_027', '凝光', 'ningguang'],
+ 10000054: ['kokomi_054', '珊瑚宫心海', 'kokomi'],
+ 10000044: ['xinyan_044', '辛焱', 'xinyan'],
+ 10000056: ['sara_056', '九条裟罗', 'sara'],
+ 10000053: ['sayu_053', '早柚', 'sayu'],
+ 10000043: ['sucrose_043', '砂糖', 'sucrose'],
+ 10000059: ['heizo_059', '鹿野院平藏', 'heizo'],
+ 10000060: ['yelan_060', '夜兰', 'yelan'],
+ 10000064: ['yunjin_064', '云堇', 'yunjin'],
+ 10000050: ['tohma_050', '托马', 'tohma'],
+ 10000066: ['ayato_066', '神里绫人', 'ayato'],
+ 10000067: ['collei_067', '柯莱', 'collei'],
+ 10000052: ['shougun_052', '雷电将军', 'shougun'],
+ 10000069: ['tighnari_069', '提纳里', 'tighnari'],
+ 10000007: ['playergirl_007', '荧', 'playergirl'],
+ 10000016: ['diluc_016', '迪卢克', 'diluc'],
+ 10000070: ['nilou_070', '妮露', 'nilou'],
+ 10000047: ['kazuha_047', '枫原万叶', 'kazuha'],
+ 10000055: ['gorou_055', '五郎', 'gorou'],
+ 10000034: ['noel_034', '诺艾尔', 'noel'],
+ 10000024: ['beidou_024', '北斗', 'beidou'],
+ 10000032: ['bennett_032', '班尼特', 'bennett'],
+ 10000062: ['aloy_062', '埃洛伊', 'aloy'],
+ 10000025: ['xingqiu_025', '行秋', 'xingqiu'],
+ 10000022: ['venti_022', '温迪', 'venti'],
+ 10000036: ['chongyun_036', '重云', 'chongyun'],
+ 10000049: ['yoimiya_049', '宵宫', 'yoimiya'],
+ 10000029: ['klee_029', '可莉', 'klee'],
+ 10000006: ['lisa_006', '丽莎', 'lisa'],
+ 10000033: ['tartaglia_033', '达达利亚', 'tartaglia'],
+ 10000039: ['diona_039', '迪奥娜', 'diona'],
+ 10000063: ['shenhe_063', '申鹤', 'shenhe'],
+ 10000072: ['candace_072', '坎蒂丝', 'candace'],
+ 10000045: ['rosaria_045', '罗莎莉亚', 'rosaria'],
+ 10000051: ['eula_051', '优菈', 'eula'],
+ 10000035: ['qiqi_035', '七七', 'qiqi'],
+ 10000057: ['itto_057', '荒泷一斗', 'itto'],
+ 10000005: ['playerboy_005', '空', 'playerboy'],
+ 10000048: ['feiyan_048', '烟绯', 'feiyan'],
+ 10000003: ['qin_003', '琴', 'qin'],
+ 10000023: ['xiangling_023', '香菱', 'xiangling'],
+ 10000071: ['cyno_071', '赛诺', 'cyno'],
+ 10000031: ['fischl_031', '菲谢尔', 'fischl'],
+ 10000046: ['hutao_046', '胡桃', 'hutao'],
+ 10000021: ['ambor_021', '安柏', 'ambor'],
+ 10000068: ['dori_068', '多莉', 'dori'],
+ 10000065: ['shinobu_065', '久岐忍', 'shinobu'],
+ 10000058: ['yae_058', '八重神子', 'yae']
+}
diff --git a/metadata/shortname.py b/metadata/shortname.py
index c2d41a9..38d4e27 100644
--- a/metadata/shortname.py
+++ b/metadata/shortname.py
@@ -1,22 +1,27 @@
from typing import Optional
+__all__ = [
+ 'roles', 'weapons',
+ 'roleToId', 'roleToName', 'weaponToName',
+]
+
# noinspection SpellCheckingInspection
roles = {
20000000: [
'主角', '旅行者', '卑鄙的外乡人', '荣誉骑士', '爷', '风主', '岩主', '雷主', '草主', '履刑者', '抽卡不歪真君'
],
- 10000002: ['神里绫华', 'Kamisato Ayaka', 'Ayaka', 'ayaka', '神里', '绫华', '神里凌华', '凌华', '白鹭公主',
+ 10000002: ['神里绫华', 'Ayaka', 'ayaka', 'Kamisato Ayaka', '神里', '绫华', '神里凌华', '凌华', '白鹭公主',
'神里大小 姐'],
10000003: ['琴', 'Jean', 'jean', '团长', '代理团长', '琴团长', '蒲公英骑士'],
- 10000005: ['空', '男主', '男主角', '龙哥', '空哥'],
+ 10000005: ['空', 'Aether', 'aether', '男主', '男主角', '龙哥', '空哥'],
10000006: ['丽莎', 'Lisa', 'lisa', '图书管理员', '图书馆管理员', '蔷薇魔女'],
- 10000007: ['荧', '女主', '女主角', '莹', '萤', '黄毛阿姨', '荧妹'],
+ 10000007: ['荧', 'Lumine', 'lumine', '女主', '女主角', '莹', '萤', '黄毛阿姨', '荧妹'],
10000014: ['芭芭拉', 'Barbara', 'barbara', '巴巴拉', '拉粑粑', '拉巴巴', '内鬼', '加湿器', '闪耀偶像', '偶像'],
10000015: ['凯亚', 'Kaeya', 'kaeya', '盖亚', '凯子哥', '凯鸭', '矿工', '矿工头子', '骑兵队长', '凯子',
'凝冰渡海真君'],
- 10000016: ['迪卢克', 'diluc', 'Diluc', '卢姥爷', '姥爷', '卢老爷', '卢锅巴', '正义人', '正e人', '正E人', '卢本伟',
+ 10000016: ['迪卢克', 'Diluc', 'diluc', '卢姥爷', '姥爷', '卢老爷', '卢锅巴', '正义人', '正e人', '正E人', '卢本伟',
'暗夜英雄', '卢卢伯爵', '落魄了', '落魄了家人们'],
- 10000020: ['雷泽', 'razor', 'Razor', '狼少年', '狼崽子', '狼崽', '卢皮卡', '小狼', '小狼狗'],
+ 10000020: ['雷泽', 'Razor', 'razor', '狼少年', '狼崽子', '狼崽', '卢皮卡', '小狼', '小狼狗'],
10000021: ['安柏', 'Amber', 'amber', '安伯', '兔兔伯爵', '飞行冠军', '侦查骑士', '点火姬', '点火机', '打火机',
'打火姬'],
10000022: ['温迪', 'Venti', 'venti', '温蒂', '风神', '卖唱的', '巴巴托斯', '巴巴脱丝', '芭芭托斯', '芭芭脱丝',
@@ -53,9 +58,9 @@ roles = {
10000045: ['罗莎莉亚', 'Rosaria', 'rosaria', '罗莎莉娅', '白色史莱姆', '白史莱姆', '修女', '罗莎利亚', '罗莎利娅',
'罗沙莉亚', '罗沙莉娅', '罗沙利亚', '罗沙利娅', '萝莎莉亚', '萝莎莉娅', '萝莎利亚', '萝莎利娅',
'萝沙莉亚', '萝沙莉娅', '萝沙利亚', '萝沙利娅'],
- 10000046: ['胡桃', 'Hu Tao', 'hu tao', 'HuTao', 'hutao', 'Hutao', '胡 淘', '往生堂堂主', '火化', '抬棺的', '蝴蝶',
+ 10000046: ['胡桃', 'HuTao', 'hutao', 'Hu Tao', 'hu tao', 'Hutao', '胡 淘', '往生堂堂主', '火化', '抬棺的', '蝴蝶',
'核桃', '堂主', '胡堂主', '雪霁梅香'],
- 10000047: ['枫原万叶', 'Kaedehara Kazuha', 'Kazuha', 'kazuha', '万叶', '叶天帝', '天帝', '叶师傅'],
+ 10000047: ['枫原万叶', 'Kazuha', 'kazuha', 'Kaedehara Kazuha', '万叶', '叶天帝', '天帝', '叶师傅'],
10000048: ['烟绯', 'Yanfei', 'yanfei', '烟老师', '律师', '罗翔'],
10000049: ['宵宫', 'Yoimiya', 'yoimiya', '霄宫', '烟花', '肖宫', '肖工', '绷带女孩'],
10000050: ['托马', 'Thoma', 'thoma', '家政官', '太郎丸', '地头蛇', '男仆', '拖马'],
@@ -63,27 +68,30 @@ roles = {
10000052: ['雷电将军', 'Raiden Shogun', 'Raiden', 'raiden', '雷神', '将军', '雷军', '巴尔', '阿影', '影',
'巴尔泽布', '煮饭婆', '奶香一刀', '无想一刀', '宅女'],
10000053: ['早柚', 'Sayu', 'sayu', '小狸猫', '狸 猫', '忍者'],
- 10000054: ['珊瑚宫心海', 'Sangonomiya Kokomi', 'Kokomi', 'kokomi', '心海', '军师', '珊瑚宫', '书记', '观赏鱼',
+ 10000054: ['珊瑚宫心海', 'Kokomi', 'kokomi', 'Sangonomiya Kokomi', '心海', '军师', '珊瑚宫', '书记', '观赏鱼',
'水母', '鱼', '美人鱼'],
10000055: ['五郎', 'Gorou', 'gorou', '柴犬', '土狗', '希娜', '希娜小姐'],
- 10000056: ['九条裟罗', 'Kujou Sara', 'Sara', 'sara', '九条', '九条沙罗', '裟罗', '沙罗', '天狗'],
- 10000057: ['荒泷一斗', 'Arataki Itto', 'Itto', 'itto', '荒龙一斗', '荒泷天下第一斗', '一斗', '一抖', '荒泷', '1斗',
+ 10000056: ['九条裟罗', 'Sara', 'sara', 'Kujou Sara', '九条', '九条沙罗', '裟罗', '沙罗', '天狗'],
+ 10000057: ['荒泷一斗', 'Itto', 'itto', 'Arataki Itto', '荒龙一斗', '荒泷天下第一斗', '一斗', '一抖', '荒泷', '1斗',
'牛牛', '斗子哥', '牛子哥', '牛子', '孩子 王', '斗虫', '巧乐兹', '放牛的'],
- 10000058: ['八重神子', 'Yae Miko', 'Miko', 'miko', '八重', '神子', '狐狸', '想得美哦', '巫女', '屑狐狸', '骚狐狸',
+ 10000058: ['八重神子', 'Miko', 'miko', 'Yae Miko', '八重', '神子', '狐狸', '想得美哦', '巫女', '屑狐狸', '骚狐狸',
'八重宫司', '婶子', '小八'],
- 10000059: ['鹿野院平藏', 'shikanoin heizou', 'Heizou', 'heizou', 'heizo', '鹿野苑', '鹿野院', '平藏', '鹿野苑平藏',
+ 10000059: ['鹿野院平藏', 'Heizou', 'heizou', 'shikanoin heizou', 'heizo', '鹿野苑', '鹿野院', '平藏', '鹿野苑平藏',
'鹿野', '小鹿'],
10000060: ['夜兰', 'Yelan', 'yelan', '夜阑', '叶 澜', '腋兰', '夜天后'],
10000062: ['埃洛伊', 'Aloy', 'aloy'],
10000063: ['申鹤', 'Shenhe', 'shenhe', '神鹤', '小姨', '小姨子', '审鹤'],
- 10000064: ['云堇', 'Yun Jin', 'yunjin', 'yun jin', '云瑾', '云先生', '云锦', '神女劈观'],
- 10000065: ['久岐忍', 'Kuki Shinobu', 'Kuki', 'kuki', 'Shinobu', 'shinobu', '97忍', '小忍', '久歧忍', '97', '茄忍',
+ 10000064: ['云堇', 'YunJin', 'yunjin', 'Yun Jin', 'yun jin', '云瑾', '云先生', '云锦', '神女劈观'],
+ 10000065: ['久岐忍', 'Kuki', 'kuki', 'Kuki Shinobu', 'Shinobu', 'shinobu', '97忍', '小忍', '久歧忍', '97', '茄忍',
'阿忍', '忍姐'],
- 10000066: ['神里绫人', 'Kamisato Ayato', 'Ayato', 'ayato', '绫人', '神里凌人', '凌人', '0人', '神人', '零人',
+ 10000066: ['神里绫人', 'Ayato', 'ayato', 'Kamisato Ayato', '绫人', '神里凌人', '凌人', '0人', '神人', '零人',
'大舅哥'],
10000067: ['柯莱', 'Collei', 'collei', '克莱', '科莱', '须弥飞行冠军', '草安伯'],
10000068: ['多莉', 'Dori', 'dori', '多利', '多丽'],
- 10000069: ['提纳里', 'Tighnari', 'tighnari', '巡林官', '小提', '缇娜里', '提哪里', '提那里']
+ 10000069: ['提纳里', 'Tighnari', 'tighnari', '巡林官', '小提', '缇娜里', '提哪里', '提那里'],
+ 10000070: ['妮露', 'Nilou', 'nilou'],
+ 10000071: ['赛诺', 'Cyno', 'cyno'],
+ 10000072: ['坎蒂丝', 'Candace', 'candace'],
}
weapons = {
"磐岩结绿": ["绿箭", "绿剑"],
@@ -166,16 +174,19 @@ weapons = {
}
+# noinspection PyPep8Naming
def roleToName(shortname: str) -> str:
"""讲角色昵称转为正式名"""
return next((value[0] for value in roles.values() for name in value if name == shortname), shortname)
+# noinspection PyPep8Naming
def roleToId(name: str) -> Optional[int]:
"""获取角色ID"""
return next((key for key, value in roles.items() for n in value if n == name), None)
+# noinspection PyPep8Naming
def weaponToName(shortname: str) -> str:
"""讲武器昵称转为正式名"""
return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname)
diff --git a/modules/wiki/material.py b/modules/wiki/material.py
index ef71f4a..c0c215b 100644
--- a/modules/wiki/material.py
+++ b/modules/wiki/material.py
@@ -1,56 +1,79 @@
import re
-from typing import List
+from typing import List, Optional, Tuple, Union
from bs4 import BeautifulSoup
from httpx import URL
-from typing_extensions import Self
from modules.wiki.base import SCRAPE_HOST, WikiModel
__all__ = ['Material']
+WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
+
class Material(WikiModel):
- """材料
-
- Attributes:
- type: 类型
- source: 获取方式
- description: 描述
- serise: 材料系列
- """
+ # noinspection PyUnresolvedReferences
+ """武器、角色培养素材
+ Attributes:
+ type: 类型
+ weekdays: 每周开放的时间
+ source: 获取方式
+ description: 描述
+ """
type: str
- source: List[str]
+ source: Optional[List[str]] = None
+ weekdays: Optional[List[int]] = None
description: str
@staticmethod
def scrape_urls() -> List[URL]:
- return [SCRAPE_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']]
+ weapon = [SCRAPE_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']]
+ talent = [SCRAPE_HOST.join(f'fam_talent_{i}/?lang=CHS') for i in ['book', 'boss', 'common', 'reward']]
+ return weapon + talent
@classmethod
- async def _parse_soup(cls, soup: BeautifulSoup) -> Self:
- """解析材料页"""
+ 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]
+
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)
- source = list(
- filter(
- lambda x: x, # filter 在这里的作用是过滤掉为空的数据
- table_rows[-2].find_all('td')[-1].encode_contents().decode().split('
')
+ 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_, source=source, description=description)
+ return Material(
+ id=id_, name=name, rarity=rarity, type=type_, description=description, source=source, weekdays=weekdays
+ )
@property
def icon(self) -> str:
diff --git a/plugins/genshin/daily/__init__.py b/plugins/genshin/daily/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/genshin/daily/material.py b/plugins/genshin/daily/material.py
new file mode 100644
index 0000000..fbcbd47
--- /dev/null
+++ b/plugins/genshin/daily/material.py
@@ -0,0 +1,367 @@
+import asyncio
+import itertools
+import os
+import re
+from asyncio import Lock
+from ctypes import c_double
+from datetime import datetime
+from multiprocessing import Value
+from pathlib import Path
+from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, Union
+
+import ujson as json
+from aiofiles import open as async_open
+from bs4 import BeautifulSoup
+from genshin import Client
+from httpx import AsyncClient, HTTPError
+from pydantic import BaseModel
+from telegram import InputMediaDocument, InputMediaPhoto, Message, Update, User
+from telegram.constants import ChatAction, ParseMode
+from telegram.error import RetryAfter, TimedOut
+from telegram.ext import CallbackContext
+
+from core.assets import AssetsService
+from core.baseplugin import BasePlugin
+from core.cookies.error import CookiesNotFoundError
+from core.plugin import Plugin, handler
+from core.template import TemplateService
+from core.user.error import UserNotFoundError
+from metadata.honey import HONEY_ID_MAP, HONEY_ROLE_NAME_MAP
+from utils.bot import get_all_args
+from utils.const import RESOURCE_DIR
+from utils.decorators.admins import bot_admins_rights_check
+from utils.decorators.error import error_callable
+from utils.decorators.restricts import restricts
+from utils.helpers import get_genshin_client
+from utils.log import logger
+
+DATA_TYPE = Dict[str, List[List[str]]]
+DATA_FILE_PATH = Path(__file__).joinpath('../daily.json').resolve()
+AREA = ['蒙德', '璃月', '稻妻', '须弥']
+DOMAINS = ['忘却之峡', '太山府', '菫色之庭', '昏识塔', '塞西莉亚苗圃', '震雷连山密宫', '砂流之庭', '有顶塔']
+DOMAIN_AREA_MAP = dict(zip(DOMAINS, AREA * 2))
+
+WEEK_MAP = ['一', '二', '三', '四', '五', '六', '日']
+
+
+def convert_path(path: Union[str, Path]) -> str:
+ return f"..{os.sep}..{os.sep}" + str(path.relative_to(RESOURCE_DIR))
+
+
+def sort_item(items: List['ItemData']) -> Iterable['ItemData']:
+ result_a = []
+ for _, group_a in itertools.groupby(sorted(items, key=lambda x: x.rarity, reverse=True), lambda x: x.rarity):
+ result_b = []
+ for _, group_b in itertools.groupby(
+ sorted(group_a, key=lambda x: x.level or -1, reverse=True), lambda x: x.level or -1
+ ):
+ result_b.append(sorted(group_b, key=lambda x: x.constellation or x.refinement or -1, reverse=True))
+ result_a.append(itertools.chain(*result_b))
+ return itertools.chain(*result_a)
+
+
+class DailyMaterial(Plugin, BasePlugin):
+ """每日素材表"""
+ data: DATA_TYPE
+ locks: Tuple[Lock] = (Lock(), Lock())
+
+ def __init__(self, assets: AssetsService, template: TemplateService):
+ self.assets_service = assets
+ self.template_service = template
+ self.client = AsyncClient()
+
+ async def __async_init__(self):
+ data = None
+ if not DATA_FILE_PATH.exists():
+ async def task_daily():
+ async with self.locks[0]:
+ logger.info("正在开始获取每日素材缓存")
+ self.data = await self._refresh_data()
+
+ self.refresh_task = asyncio.create_task(task_daily())
+ if not data and DATA_FILE_PATH.exists():
+ async with async_open(DATA_FILE_PATH) as file:
+ data = json.loads(await file.read())
+ self.data = data
+
+ async def _get_data_from_user(self, user: User) -> Tuple[Optional[Client], Dict[str, List[Any]]]:
+ client = None
+ user_data = {'character': [], 'weapon': []}
+ try:
+ logger.debug("尝试获取已绑定的原神账号")
+ client = await get_genshin_client(user.id)
+ logger.debug(f"获取成功, UID: {client.uid}")
+ characters = await client.get_genshin_characters(client.uid)
+ for character in characters:
+ cid = HONEY_ROLE_NAME_MAP[character.id][0]
+ weapon = character.weapon
+ user_data['character'].append(
+ ItemData(
+ id=cid, name=character.name, rarity=character.rarity, level=character.level,
+ constellation=character.constellation,
+ icon=convert_path(await self.assets_service.character(cid).icon())
+ )
+ )
+ user_data['weapon'].append(
+ ItemData(
+ id=(wid := f"i_n{weapon.id}"), name=weapon.name, level=weapon.level, rarity=weapon.rarity,
+ refinement=weapon.refinement,
+ icon=convert_path(
+ await getattr(
+ self.assets_service.weapon(wid), 'icon' if weapon.ascension < 2 else 'awakened'
+ )()
+ ),
+ c_path=convert_path(await self.assets_service.character(cid).side())
+ )
+ )
+ except (UserNotFoundError, CookiesNotFoundError):
+ logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息")
+ return client, user_data
+
+ @handler.command('daily_material', block=False)
+ @restricts(restricts_time_of_groups=20, without_overlapping=True)
+ @error_callable
+ async def daily_material(self, update: Update, context: CallbackContext):
+ user = update.effective_user
+ args = get_all_args(context)
+ now = datetime.now()
+
+ if args and str(args[0]).isdigit():
+ weekday = int(args[0]) - 1
+ if weekday < 0:
+ weekday = 0
+ elif weekday > 6:
+ weekday = 6
+ time = title = f"星期{WEEK_MAP[weekday]}"
+ else: # 获取今日是星期几,判定了是否过了凌晨4点
+ title = "今日"
+ weekday = now.weekday() - (1 if now.hour < 4 else 0)
+ weekday = 6 if weekday < 0 else weekday
+ time = now.strftime("%m-%d %H:%M") + " 星期" + WEEK_MAP[weekday]
+ full = args and args[-1] == 'full'
+
+ if weekday == 6:
+ notice = await update.message.reply_text(
+ ("今天" if title == '今日' else '这天') + "是星期天, 全部素材都可以刷哦~",
+ parse_mode=ParseMode.HTML
+ )
+ self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5)
+ return
+
+ if self.locks[0].locked():
+ notice = await update.message.reply_text("派蒙正在摘抄每日素材表,以后再来探索吧~")
+ self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5)
+ return
+
+ if self.locks[1].locked():
+ notice = await update.message.reply_text("派蒙正在搬运每日素材的图标,以后再来探索吧~")
+ self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5)
+ return
+
+ notice = await update.message.reply_text("派蒙可能需要找找图标素材,还请耐心等待哦~")
+ await update.message.reply_chat_action(ChatAction.TYPING)
+
+ # 获取已经缓存至本地的秘境素材信息
+ local_data = {'character': [], 'weapon': []}
+ if not self.data:
+ logger.info("正在获取每日素材缓存")
+ await self._refresh_data()
+ for domain, sche in self.data.items():
+ area = DOMAIN_AREA_MAP[domain]
+ type_ = 'character' if DOMAINS.index(domain) < 4 else 'weapon'
+ local_data[type_].append({'name': area, 'materials': sche[weekday][0], 'items': sche[weekday][1]})
+
+ # 尝试获取用户已绑定的原神账号信息
+ client, user_data = self._get_data_from_user(user)
+
+ await update.message.reply_chat_action(ChatAction.TYPING)
+ render_data = RenderData(title=title, time=time, uid=client.uid if client else client)
+ for type_ in ['character', 'weapon']:
+ areas = []
+ for area_data in local_data[type_]:
+ items = []
+ for id_ in area_data['items']:
+ added = False
+ for i in user_data[type_]:
+ if id_ == i.id:
+ if i.rarity > 3: # 跳过 3 星及以下的武器
+ items.append(i)
+ added = True
+ break
+ if added:
+ continue
+ item = HONEY_ID_MAP[type_][id_]
+ if item[1] < 4: # 跳过 3 星及以下的武器
+ continue
+ items.append(ItemData(
+ id=id_, name=item[0], rarity=item[1],
+ icon=convert_path(await getattr(self.assets_service, f'{type_}')(id_).icon())
+ ))
+ materials = []
+ for mid in area_data['materials']:
+ path = convert_path(await self.assets_service.material(mid).icon())
+ material = HONEY_ID_MAP['material'][mid]
+ materials.append(ItemData(id=mid, icon=path, name=material[0], rarity=material[1]))
+ areas.append(AreaData(name=area_data['name'], materials=materials, items=sort_item(items)))
+ setattr(render_data, type_, areas)
+ await update.message.reply_chat_action(ChatAction.TYPING)
+ character_img_data = await self.template_service.render(
+ 'genshin/daily_material', 'character.html', {'data': render_data}, {'width': 1164, 'height': 500}
+ )
+ weapon_img_data = await self.template_service.render(
+ 'genshin/daily_material', 'weapon.html', {'data': render_data}, {'width': 1164, 'height': 500}
+ )
+ await update.message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
+ if full:
+ await update.message.reply_media_group([
+ InputMediaDocument(character_img_data, filename="可培养角色.png"),
+ InputMediaDocument(weapon_img_data, filename="可培养武器.png")
+ ])
+ else:
+ await update.message.reply_media_group(
+ [InputMediaPhoto(character_img_data), InputMediaPhoto(weapon_img_data)]
+ )
+ await notice.delete()
+
+ @handler.command('refresh_daily_material', block=False)
+ @bot_admins_rights_check
+ async def refresh(self, update: Update, context: CallbackContext):
+ message = update.effective_message
+ if self.locks[0].locked():
+ notice = await message.reply_text("派蒙还在抄每日素材表呢,我有在好好工作哦~")
+ self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10)
+ return
+ if self.locks[1].locked():
+ notice = await message.reply_text("派蒙正在搬运每日素材图标,在努力工作呢!")
+ self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10)
+ return
+ async with self.locks[1]:
+ notice = await message.reply_text("派蒙正在重新摘抄每日素材表,请稍等~", parse_mode=ParseMode.HTML)
+ async with self.locks[0]:
+ data = await self._refresh_data()
+ notice = await notice.edit_text(
+ "每日素材表" +
+ ("摘抄完成!" if data else "坏掉了!等会它再长好了之后我再抄。。。") +
+ '\n正搬运每日素材的图标中。。。',
+ parse_mode=ParseMode.HTML
+ )
+ self.data = data or self.data
+ await self._download_icon(notice)
+ notice = await notice.edit_text(
+ notice.text_html.split('\n')[0] + "\n每日素材图标搬运完成!",
+ parse_mode=ParseMode.HTML
+ )
+ self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10)
+
+ async def _refresh_data(self, retry: int = 5) -> DATA_TYPE:
+ from bs4 import Tag
+ from asyncio import sleep
+ result = {}
+ for i in range(retry):
+ try:
+ response = await self.client.get("https://genshin.honeyhunterworld.com/?lang=CHS")
+ soup = BeautifulSoup(response.text, 'lxml')
+ calendar = soup.select(".calendar_day_wrap")[0]
+ key: str = ''
+ for tag in calendar:
+ tag: Tag
+ if tag.name == 'span':
+ key = tag.find('a').text
+ result[key] = [[[], []] for _ in range(7)]
+ for day, div in enumerate(tag.find_all('div')):
+ result[key][day][0] = [re.findall(r"/(.*)?/", a['href'])[0] for a in div.find_all('a')]
+ else:
+ id_ = re.findall(r"/(.*)?/", tag['href'])[0]
+ if tag.text.strip() == '旅行者':
+ continue
+ for day in map(int, tag.find('div')['data-days']):
+ result[key][day][1].append(id_)
+ for stage, schedules in result.items():
+ for day, _ in enumerate(schedules):
+ result[stage][day][1] = list(set(result[stage][day][1]))
+ async with async_open(DATA_FILE_PATH, 'w', encoding='utf-8') as file:
+ await file.write(json.dumps(result)) # pylint: disable=PY-W0079
+ logger.info("每日素材刷新成功")
+ break
+ except HTTPError:
+ await sleep(1)
+ if i <= retry - 1:
+ logger.warning("每日素材刷新失败, 正在重试")
+ else:
+ logger.error("每日素材刷新失败, 请稍后重试")
+ continue
+ # noinspection PyTypeChecker
+ return result
+
+ async def _download_icon(self, message: Optional[Message] = None):
+ from time import time as time_
+ lock = asyncio.Lock()
+ the_time = Value(c_double, time_() - 1)
+ interval = 0.2
+
+ async def task(_id, _item, _type):
+ logger.debug(f"正在开始下载 \"{_item[0]}\" 的图标素材")
+ async with lock:
+ if message is not None and time_() >= the_time.value + interval:
+ text = '\n'.join(message.text_html.split('\n')[:2]) + f"\n正在搬运 {_item[0]} 的图标素材。。。"
+ try:
+ await message.edit_text(text, parse_mode=ParseMode.HTML)
+ except (TimedOut, RetryAfter):
+ pass
+ the_time.value = time_()
+ asset = getattr(self.assets_service, _type)(_id)
+ icon_types = list(filter(
+ lambda x: not x.startswith('_') and x not in ['path'] and callable(getattr(asset, x)),
+ dir(asset)
+ ))
+ icon_coroutines = map(lambda x: getattr(asset, x), icon_types)
+ for coroutine in icon_coroutines:
+ await coroutine()
+ logger.debug(f"\"{_item[0]}\" 的图标素材下载成功")
+ async with lock:
+ if message is not None and time_() >= the_time.value + interval:
+ text = (
+ '\n'.join(message.text_html.split('\n')[:2]) +
+ f"\n正在搬运 {_item[0]} 的图标素材。。。成功!"
+ )
+ try:
+ await message.edit_text(text, parse_mode=ParseMode.HTML)
+ except (TimedOut, RetryAfter):
+ pass
+ the_time.value = time_()
+
+ for type_, items in HONEY_ID_MAP.items():
+ task_list = []
+ for id_, item in items.items():
+ task_list.append(asyncio.create_task(task(id_, item, type_)))
+ await asyncio.gather(*task_list)
+
+ logger.info("图标素材下载完成")
+
+
+class ItemData(BaseModel):
+ id: str
+ name: str
+ rarity: int
+ icon: str
+ level: Optional[int] = None
+ constellation: Optional[int] = None
+ refinement: Optional[int] = None
+ c_path: Optional[str] = None
+
+
+class AreaData(BaseModel):
+ name: Literal['蒙德', '璃月', '稻妻', '须弥']
+ materials: List[ItemData] = []
+ items: Iterable[ItemData] = []
+
+
+class RenderData(BaseModel):
+ title: str
+ time: str
+ uid: Optional[int] = None
+ character: List[AreaData] = []
+ weapon: List[AreaData] = []
+
+ def __getitem__(self, item):
+ return self.__getattribute__(item)
diff --git a/resources/genshin/daily_material/bg/area/0.png b/resources/genshin/daily_material/bg/area/0.png
new file mode 100644
index 0000000..23162f7
Binary files /dev/null and b/resources/genshin/daily_material/bg/area/0.png differ
diff --git a/resources/genshin/daily_material/bg/area/1.png b/resources/genshin/daily_material/bg/area/1.png
new file mode 100644
index 0000000..7a40aa3
Binary files /dev/null and b/resources/genshin/daily_material/bg/area/1.png differ
diff --git a/resources/genshin/daily_material/bg/area/2.png b/resources/genshin/daily_material/bg/area/2.png
new file mode 100644
index 0000000..979a9f5
Binary files /dev/null and b/resources/genshin/daily_material/bg/area/2.png differ
diff --git a/resources/genshin/daily_material/bg/area/3.png b/resources/genshin/daily_material/bg/area/3.png
new file mode 100644
index 0000000..18e5bcc
Binary files /dev/null and b/resources/genshin/daily_material/bg/area/3.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/full/1.png b/resources/genshin/daily_material/bg/rarity/full/1.png
new file mode 100644
index 0000000..5da532e
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/1.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/full/2.png b/resources/genshin/daily_material/bg/rarity/full/2.png
new file mode 100644
index 0000000..82cb30c
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/2.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/full/3.png b/resources/genshin/daily_material/bg/rarity/full/3.png
new file mode 100644
index 0000000..5753e70
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/3.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/full/4.png b/resources/genshin/daily_material/bg/rarity/full/4.png
new file mode 100644
index 0000000..36cb6cd
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/4.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/full/5.png b/resources/genshin/daily_material/bg/rarity/full/5.png
new file mode 100644
index 0000000..efb48ab
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/5.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/half/1.png b/resources/genshin/daily_material/bg/rarity/half/1.png
new file mode 100644
index 0000000..5811598
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/1.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/half/2.png b/resources/genshin/daily_material/bg/rarity/half/2.png
new file mode 100644
index 0000000..c1d5363
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/2.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/half/3.png b/resources/genshin/daily_material/bg/rarity/half/3.png
new file mode 100644
index 0000000..44aa8c3
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/3.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/half/4.png b/resources/genshin/daily_material/bg/rarity/half/4.png
new file mode 100644
index 0000000..8079547
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/4.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/half/5.png b/resources/genshin/daily_material/bg/rarity/half/5.png
new file mode 100644
index 0000000..2630db5
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/5.png differ
diff --git a/resources/genshin/daily_material/bg/rarity/star.webp b/resources/genshin/daily_material/bg/rarity/star.webp
new file mode 100644
index 0000000..c58ea8e
Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/star.webp differ
diff --git a/resources/genshin/daily_material/bg/title/01.png b/resources/genshin/daily_material/bg/title/01.png
new file mode 100644
index 0000000..af014b2
Binary files /dev/null and b/resources/genshin/daily_material/bg/title/01.png differ
diff --git a/resources/genshin/daily_material/bg/title/02.png b/resources/genshin/daily_material/bg/title/02.png
new file mode 100644
index 0000000..55e797e
Binary files /dev/null and b/resources/genshin/daily_material/bg/title/02.png differ
diff --git a/resources/genshin/daily_material/character.html b/resources/genshin/daily_material/character.html
new file mode 100644
index 0000000..2242f0d
--- /dev/null
+++ b/resources/genshin/daily_material/character.html
@@ -0,0 +1,74 @@
+
+
+
+