diff --git a/.gitignore b/.gitignore
index 68bc17f..2dc53ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -157,4 +157,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+.idea/
diff --git a/add.py b/add.py
new file mode 100644
index 0000000..08180b7
--- /dev/null
+++ b/add.py
@@ -0,0 +1,55 @@
+from pathlib import Path
+from sys import argv
+from datetime import datetime
+from typing import List
+
+from models.code import CodeList, Code, Reward
+
+data_path = Path("data")
+custom_path = data_path / "custom.json"
+
+
+def add(code: str, expire_str: str, rewards_str: List[str]) -> Code:
+ expire = datetime.strptime(expire_str, "%Y-%m-%d")
+ expire = expire.replace(hour=23, minute=59, second=59, microsecond=999999)
+ rewards = []
+ for reward_str in rewards_str:
+ reward_list = reward_str.split(":")
+ rewards.append(
+ Reward(
+ name=reward_list[0],
+ cnt=int(reward_list[1])
+ )
+ )
+ return Code(
+ code=code,
+ expire=int(expire.timestamp() * 1000),
+ reward=rewards,
+ )
+
+
+if __name__ == '__main__':
+ try:
+ add_type = argv[1]
+ if add_type not in ["main", "over"]:
+ raise IndexError
+ code = add(argv[2], argv[3], argv[4:])
+ with open(custom_path, "r", encoding="utf-8") as f:
+ custom: CodeList = CodeList.parse_raw(f.read())
+ if add_type == "main":
+ main_codes = [i.code for i in custom.main]
+ if code.code in main_codes:
+ raise ValueError("Duplicate code")
+ custom.main.append(code)
+ else:
+ over_codes = [i.code for i in custom.over]
+ if code.code in over_codes:
+ raise ValueError("Duplicate code")
+ custom.over.append(code)
+ custom.main.sort(key=lambda x: x.expire, reverse=True)
+ custom.over.sort(key=lambda x: x.expire, reverse=True)
+ with open(custom_path, "w", encoding="utf-8") as f:
+ f.write(custom.json(indent=4, ensure_ascii=False))
+ except IndexError:
+ print("Usage: python add.py [main/over] [code] [expire] [rewards...]")
+ print("Example: python add.py main code 2023-11-1 星琼:1 信用点:1000")
diff --git a/data/code.html b/data/code.html
new file mode 100644
index 0000000..f614781
--- /dev/null
+++ b/data/code.html
@@ -0,0 +1,189 @@
+
+
+
+
+
+ Dark Responsive Page with Tabs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
未到期
+
+
+
+ 兑换码 |
+ 奖励 |
+ 到期时间 |
+
+
+
+
+
+
已到期
+
+
+
+ 兑换码 |
+ 奖励 |
+ 到期时间 |
+
+
+
+
+
+
+
+
+
+
+
+
未到期
+
+
+
+ 兑换码 |
+ 奖励 |
+ 到期时间 |
+
+
+
+
+
+
已到期
+
+
+
+ 兑换码 |
+ 奖励 |
+ 到期时间 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/code.json b/data/code.json
new file mode 100644
index 0000000..a43cfc7
--- /dev/null
+++ b/data/code.json
@@ -0,0 +1,214 @@
+{
+ "main": [
+ {
+ "code": "MIYOUSHE2023",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 60
+ }
+ ],
+ "expire": 1686402166000
+ }
+ ],
+ "over": [
+ {
+ "code": "BSN2EWMHA4RP",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "信用点",
+ "cnt": 10000
+ }
+ ],
+ "expire": 4102415999999
+ },
+ {
+ "code": "HSRVER10JYTGHC",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "信用点",
+ "cnt": 10000
+ }
+ ],
+ "expire": 4102415999999
+ },
+ {
+ "code": "STARRAILGIFT",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 50
+ },
+ {
+ "name": "信用点",
+ "cnt": 10000
+ }
+ ],
+ "expire": 4102415999999
+ },
+ {
+ "code": "SURPRISE1024",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "冒险记录",
+ "cnt": 3
+ },
+ {
+ "name": "疾速粉尘",
+ "cnt": 2
+ },
+ {
+ "name": "信用点",
+ "cnt": 5000
+ }
+ ],
+ "expire": 1686070799999
+ },
+ {
+ "code": "ZTPTNMTX8LUF",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "信用点",
+ "cnt": 50000
+ }
+ ],
+ "expire": 1685206799999
+ },
+ {
+ "code": "8A6T6LBFQ4D3",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "漫游指南",
+ "cnt": 5
+ }
+ ],
+ "expire": 1685206799999
+ },
+ {
+ "code": "DB7A64BW8LC7",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "提纯以太",
+ "cnt": 4
+ }
+ ],
+ "expire": 1685206799999
+ },
+ {
+ "code": "CS75WMP976AK",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ }
+ ],
+ "expire": 1685206799999
+ },
+ {
+ "code": "HSRVER10XEDLFE",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 50
+ },
+ {
+ "name": "信用点",
+ "cnt": 10000
+ },
+ {
+ "name": "漫游指南",
+ "cnt": 2
+ }
+ ],
+ "expire": 1683565199999
+ },
+ {
+ "code": "2T7BP4JVEBT7",
+ "reward": [
+ {
+ "name": "信用点",
+ "cnt": 5000
+ },
+ {
+ "name": "冒险记录",
+ "cnt": 3
+ },
+ {
+ "name": "凝缩以太",
+ "cnt": 2
+ },
+ {
+ "name": "大宇宙炒饭",
+ "cnt": 3
+ }
+ ],
+ "expire": 1683392399999
+ },
+ {
+ "code": "HSRGRANDOPEN1",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "信用点",
+ "cnt": 5000
+ }
+ ],
+ "expire": 1682787599999
+ },
+ {
+ "code": "HSRGRANDOPEN2",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "漫游指南",
+ "cnt": 5
+ }
+ ],
+ "expire": 1682787599999
+ },
+ {
+ "code": "HSRGRANDOPEN3",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 100
+ },
+ {
+ "name": "提纯以太",
+ "cnt": 4
+ }
+ ],
+ "expire": 1682787599999
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/custom.json b/data/custom.json
new file mode 100644
index 0000000..4765dcd
--- /dev/null
+++ b/data/custom.json
@@ -0,0 +1,15 @@
+{
+ "main": [
+ {
+ "code": "MIYOUSHE2023",
+ "reward": [
+ {
+ "name": "星琼",
+ "cnt": 60
+ }
+ ],
+ "expire": 1689695999999
+ }
+ ],
+ "over": []
+}
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..3b402cd
--- /dev/null
+++ b/main.py
@@ -0,0 +1,33 @@
+from typing import List
+
+from models.code import CodeList, Code
+from models.honkai import get_code
+
+from pathlib import Path
+
+data_path = Path("data")
+custom_path = data_path / "custom.json"
+code_path = data_path / "code.json"
+
+
+def merge_code(over: List[Code], custom: CodeList) -> CodeList:
+ over_codes = [i for i in over]
+ custom_over_codes = [i.code for i in custom.over]
+ for code in over_codes:
+ if code.code in custom_over_codes:
+ continue
+ custom.over.append(code)
+ return custom
+
+
+def main():
+ over = get_code()
+ with open(custom_path, "r", encoding="utf-8") as f:
+ custom = CodeList.parse_raw(f.read())
+ custom = merge_code(over, custom)
+ with open(code_path, "w", encoding="utf-8") as f:
+ f.write(custom.json(indent=4, ensure_ascii=False))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/models/__init__.py b/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/models/code.py b/models/code.py
new file mode 100644
index 0000000..46a2546
--- /dev/null
+++ b/models/code.py
@@ -0,0 +1,19 @@
+from typing import List
+
+from pydantic import BaseModel
+
+
+class Reward(BaseModel):
+ name: str
+ cnt: int
+
+
+class Code(BaseModel):
+ code: str
+ reward: List[Reward]
+ expire: int
+
+
+class CodeList(BaseModel):
+ main: List[Code]
+ over: List[Code]
diff --git a/models/honkai.py b/models/honkai.py
new file mode 100644
index 0000000..4962d58
--- /dev/null
+++ b/models/honkai.py
@@ -0,0 +1,81 @@
+from datetime import datetime
+from typing import List
+
+from httpx import get
+from bs4 import BeautifulSoup, Tag
+from .code import Code, Reward
+
+
+url = "https://honkai.gg/cn/codes"
+reward_map = {
+ "Stellar Jade": "星琼",
+ "Credit": "信用点",
+ "Credits": "信用点",
+ "Traveler's Guide": "漫游指南",
+ "Refined Aether": "提纯以太",
+ "Adventure Log": "冒险记录",
+ "Dust of Alacrity": "疾速粉尘",
+ "Condensed Aether": "凝缩以太",
+ "Cosmic Fried Rice": "大宇宙炒饭",
+}
+
+
+def parse_reward(reward: List[str]) -> Reward:
+ try:
+ name = reward_map.get(reward[0])
+ if not name:
+ print("Unknown reward: ", reward[0])
+ name = reward[0]
+ return Reward(
+ name=name,
+ cnt=int(reward[1]),
+ )
+ except ValueError:
+ print("Bad reward data: ", reward)
+
+
+def parse_code(tr: Tag) -> Code:
+ tds = tr.find_all("td")
+ code = tds[0].text.strip()
+ expire = tds[2].text.strip()
+ if expire.endswith("?"):
+ expire = datetime(2099, 12, 31, 23, 59, 59, 999999)
+ else:
+ expires = expire.split(" - ")
+ day = expires[1].split(" ")[-1]
+ month = expires[0].split(" ")[0]
+ try:
+ if " " not in expires[1]:
+ raise ValueError
+ month = expires[1].split(" ")[0]
+ except ValueError:
+ pass
+ now = datetime.now()
+ expire = datetime.strptime(f"{day} {month}", "%d %b")
+ expire = expire.replace(year=now.year, hour=23, minute=59, second=59, microsecond=999999)
+ expire = int(expire.timestamp() * 1000)
+ rewards = []
+ for reward in tds[1].find_all("div", {"class": "flex"}):
+ reward_div = reward.text.strip().split("\xa0x ")
+ parsed_reward = parse_reward(reward_div)
+ if parsed_reward:
+ rewards.append(parsed_reward)
+ for reward in tds[1].find_all("a"):
+ reward_a = reward.text.strip().split(" x ")
+ parsed_reward = parse_reward(reward_a)
+ if parsed_reward:
+ rewards.append(parsed_reward)
+ return Code(code=code, reward=rewards, expire=expire)
+
+
+def get_code():
+ html = get(url).text
+ soup = BeautifulSoup(html, "lxml")
+ tables = soup.find_all("table")
+ codes = []
+ for table in tables:
+ trs = table.find_all("tr")[1:]
+ for tr in trs:
+ codes.append(parse_code(tr))
+ codes.sort(key=lambda x: x.expire, reverse=True)
+ return codes
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fe60500
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+pydantic
+httpx
+beautifulsoup4
+lxml