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