mirror of
https://github.com/Xtao-Labs/WakeUp2XiaoAi.git
synced 2024-11-16 04:35:52 +00:00
🎉 begin a project
This commit is contained in:
parent
bdfbdeccb7
commit
fad79bc923
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
FILE_PATH=test.wakeup_schedule
|
||||||
|
XIAOAI_URL=https://i.ai.mi.com/course-multi/table?ctId=1&userId=2&deviceId=3&sourceName=course-app-browser
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -127,3 +127,6 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# data
|
||||||
|
*.wakeup_schedule
|
||||||
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: main",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "main.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
27
README.md
27
README.md
@ -1,2 +1,29 @@
|
|||||||
# WakeUp2XiaoAi
|
# WakeUp2XiaoAi
|
||||||
|
|
||||||
A python script to convert WakeUp json to Xiao Ai Schedule.
|
A python script to convert WakeUp json to Xiao Ai Schedule.
|
||||||
|
|
||||||
|
将 WakeUp 课程表的数据导入到小爱课程表,解决小爱课程表教务导入过于破烂的问题
|
||||||
|
|
||||||
|
# 使用方法
|
||||||
|
|
||||||
|
## 导出课程表
|
||||||
|
|
||||||
|
将 WakeUp 中已经导入好的课程表使用右上方分享按钮 `导出为备份(可导入)文件` 放置于 `main.py` 同目录下,假设此文件名称为:`test.wakeup_schedule`
|
||||||
|
|
||||||
|
## 建立课程表
|
||||||
|
|
||||||
|
在小爱课程表建立一个课程表,设置好上课时间、总周数、课程节数、课表时间
|
||||||
|
|
||||||
|
## 导出编辑链接
|
||||||
|
|
||||||
|
在建好的课程表设置页,选择 `PC编辑课表` 导出编辑链接,然后复制链接,假设此链接为:`https://i.ai.mi.com/h5/precache/ai-schedule/#/pceditor?token=1`
|
||||||
|
|
||||||
|
## 运行脚本
|
||||||
|
|
||||||
|
`python3 main.py`
|
||||||
|
|
||||||
|
然后输入 `test.wakeup_schedule`
|
||||||
|
|
||||||
|
然后输入 `https://i.ai.mi.com/h5/precache/ai-schedule/#/pceditor?token=1`
|
||||||
|
|
||||||
|
回到小爱课程表,发现同步完成。
|
||||||
|
7
config.py
Normal file
7
config.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
FILE_PATH = os.getenv('FILE_PATH')
|
||||||
|
XIAOAI_URL = os.getenv("XIAOAI_URL")
|
65
defs/wakeup.py
Normal file
65
defs/wakeup.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
from models.wakeup import Course, CourseInfo
|
||||||
|
from models.xiaoai import AiCourse
|
||||||
|
|
||||||
|
|
||||||
|
class WakeUp:
|
||||||
|
def __init__(self, file_path: str) -> None:
|
||||||
|
while True:
|
||||||
|
self.file_path = file_path or input("请输入文件路径 (例如 test.wakeup_schedule ): ")
|
||||||
|
path = Path(self.file_path)
|
||||||
|
if not path.exists():
|
||||||
|
print("文件不存在, 请重新输入路径.")
|
||||||
|
file_path = None
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
self.old_data: List[Course] = []
|
||||||
|
self.old_data_info: List[CourseInfo] = []
|
||||||
|
self.old_data_info_map: Dict[int, CourseInfo] = {}
|
||||||
|
self.weeks_map: Dict[str, List[int]] = {}
|
||||||
|
self.new_data: List[AiCourse] = []
|
||||||
|
self.new_data_map: Dict[str, List[AiCourse]] = {}
|
||||||
|
|
||||||
|
def load_data(self) -> None:
|
||||||
|
""" 解析 wakeup 备份文件数据 """
|
||||||
|
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
self.old_data_info = [CourseInfo(**i) for i in json.loads(lines[3])]
|
||||||
|
for i in self.old_data_info:
|
||||||
|
self.old_data_info_map[i.id] = i
|
||||||
|
self.old_data = [Course(**i) for i in json.loads(lines[4])]
|
||||||
|
for i in self.old_data:
|
||||||
|
data = self.weeks_map.get(i.key, [].copy())
|
||||||
|
data.extend(i.weeks)
|
||||||
|
self.weeks_map[i.key] = data
|
||||||
|
# 移除重复 id
|
||||||
|
temp_keys, old_data = [], []
|
||||||
|
for i in self.old_data:
|
||||||
|
if i.key in temp_keys:
|
||||||
|
continue
|
||||||
|
old_data.append(i)
|
||||||
|
temp_keys.append(i.key)
|
||||||
|
self.old_data = old_data
|
||||||
|
|
||||||
|
def get_weeks(self, temp: Course) -> str:
|
||||||
|
""" 获取当前课程周数 """
|
||||||
|
return ",".join(list(map(str, self.weeks_map[temp.key])))
|
||||||
|
|
||||||
|
def convert_data(self) -> List[AiCourse]:
|
||||||
|
""" 转换为小爱课程表格式 """
|
||||||
|
for i in self.old_data:
|
||||||
|
old_data_info = self.old_data_info_map[i.id]
|
||||||
|
self.new_data.append(
|
||||||
|
AiCourse(
|
||||||
|
name=old_data_info.courseName,
|
||||||
|
position=i.room,
|
||||||
|
teacher=i.teacher,
|
||||||
|
weeks=self.get_weeks(i),
|
||||||
|
day=i.day,
|
||||||
|
style=old_data_info.style,
|
||||||
|
sections=i.sections,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return self.new_data
|
81
defs/xiaoai.py
Normal file
81
defs/xiaoai.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import base64
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from models.xiaoai import AiCourse, AiCourseInfo, Table, TabCourse
|
||||||
|
|
||||||
|
|
||||||
|
class XiaoAi:
|
||||||
|
HEADERS = {
|
||||||
|
'authority': 'i.ai.mi.com',
|
||||||
|
'accept': 'application/json',
|
||||||
|
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,hu;q=0.5',
|
||||||
|
'access-control-allow-origin': 'true',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'origin': 'https://i.ai.mi.com',
|
||||||
|
'referer': 'https://i.ai.mi.com/h5/precache/ai-schedule/',
|
||||||
|
'sec-ch-ua': '"Chromium";v="110", "Not A(Brand";v="24", "Microsoft Edge";v="110"',
|
||||||
|
'sec-ch-ua-mobile': '?0',
|
||||||
|
'sec-ch-ua-platform': '"Windows"',
|
||||||
|
'sec-fetch-dest': 'empty',
|
||||||
|
'sec-fetch-mode': 'cors',
|
||||||
|
'sec-fetch-site': 'same-origin',
|
||||||
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
|
||||||
|
}
|
||||||
|
URL = "https://i.ai.mi.com/course-multi/courseInfo"
|
||||||
|
TABLE_URL = "https://i.ai.mi.com/course-multi/table"
|
||||||
|
|
||||||
|
def __init__(self, url_temp: str) -> None:
|
||||||
|
while True:
|
||||||
|
url = url_temp or input("请输入包含 token 的小爱课程表链接 : ")
|
||||||
|
token = re.search(r"token=(.*)", url)
|
||||||
|
if not token:
|
||||||
|
print("链接错误, 请重新输入。")
|
||||||
|
url_temp = None
|
||||||
|
continue
|
||||||
|
decoded_text = base64.b64decode(token[1]).decode('utf-8').split("%26")
|
||||||
|
self.user_id = int(decoded_text[0])
|
||||||
|
self.device_id = decoded_text[1]
|
||||||
|
timestamp = int(decoded_text[2]) / 1000
|
||||||
|
self.expire_time = datetime.fromtimestamp(timestamp)
|
||||||
|
self.ct_id = int(decoded_text[3])
|
||||||
|
break
|
||||||
|
self.client: AsyncClient = AsyncClient(headers=self.HEADERS)
|
||||||
|
self.table: Table = None
|
||||||
|
|
||||||
|
async def get_all_course(self) -> Table:
|
||||||
|
""" 获取课程表数据 """
|
||||||
|
params = {
|
||||||
|
'ctId': str(self.ct_id),
|
||||||
|
'userId': str(self.user_id),
|
||||||
|
'deviceId': self.device_id,
|
||||||
|
'sourceName': 'course-app-browser',
|
||||||
|
}
|
||||||
|
data = await self.client.get(self.TABLE_URL, params=params)
|
||||||
|
json_data = data.json()
|
||||||
|
if json_data["code"] != 0:
|
||||||
|
raise ValueError("课程表不存在或者链接已过期")
|
||||||
|
self.table = Table(**json_data["data"])
|
||||||
|
return self.table
|
||||||
|
|
||||||
|
async def delete_course(self, course: TabCourse) -> None:
|
||||||
|
""" 删除某个课程 """
|
||||||
|
json_data = {
|
||||||
|
'ctId': self.ct_id,
|
||||||
|
'userId': self.user_id,
|
||||||
|
'deviceId': self.device_id,
|
||||||
|
'cId': course.id,
|
||||||
|
'sourceName': 'course-app-browser',
|
||||||
|
}
|
||||||
|
await self.client.request("DELETE", self.URL, json=json_data)
|
||||||
|
|
||||||
|
async def delete_all_course(self) -> None:
|
||||||
|
""" 删除课程表数据 """
|
||||||
|
for i in self.table.courses:
|
||||||
|
await self.delete_course(i)
|
||||||
|
|
||||||
|
async def add_course(self, course: AiCourse) -> None:
|
||||||
|
""" 添加课程 """
|
||||||
|
info = AiCourseInfo(userId=self.user_id, deviceId=self.device_id, ctId=self.ct_id, course=course)
|
||||||
|
await self.client.post(self.URL, json=info.dict())
|
27
main.py
Normal file
27
main.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import asyncio
|
||||||
|
import warnings
|
||||||
|
from config import FILE_PATH, XIAOAI_URL
|
||||||
|
from defs.wakeup import WakeUp
|
||||||
|
from defs.xiaoai import XiaoAi
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
wakeup = WakeUp(FILE_PATH)
|
||||||
|
xiaoai = XiaoAi(XIAOAI_URL)
|
||||||
|
print(f"链接过期时间:{xiaoai.expire_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
wakeup.load_data()
|
||||||
|
data = wakeup.convert_data()
|
||||||
|
table = await xiaoai.get_all_course()
|
||||||
|
print(f"开始处理课程表 - {table.name}")
|
||||||
|
print("开始删除课程表数据")
|
||||||
|
await xiaoai.delete_all_course()
|
||||||
|
print("开始导入课程表数据")
|
||||||
|
for i in data:
|
||||||
|
await xiaoai.add_course(i)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(main())
|
36
models/wakeup.py
Normal file
36
models/wakeup.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from typing import List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Course(BaseModel):
|
||||||
|
id: int
|
||||||
|
day: int # 周几
|
||||||
|
startWeek: int # 开始的周次
|
||||||
|
endWeek: int # 停止的周次
|
||||||
|
startNode: int # 开始的节次
|
||||||
|
step: int # 有多少节
|
||||||
|
room: str # 位置
|
||||||
|
teacher: str # 老师
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sections(self) -> str:
|
||||||
|
data = map(str, list(range(self.startNode, self.startNode + self.step)))
|
||||||
|
return ",".join(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key(self) -> str:
|
||||||
|
return f"{self.id}_{self.day}_{self.startNode}_{self.step}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weeks(self) -> List[int]:
|
||||||
|
return list(range(self.startWeek, self.endWeek + 1))
|
||||||
|
|
||||||
|
|
||||||
|
class CourseInfo(BaseModel):
|
||||||
|
id: int
|
||||||
|
color: str
|
||||||
|
courseName: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self) -> str:
|
||||||
|
return '{"color":"#FFFFFF", "background":"#' + self.color[3:] + '"}'
|
30
models/xiaoai.py
Normal file
30
models/xiaoai.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from typing import List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AiCourse(BaseModel):
|
||||||
|
name: str
|
||||||
|
position: str
|
||||||
|
teacher: str
|
||||||
|
weeks: str
|
||||||
|
day: int
|
||||||
|
style: str
|
||||||
|
sections: str
|
||||||
|
|
||||||
|
|
||||||
|
class TabCourse(AiCourse):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class AiCourseInfo(BaseModel):
|
||||||
|
userId: int
|
||||||
|
deviceId: str
|
||||||
|
ctId: int
|
||||||
|
course: AiCourse
|
||||||
|
sourceName: str = "course-app-browser"
|
||||||
|
|
||||||
|
|
||||||
|
class Table(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
courses: List[TabCourse]
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pydantic==1.10.5
|
||||||
|
httpx==0.23.3
|
||||||
|
python-dotenv==1.0.0
|
Loading…
Reference in New Issue
Block a user