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/
|
||||
|
||||
# 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
|
||||
|
||||
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