1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 07:23:03 +08:00

Compare commits

...

13 Commits

Author SHA1 Message Date
KenanZhu 842fb434f4 feat(AutoLib): new feature 'Auto Renew' 2025-11-28 15:03:51 +08:00
KenanZhu 6cabddf0cd fix(operators): optimized the reserve information pre-check and more readable output 2025-11-28 15:00:09 +08:00
KenanZhu 0322558339 fix(operators): the operations's result message only show in their output queue 2025-11-28 14:58:13 +08:00
KenanZhu 703ee527ae fix(LibChecker): fix the checker of check in and renew
we only check the reservations and their status in
today's record, and return the checked renewable
record for the upcoming new feature 'Auto-Renew'
2025-11-28 14:54:37 +08:00
KenanZhu 9a925fecb6 fix(operators): fix some type hint, and add imports for LibRenew 2025-11-28 14:53:08 +08:00
KenanZhu 189fddfb6a fix(LibReserve): more fast operations of reserve 2025-11-28 14:46:17 +08:00
KenanZhu c2d53a8b78 chore(*): refactor the project structure 2025-11-25 08:48:18 +08:00
KenanZhu b99431476a hotfix(LibChecker): optimize the reserve records check process 2025-11-22 15:12:40 +08:00
KenanZhu 977c0835b7 hotfix(ALMainWindow): fix the config file paths initialization 2025-11-22 15:11:25 +08:00
KenanZhu cd565ec57d feat(gui.SeatMapWidget): add seat select map widget 2025-11-22 14:29:01 +08:00
KenanZhu 9f17474c1b fix(gui): optimize the config files' status management 2025-11-22 14:27:40 +08:00
KenanZhu 04d66346dc fix(ALConfigWidget): optimize the config window usage
add date calendar popup so that user can select
the date more easily
fix some file dialog title display issue
default max diff time change to 30 minutes
2025-11-22 14:23:35 +08:00
KenanZhu f858295af1 refactor(LibChecker): refactor the code of LibChecker to make it more readable and maintainable 2025-11-22 14:16:38 +08:00
31 changed files with 1850 additions and 706 deletions
+6 -6
View File
@@ -8,10 +8,10 @@ build/
dist/
model/*.onnx
driver/*.exe
gui/configs/*.json
gui/translators/qtbase_zh_CN.qm
gui/AutoLibraryResources.py
gui/AutoLibraryResource.py
gui/Ui_ALMainWindow.py
gui/Ui_ALConfigWidget.py
src/gui/configs/*.json
src/gui/translators/qtbase_zh_CN.qm
src/gui/AutoLibraryResources.py
src/gui/AutoLibraryResource.py
src/gui/Ui_ALMainWindow.py
src/gui/Ui_ALConfigWidget.py
Main.spec
-34
View File
@@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import queue
from LibOperator import LibOperator
class LibRenew(LibOperator):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
):
super().__init__(input_queue, output_queue)
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
pass
+1 -2
View File
@@ -1,6 +1,5 @@
# AutoLibrary
请访问[AutoLibrary 网站](http://autolibrary.cv)
请访问[AutoLibrary 网站](http://autolibrary.cv)\
Please access the [AutoLibrary Website](http://autolibrary.cv)
BIN
View File
Binary file not shown.
View File
+1 -1
View File
@@ -9,7 +9,7 @@ See the LICENSE file for details.
"""
import queue
from MsgBase import MsgBase
from base.MsgBase import MsgBase
class LibOperator(MsgBase):
View File
+8
View File
@@ -0,0 +1,8 @@
"""
Base module for the AutoLibrary project.
Here are the classes and modules in this package:
- MsgBase: Base class for messages.\
- LibOperator: Base class for library operators.
"""
@@ -18,10 +18,12 @@ from PySide6.QtWidgets import (
)
from PySide6.QtGui import QCloseEvent
from .Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.SeatMapWidget import SeatMapWidget
from ConfigReader import ConfigReader
from ConfigWriter import ConfigWriter
from gui.SeatMapTable import seats_maps
from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -32,27 +34,24 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self,
parent = None,
config_paths = {
"system":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
"users":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
"system": "",
"users": ""
}
):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
self.__config_paths = config_paths
self.__system_config_data = self.loadSystemConfig(self.__config_paths["system"])
self.__users_config_data = self.loadUsersConfig(self.__config_paths["users"])
if not self.__system_config_data:
self.initlizeDefaultConfig("system")
if not self.__users_config_data:
self.initlizeDefaultConfig("users")
self.initlizeConfigToWidget("system", self.__system_config_data)
self.initlizeConfigToWidget("users", self.__users_config_data)
self.__config_data = {"system": {}, "users": {}}
self.__seat_map_widget = None
self.modifyUi()
self.connectSignals()
self.initlizeFloorRoomMap()
self.initlizeDefaultConfigPaths()
if not self.initlizeConfigs():
self.close()
def modifyUi(
@@ -69,6 +68,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
self.SelectSeatsButton.clicked.connect(self.onSelectSeatsButtonClicked)
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
@@ -129,42 +129,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
def initlizeDefaultConfigPaths(
self
) -> dict:
):
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
return {
self.__default_config_paths = {
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
}
def initlizeDefaultConfig(
self,
which: str
):
default_config_paths = self.initlizeDefaultConfigPaths()
if which == "system":
self.__system_config_data = self.defaultSystemConfig()
self.__config_paths["system"] = default_config_paths["system"]
self.saveSystemConfig(self.__config_paths["system"], self.__system_config_data)
elif which == "users":
self.__users_config_data = self.defaultUsersConfig()
self.__config_paths["users"] = default_config_paths["users"]
self.saveUsersConfig(self.__config_paths["users"], self.__users_config_data)
if which == "system":
file_type = "系统配置文件"
elif which == "users":
file_type = "用户配置文件"
QMessageBox.information(
self,
"提示 - AutoLibrary",
f"{file_type}已初始化, \n"\
f" 文件路径: {self.__config_paths[which]}"
)
def initlizeConfigToWidget(
self,
which: str,
@@ -180,6 +154,63 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
def initlizeConfig(
self,
which: str
) -> bool:
msg = ""
is_success = True
if which == "system":
system_config_path = self.__config_paths[which]
if not os.path.exists(system_config_path):
self.__config_data[which] = self.defaultSystemConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveSystemConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"系统配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else:
is_success = False
else:
self.__config_data[which] = self.loadSystemConfig(system_config_path)
if self.__config_data[which] is None:
is_success = False
elif which == "users":
users_config_path = self.__config_paths[which]
if not os.path.exists(users_config_path):
self.__config_data[which] = self.defaultUsersConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveUsersConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else:
is_success = False
else:
self.__config_data[which] = self.loadUsersConfig(users_config_path)
if self.__config_data[which] is None:
is_success = False
if msg:
QMessageBox.information(
self,
"提示 - AutoLibrary",
f"配置文件初始化完成: \n{msg}"
)
return is_success
def initlizeConfigs(
self
) -> bool:
is_success = True
for which in ["system", "users"]:
if not self.__config_paths[which]:
self.__config_paths[which] = self.__default_config_paths[which]
if not self.initlizeConfig(which):
is_success = False
break
self.initlizeConfigToWidget(which, self.__config_data[which])
return is_success
def defaultSystemConfig(
self
) -> dict:
@@ -261,23 +292,23 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UsernameEdit.setText("")
self.PasswordEdit.setText("")
self.UserListWidget.setSortingEnabled(True)
self.PasswordEdit.setEchoMode(QLineEdit.Password)
self.PasswordEdit.setEchoMode(QLineEdit.EchoMode.Password)
self.ShowPasswordCheckBox.setChecked(False)
self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
self.FloorComboBox.setCurrentIndex(0)
self.onFloorComboBoxCurrentIndexChanged()
self.DateEdit.setDate(QDate.currentDate())
self.DateEdit.setMinimumDate(QDate.currentDate())
self.DateEdit.setMaximumDate(QDate.currentDate())
if QTime.currentTime() > QTime(18, 0, 0) and QTime.currentTime() < QTime(23, 0, 0):
self.DateEdit.setMaximumDate(QDate.currentDate().addDays(1))
self.BeginTimeEdit.setTime(QTime.currentTime())
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
self.MaxBeginTimeDiffSpinBox.setValue(10)
self.MaxBeginTimeDiffSpinBox.setValue(30)
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
self.PreferLateEndTimeCheckBox.setChecked(False)
self.MaxEndTimeDiffSpinBox.setValue(10)
self.MaxEndTimeDiffSpinBox.setValue(30)
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
self.SatisfyDurationCheckBox.setChecked(False)
self.ExpectRenewDurationSpinBox.setValue(1.0)
self.MaxRenewTimeDiffSpinBox.setValue(30)
self.PreferLateRenewTimeCheckBox.setChecked(False)
def collectUserConfigFromUserInfoWidget(
@@ -289,7 +320,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"password": self.PasswordEdit.text(),
"reserve_info": {
"begin_time":{},
"end_time": {}
"end_time": {},
"renew_time": {}
}
}
user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
@@ -305,6 +337,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked()
user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value()
user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked()
user_config["reserve_info"]["renew_time"]["expect_duration"] = self.ExpectRenewDurationSpinBox.value()
user_config["reserve_info"]["renew_time"]["max_diff"] = self.MaxRenewTimeDiffSpinBox.value()
user_config["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked()
return user_config
@@ -343,6 +378,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"])
self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"])
self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"])
self.ExpectRenewDurationSpinBox.setValue(user_config["reserve_info"]["renew_time"]["expect_duration"])
self.MaxRenewTimeDiffSpinBox.setValue(user_config["reserve_info"]["renew_time"]["max_diff"])
self.PreferLateRenewTimeCheckBox.setChecked(not user_config["reserve_info"]["renew_time"]["prefer_early"])
except:
QMessageBox.warning(
self,
@@ -451,21 +489,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> bool:
if users_config_path:
self.__users_config_data = self.defaultUsersConfig()
self.__config_data["users"] = self.defaultUsersConfig()
for index in range(self.UserListWidget.count()):
user_config = self.collectUserConfigFromUserListWidget(index)
if user_config:
self.__users_config_data["users"].append(user_config)
self.__config_data["users"]["users"].append(user_config)
if not self.saveUsersConfig(
users_config_path,
self.__users_config_data
self.__config_data["users"]
):
return False
if system_config_path:
self.__system_config_data = self.collectSystemConfigFromWidget()
self.__config_data["system"] = self.collectSystemConfigFromWidget()
if not self.saveSystemConfig(
system_config_path,
self.__system_config_data
self.__config_data["system"]
):
return False
return True
@@ -489,12 +527,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
system_config = self.loadSystemConfig(config_path)
users_config = self.loadUsersConfig(config_path)
if system_config is not None:
self.__system_config_data.update(system_config)
self.setSystemConfigToWidget(self.__system_config_data)
self.__config_data["system"].update(system_config)
self.setSystemConfigToWidget(self.__config_data["system"])
return True
if users_config is not None:
self.__users_config_data.update(users_config)
self.fillUsersList(self.__users_config_data)
self.__config_data["users"].update(users_config)
self.fillUsersList(self.__config_data["users"])
return True
except:
return False
@@ -528,16 +566,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"seat_id": "",
"begin_time": {
"time": f"{QTime.currentTime().toString("hh:mm")}",
"max_diff": 0,
"max_diff": 30,
"prefer_early": False
},
"end_time": {
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
"max_diff": 0,
"max_diff": 30,
"prefer_early": True
},
"expect_duration": 2.0,
"satisfy_duration": False
"satisfy_duration": False,
"renew_time": {
"expect_duration": 1.0,
"max_diff": 30,
"prefer_early": True
}
}
}
user_item = QListWidgetItem(new_user["username"])
@@ -577,6 +620,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.RoomComboBox.addItems(self.__floor_room_map[floor])
self.RoomComboBox.setCurrentIndex(0)
@Slot()
def onSeatMapWidgetClosed(
self,
selected_seats: list[str]
):
self.__seat_map_widget.seatMapWidgetClosed.disconnect(self.onSeatMapWidgetClosed)
self.__seat_map_widget.deleteLater()
self.__seat_map_widget = None
if len(selected_seats) == 0:
return
self.SeatIDEdit.setText(",".join(selected_seats))
@Slot()
def onSelectSeatsButtonClicked(
self
):
floor = self.FloorComboBox.currentText()
room = self.RoomComboBox.currentText()
floor_idx = self.__floor_rmap[floor]
room_idx = self.__room_rmap[room]
if self.__seat_map_widget is None:
self.__seat_map_widget = SeatMapWidget(
self,
floor,
room,
seats_maps[floor_idx][room_idx]
)
self.__seat_map_widget.seatMapWidgetClosed.connect(self.onSeatMapWidgetClosed)
self.__seat_map_widget.show()
self.__seat_map_widget.raise_()
self.__seat_map_widget.activateWindow()
self.__seat_map_widget.selectSeats(self.SeatIDEdit.text().split(","))
@Slot()
def onUserListWidgetCurrentItemChanged(
self,
@@ -621,7 +699,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
browser_driver_path = QFileDialog.getOpenFileName(
self,
"选择浏览器驱动 - AutoLibrary",
self.CurrentSystemConfigEdit.text(),
self.BrowseBrowserDriverEdit.text(),
"可执行文件 (*.exe);;所有文件 (*)"
)[0]
if browser_driver_path:
@@ -700,19 +778,22 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
users_config_path = self.ExportUserConfigEdit.text()
if system_config_path:
if self.saveConfigs(
system_config_path,
users_config_path=""
system_config_path, ""
):
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
else:
msg += f"系统配置文件导出失败: \n'{system_config_path}'\n"
if users_config_path:
if self.saveConfigs(
"", users_config_path
):
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
else:
msg += f"用户配置文件导出失败: \n'{users_config_path}'\n"
if msg:
QMessageBox.information(
self,
"信息 - AutoLibrary",
"提示 - AutoLibrary",
msg
)
@@ -748,21 +829,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
exist_files.append(users_config_path)
reply = QMessageBox.information(
self,
"信息 - AutoLibrary",
"提示 - AutoLibrary",
f"文件夹中已存在以下文件, 是否覆盖 ?\n{chr(10).join(exist_files)}",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
return
self.__system_config_data = self.defaultSystemConfig()
self.__users_config_data = self.defaultUsersConfig()
self.__config_data["system"] = self.defaultSystemConfig()
self.__config_data["users"] = self.defaultUsersConfig()
self.__config_paths = {
"system": system_config_path,
"users": users_config_path
}
self.initlizeConfigToWidget("system", self.__system_config_data)
self.initlizeConfigToWidget("users", self.__users_config_data)
self.initlizeConfigToWidget("system", self.__config_data["system"])
self.initlizeConfigToWidget("users", self.__config_data["users"])
@Slot()
def onConfirmButtonClicked(
@@ -770,16 +851,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
):
if self.UserListWidget.currentItem() is not None:
user = self.collectUserConfigFromUserInfoWidget()
if user:
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
user_config = self.collectUserConfigFromUserInfoWidget()
if user_config:
self.UserListWidget.currentItem().setData(Qt.UserRole, user_config)
if self.saveConfigs(
self.__config_paths["system"],
self.__config_paths["users"]
):
QMessageBox.information(
self,
"信息 - AutoLibrary",
"提示 - AutoLibrary",
"配置文件保存成功 !\n"
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
f"用户配置文件路径: \n{self.__config_paths['users']}"
File diff suppressed because it is too large Load Diff
@@ -27,8 +27,8 @@ from .ALConfigWidget import ALConfigWidget
from . import AutoLibraryResource
from AutoLib import AutoLib
from ConfigReader import ConfigReader
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader
class AutoLibWorker(QThread):
@@ -129,11 +129,11 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = {
"system":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
"users":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")),
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
}
self.__alConfigWidget = None
self.__auto_lib_thread = None
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import (
Qt, Signal
)
from PySide6.QtWidgets import (
QFrame, QLabel
)
class SeatFrame(QFrame):
clicked = Signal(str)
def __init__(
self,
seat_number,
parent=None
):
super().__init__(parent)
self.__seat_number = seat_number
self.__is_selected = False
self.setUpUi()
def setUpUi(
self
):
self.setFixedSize(60, 40)
self.setFrameStyle(QFrame.Box | QFrame.Plain)
self.setLineWidth(2)
self.setStyleSheet("""
QFrame {
background-color: #4196EB;
border: 2px solid #4196EB;
border-radius: 5px;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
self.label = QLabel(self.__seat_number, self)
self.label.setAlignment(Qt.AlignCenter)
self.label.setGeometry(0, 0, 60, 40)
def mousePressEvent(
self,
event
):
if event.button() == Qt.LeftButton:
self.toggleSelection()
self.clicked.emit(self.__seat_number)
def isSelected(
self
):
return self.__is_selected
def toggleSelection(self):
self.__is_selected = not self.__is_selected
if self.__is_selected:
self.setStyleSheet("""
QFrame {
background-color: #4CAF50;
border: 2px solid #388E3C;
border-radius: 5px;
color: white;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
else:
self.setStyleSheet("""
QFrame {
background-color: #4196EB;
border: 2px solid #4196EB;
border-radius: 5px;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
+270
View File
@@ -0,0 +1,270 @@
seats_maps = {
"2": {
"1": """
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
,,,,,,,,,,,039C,039D,,040C,040D,,041C,041D,,042C,042D,,043C,043D,,044C,044D,,,,,,,,,
038B,038D,,037B,037D,,036B,036D,,,,,,,,,,,,,,,,,,,,,,045C,045A,,046C,046A,,047C,047A
038A,038C,,037A,037C,,036A,036C,,,,,,,,,,,,,,,,,,,,,,045D,045B,,046D,046B,,047D,047B
035B,035D,,034B,034D,,033B,033D,,,,,,,,,,,,,,,,,,,,,,048C,048A,,049C,049A,,050C,050A
035A,035C,,034A,034C,,033A,033C,,,,,,,,,,,,,,,,,,,,,,048D,048B,,049D,049B,,050D,050B
032B,032D,,031B,031D,,030B,030D,,,,,,,,,,,,,,,,,,,,,,051C,051A,,052C,052A,,053C,053A
032A,032C,,031A,031C,,030A,030C,,,,,,,,,,,,,,,,,,,,,,051D,051B,,052D,052B,,053D,053B
029B,029D,,028B,028D,,027B,027D,,,,,,,,,,,,,,,,,,,,,,054C,054A,,055C,055A,,056C,056A
029A,029C,,028A,028C,,027A,027C,,,,,,,,,,,,,,,,,,,,,,054D,054B,,055D,055B,,056D,056B
026B,026D,,025B,025D,,024B,024D,,,,,,,,,,,,,,,,,,,,,,057C,057A,,058C,058A,,059C,059A
026A,026C,,025A,025C,,024A,024C,,,,,,,,,,,,,,,,,,,,,,057D,057B,,058D,058B,,059D,059B
023B,023D,,022B,022D,,021B,021D,,,,,,,,,,,,,,,,,,,,,,060C,060A,,061C,061A,,062C,062A
023A,023C,,022A,022C,,021A,021C,,,,,,,,,,,,,,,,,,,,,,060D,060B,,061D,061B,,062D,062B
020B,020D,,019B,019D,,018B,018D,,,,,,,,,,,,,,,,,,,,,,063C,063A,,064C,064A,,065C,065A
020A,020C,,019A,019C,,018A,018C,,,,,,,,,,,,,,,,,,,,,,063D,063B,,064D,064B,,065D,065B
,,,,,,,,,,,017D,017C,,014D,014C,,011D,011C,,008D,008C,,005D,005C,,002D,002C,001D,001C,,,,,,,
,,,,,,,,,,,017B,017A,,014B,014A,,011B,011A,,008B,008A,,005B,005A,,002B,002A,001B,001A,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,073D,073C,,015D,015C,,012D,012C,,,,,006D,006C,,003D,003C,,,,,,,,,
,,,,,,,,,,,073B,073A,,015B,015A,,012B,012A,,,,,006B,006A,,003B,003A,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,072D,072C,,016D,016C,,013D,013C,,,,,007D,007C,,004D,004C,,,,,,,,,
,,,,,,,,,,,072B,072A,,016B,016A,,013B,013A,,,,,007B,007A,,004B,004A,,,,,,,,,
,,,,,,,,,,,071D,071C,,070D,070C,,069D,069C,,068D,068C,,067D,067C,,066D,066C,,,,,,,,,
,,,,,,,,,,,071B,071A,,070B,070A,,069B,069A,,068B,068A,,067B,067A,,066B,066A,,,,,,,,,
""",
"2": """
023B,023D,024B,024D,,,,,,,,,,,,,,,
023A,023C,024A,024C,,,,,,,,,,,,,,,
022B,022D,032D,032C,,,,,,,,,,,,,,,
022A,022C,032B,032A,,,,,,,,,,,,,,,
021B,021D,,,,,,,,,,,,,,,,,
021A,021C,,,,,,,,,,,,,,,,,
020B,020D,,,,,,,,,,,,,,,,,
020A,020C,,,,,,,,,,,,,,,,,
019B,019D,,,,,,,,,,,,,,,,,
019A,019C,,,,,,,,,,,,,,,,,
018B,018D,,,,,,,,,,,,,,,,,
018A,018C,,,,,,,,,,,,,,,,,
017B,017D,,,,,,,,,,,,,,,,,
017A,017C,,,,,,,,,,,,,,,,,
016B,016D,,,,,,,,,,,,,,,,,
016A,016C,,,,,031A,031C,,,,,,,,,,,
015B,015D,,,,,030B,030D,,,,,,,,,,,
015A,015C,,,,,030A,030C,,,,,,,,,,,
014B,014D,,,,,029B,029D,,,,,,,,,,,
014A,014C,,,,,029A,029C,,,,,,,,,,,
013B,013D,,,,,028B,028D,,,,,,,,,,,
013A,013C,,,,,028A,028C,,,,,,,,,,,
012B,012D,,,,,027B,027D,,,,,,,,,,,
012A,012C,,,,,027A,027C,,,,,,,,,,,
011B,011D,,,,,026B,026D,,,,,,,,,,,
011A,011C,,,,,026A,026C,,,,,,,,,,,
010B,010D,,,,,025B,025D,,,,,,,,,,,
010A,010C,,,,,,,,,,,,,,,,,
009B,009D,,,,,,,,,,,,,,,,,
009A,009C,,,,,,,,,,,,,,,,,
008B,008D,,,,,,,,,,,,,,,,,
008A,008C,,,,,,,,,,,,,,,,,
007B,007D,,,,,,,,,,,,,,,,,
007A,007C,,,,,,,,,,,,,,,,,
006B,006D,,,,,,,,,,,,,,,,,
006A,006C,,,,,,,,,,,,,,,,,
005B,005D,,,,,,,,,,,,,,,,,
005A,005C,,,,,,,,,,,,,,,,,
004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,
004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,
"""
},
"3": {
"3": """
,,007B,007D,,,,,,,,008C,008A,,
,,007A,007C,,,,,,,,008D,008B,,
,,006B,006D,,,,,,,,009C,009A,,
,,006A,006C,,,,,,,,009D,009B,,
,,005B,005D,,,,,,,,010C,010a,,
,,005A,005C,,,,,,,,010D,010B,,
,,004B,004D,,,,,,,,011C,011A,,
,,004A,004C,,,,,,,,011D,011B,,
,,003B,003D,,,,,,,,012C,012A,,
,,003A,003C,,,,,,,,012D,012B,,
,,002B,002D,,,,,,,,013C,013A,,
,,002A,002C,,,,,,,,013D,013B,,
,,001B,001D,,,,,,,,014C,014A,,
,,001A,001C,,,,,,,,014D,014B,,
""",
"4": """
,,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,056D,056C,057D,057C,,
,,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,056B,056A,057B,057A,,
036B,036D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058C,058A,,060C,060A
036A,036C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058D,058B,,060D,060B
035B,035D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059C,059A,,061C,061A
035A,035C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059D,059B,,061D,061B
034B,034D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062C,062A
034A,034C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062D,062B
033B,033D,,,,,,,,,,,,080B,080D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,063C,063A
033A,033C,,,,,,,,,,,,080A,080C,,081A,081B,082A,082B,083A,083B,084A,084B,085A,085B,086A,086B,087A,,,,,,,,,,,,,,,,,,063D,063B
032B,032D,,,,,,,,,,,,079B,079D,,081C,081D,082C,082D,083C,083D,084C,084D,085C,085D,086C,086D,087C,,,,,,,,,,,,,,,,,,064C,064A
032A,032C,,,,,,,,,,,,079A,079C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,064D,064B
031B,031D,,,,,,,,,,,,078B,078D,,,,,,,,,,,,,,088A,088C,,,,,,,,,,,,,,,,,065C,065A
031A,031C,,,,,,,,,,,,078A,078C,,,,,,,,,,,,,,088B,088D,,,,,,,,,,,,,,,,,065D,065B
030B,030D,,,,,,,,,,,,077B,077D,,,,,,,,,,,,,,089A,089C,,,,,,,,,,,,,,,,,066C,066A
030A,030C,,,,,,,,,,,,077A,077C,,,,,,,,,,,,,,089B,089D,,,,,,,,,,,,,,,,,066D,066B
029B,029D,,,,,,,,,,,,076B,076D,,,,,,,,,,,,,,090A,090C,,,,,,,,,,,,,,,,,,
029A,029C,,,,,,,,,,,,076A,076C,,,,,,,,,,,,,,090B,090D,,,,,,,,,,,,,,,,,,
028B,028D,,,,,,,,,,,,075B,075D,,,,,,,,,,,,,,091A,091C,,,,,,,,,,,,,,,,,,
028A,028C,,,,,,,,,,,,075A,075C,,,,,,,,,,,,,,091B,091D,,,,,,,,,,,,,,,,,,
027B,027D,,,,,,,,,,,,074B,074D,,,,,,,,,,,,,,092A,092C,,,,,,,,,,,,,,,,,,
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,092B,092D,,,,,,,,,,,,,,,,,,
026B,026D,,,,,,,,,,,,,,,073D,073C,072D,072C,071D,071C,070D,070C,069D,069C,068D,068C,,,,,,,,,,,,,,,,,,,,
026A,026C,,,,,,,,,,,,,,,073B,073A,072B,072A,071B,071A,070B,070A,069B,069A,068B,068A,,,,,,,,,,,,,,,,,,,,
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
024B,024D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
024A,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
023B,023D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
023A,023C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067C,,
022B,022D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067B,,
022A,022C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067A,,
,,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,
,,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007a,006B,006A,005B,005A,004B,004A,003b,003A,002B,002A,001B,001A,,,,
"""
},
"4": {
"5": """
,,,,,,,,042A,042B,045A,045B,048A,048B,051A,051B,054A,054B,057A,057B,060A,060B,,,,,,
,,,,,,,,042C,042D,045C,045D,048C,048D,051C,051D,054C,054D,057C,057D,060C,060D,,,,,,
,,,,,,,,041A,041B,044A,044B,047A,047B,050A,050B,053A,053B,056A,056B,059A,059B,,,,,,
,,,,,,,,041C,041D,044C,044D,047C,047D,050C,050D,053C,053D,056C,056D,059C,059D,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,040A,040B,043A,043B,046A,046B,049A,049B,052A,052B,055A,055B,058A,058B,,,,,,
,,,,,,,,040C,040D,043C,043D,046C,046D,049C,049D,052C,052D,055C,055D,058C,058D,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,039B,039D,038B,038D,,037B,037D,,,,,,,,,,,,,,,,,,,,,
,039A,039C,038A,038C,,037A,037C,,,,,,,,,,,,,,,,,,,,,
,036B,036D,035B,035D,,034B,034D,,,,,,,,,,,,,,,,,,,,,
,036A,036C,035A,035C,,034A,034C,,,,,,,,,,,,,,,,,,,,,
,033B,033D,032B,032D,,031B,031D,,,,,,,,,,,,,,,,,,,,,
,033A,033C,032A,032C,,031A,031C,,,,,,,,,,,,,,,,,,,,,
,030B,030D,029B,029D,,028B,028D,,,,,,,,,,,,,,,,,,,,,
,030A,030C,029A,029C,,028A,028C,,,,,,,,,,,,,,,,,,,,,
,027B,027D,026B,026D,,025B,025D,,,,,,,,,,,,,,,,,,,,,
,027A,027C,026A,026C,,025A,025C,,,,,,,,,,,,,,,,,,,,,
,024B,024D,023B,023D,,022B,022D,,,,,,,,,,,,,,,,,,,,,
,024A,024C,023A,023C,,022A,022C,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,019D,019C,016D,016C,013D,013C,010D,010C,007D,007C,004D,004C,001D,001C,,,,,,
,,,,,,,,019B,019A,016B,016A,013B,013A,010B,010A,007B,007A,004B,004A,001B,001A,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,020D,020C,017D,017C,014D,014C,011D,011C,008D,008C,005D,005C,002D,002C,,,,,,
,,,,,,,,020B,020A,017B,017A,014B,014A,011B,011A,008B,008A,005B,005A,002B,002A,,,,,,
,,,,,,,,021D,021C,018D,018C,015D,015C,012D,012C,009D,009C,006D,006C,003D,003C,,,,,,
,,,,,,,,021B,021A,018B,018A,015B,015A,012B,012A,009B,009A,006B,006A,003B,003A,,,,,,
""",
"6": """
,,,026C,026D,027D,027C,028D,028C,029D,029C,030D,030C,031D,031C,032D,032C,033D,033C,035D,035C,036D,036C,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,046D,046C
,,,026A,026B,027B,027A,028B,028A,029B,029A,030B,030A,031B,031A,032B,032A,033B,033A,035B,035A,036B,036A,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,046B,046A
025D,025C,,,,,,,,,,,,,,,,034D,034C,,,,,,,,,,,,,,,,,,,,,,,047C,047A
025B,025A,,,,,,,,,,,,,,,,034B,034A,,,,,,,,,,,,,,,,,,,,,,,047D,047B
024D,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,048C,048A
024B,024A,,,,,,,,,,,,,,050D,050C,052D,052C,054D,054C,056D,056C,058D,058C,060D,060C,,,,,,,,,,,,,,,048D,048B
023D,023C,,,,,,,,,,,,,,050B,050A,052B,052A,054B,054A,056B,056A,058B,058A,060B,060A,,,,,,,,,,,,,,,,
023B,023A,,,,,,,,,,,,,,049D,049C,051D,051C,053D,053C,055D,055C,057D,057C,059D,059C,,,,,,,,,,,,,,,,
022D,022C,,,,,,,,,,,,,,049B,049A,051B,051A,053B,053A,055B,055A,057B,057A,059B,059A,,,,,,,,,,,,,,,,
022B,022A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
021D,021C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
021B,021A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
020D,020C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
020B,020A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
019D,019C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
019B,019A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
015D,015C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
015B,015A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
014D,014C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
014B,014A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
013D,013C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
013B,013A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
012D,012C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
012B,012A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
011D,011C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
011B,011A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
010D,010C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
010B,010A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
009D,009C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
009B,009A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
008D,008C,,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,,,,,,,,,,,,,,,,
008B,008A,,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,,,,,,,,,,,,,,,,
""",
"7": """
,,,,,,,,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,,,,,,,,,,,,
,,,,,,,,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
"""
},
"5": {
"8": """
,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,056C,056A,,,,,,,,,,,,,,,,,,,,,,,,,,
045B,045D,,,,,,,,,,,,,,,,,,,,,,056D,056B,,,,,,,,,,,,,,,,,,,,,,,,,,
045A,045C,,,,,,,,,,,,,,,,,,,,,,057C,057A,,,,,,,,,,,,,,,,,,,,,,,,,,
044B,044D,,,,,,,,,,,,,,,,,,,,,,057D,057B,,,,,,,,,,,,,,,,,,,,,,,,,,
044A,044C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
043B,043D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
043A,043C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
042B,042D,,,,,,,,,,,,,,,,,070B,070D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
042A,042C,,,,,,,,,,,,,,,,,070A,070C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
041B,041D,,,,,,,,,,,,,,,,,069B,069D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
041A,041C,,,,,,,,,,,,,,,,,069A,069C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
040B,040D,,,,,,,,,,,,,,,,,068B,068D,,071A,071B,072A,072B,073A,073B,074A,074B,075A,075B,076A,076B,077A,077B,,,,,,,,,,,,,,,,
040A,040C,,,,,,,,,,,,,,,,,068A,068C,,071C,071D,072C,072D,073C,073D,074C,074D,075C,075D,076C,076D,077C,077D,,,,,,,,,,,,,,,,
039B,039D,,,,,,,,,,,,,,,,,067B,067D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
039A,039C,,,,,,,,,,,,,,,,,067A,067C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
038B,038D,,,,,,,,,,,,,,,,,066B,066D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
038A,038C,,,,,,,,,,,,,,,,,066A,066C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
037B,037D,,,,,,,,,,,,,,,,,065B,065D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
037A,037C,,,,,,,,,,,,,,,,,065A,065C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
036B,036D,,,,,,,,,,,,,,,,,064B,064D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
036A,036C,,,,,,,,,,,,,,,,,064A,064C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
035B,035D,,,,,,,,,,,,,,,,,063B,063D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
035A,035C,,,,,,,,,,,,,,,,,063A,063C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
034B,034D,,,,,,,,,,,,,,,,,062B,062D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
034A,034C,,,,,,,,,,,,,,,,,062A,062C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
033B,033D,,,,,,,,,,,,,,,,,,,061D,061C,,060D,060C,,059D,059C,,058D,058C,,,,,,,,,,,,,,,,,,,,
033A,033C,,,,,,,,,,,,,,,,,,,061B,061A,,060B,060A,,059B,059A,,058B,058A,,,,,,,,,,,,,,,,,,,,
032B,032D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
032A,032C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
031B,031D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
031A,031C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
030B,030D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
030A,030C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
029B,029D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
029A,029C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
028B,028D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
028A,028C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
027B,027D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
026B,026D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
026A,026C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,024D,024C,023D,023C,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
,,,024B,024A,023B,023A,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
"""
}
}
+252
View File
@@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import (
Qt, Slot, Signal, QEvent
)
from PySide6.QtWidgets import (
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
QPushButton,
)
from PySide6.QtGui import (
QPainter, QWheelEvent, QCloseEvent
)
from gui.SeatFrame import SeatFrame
class SeatMapWidget(QWidget):
seatMapWidgetClosed = Signal(list)
def __init__(
self,
parent: QWidget = None,
floor: str = "",
room: str = "",
seats_data: dict = {},
):
super().__init__(parent)
self.__floor = floor
self.__room = room
self.__seats_data = seats_data
self.__selected_seats = []
self.__seat_frames = {}
self.setUpUi()
self.connectSignals()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
def setUpUi(
self
):
self.setWindowFlags(Qt.WindowType.Window)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(800, 600)
self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
self.SeatMapGraphicsView = QGraphicsView(self)
self.SeatMapGraphicsScene = QGraphicsScene(self)
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.viewport().installEventFilter(self)
self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.createSeatMap()
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
)
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.SeatMapWidgetControlLayout = QHBoxLayout()
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals(
self
):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
self.seatMapWidgetClosed.emit(self.__selected_seats)
super().closeEvent(event)
def eventFilter(
self,
watched,
event
):
if (watched is self.SeatMapGraphicsView.viewport() and
event.type() == QEvent.Type.Wheel and
event.modifiers() == Qt.KeyboardModifier.ControlModifier
):
self.zoomGraphicsView(event)
return True
return super().eventFilter(watched, event)
def zoomGraphicsView(
self,
event: QWheelEvent
):
delta = event.angleDelta().y()
zoom_factor = 1.2 if delta > 0 else 1/1.2
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
def createSeatMap(
self
):
rows = self.__seats_data.strip().split("\n")
for row_idx, row in enumerate(rows):
col_idx = 0
seats_number = [seat.strip() for seat in row.split(",")]
for seat_number in seats_number:
if seat_number:
seat_widget = SeatFrame(seat_number)
seat_widget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
self.__seat_frames[seat_number] = seat_widget
else:
spacer = QFrame()
spacer.setFixedSize(20, 30)
spacer.setStyleSheet("background-color: transparent; border: none;")
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
col_idx += 1
self.SeatsContainerLayout.setSpacing(20)
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
self.SeatsContainerWidget.adjustSize()
def selectSeat(
self,
seat_number: str
):
if len(self.__selected_seats) >= 1:
return
seat_number = self.formatSeatNumber(seat_number)
if seat_number not in self.__seat_frames:
return
widget = self.__seat_frames[seat_number]
if widget.isSelected():
return
widget.toggleSelection()
self.__selected_seats.append(seat_number)
def selectSeats(
self,
selected_seats: list
):
self.clearSelections()
for seat_number in selected_seats:
self.selectSeat(seat_number)
def getSelectedSeats(
self
) -> list[str]:
return self.__selected_seats
def clearSelections(
self
):
seats_to_clear = self.__selected_seats.copy()
for seat_number in seats_to_clear:
if seat_number not in self.__seat_frames:
continue
widget = self.__seat_frames[seat_number]
if widget.isSelected():
widget.toggleSelection()
self.__selected_seats = []
@Slot(str)
def onSeatClicked(
self,
seat_number: str
):
if seat_number in self.__selected_seats:
self.__selected_seats.remove(seat_number)
else:
if len(self.__selected_seats) < 1:
self.__selected_seats.append(seat_number)
else:
self.__seat_frames[seat_number].toggleSelection()
@Slot()
def onConfirmButtonClicked(
self
):
self.close()
@Slot()
def onCancelButtonClicked(
self
):
self.clearSelections()
self.close()

Before

Width:  |  Height:  |  Size: 785 KiB

After

Width:  |  Height:  |  Size: 785 KiB

+15 -14
View File
@@ -16,14 +16,15 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.edge.service import Service
from MsgBase import MsgBase
from LibChecker import LibChecker
from LibLogin import LibLogin
from LibLogout import LibLogout
from LibReserve import LibReserve
from LibCheckin import LibCheckin
from base.MsgBase import MsgBase
from operators.LibChecker import LibChecker
from operators.LibLogin import LibLogin
from operators.LibLogout import LibLogout
from operators.LibReserve import LibReserve
from operators.LibCheckin import LibCheckin
from operators.LibRenew import LibRenew
from ConfigReader import ConfigReader
from utils.ConfigReader import ConfigReader
class AutoLib(MsgBase):
@@ -114,6 +115,7 @@ class AutoLib(MsgBase):
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
def __waitResponseLoad(
@@ -186,30 +188,29 @@ class AutoLib(MsgBase):
if run_mode["auto_reserve"]:
if self.__lib_checker.canReserve(reserve_info.get("date")):
if self.__lib_reserve.reserve(reserve_info):
self._showTrace(f"用户 {username} 预约成功 !")
result = 0
else:
self._showTrace(f"用户 {username} 预约失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法预约,已跳过")
result = 2
# checkin
if run_mode["auto_checkin"] and result == 2:
if self.__lib_checker.canCheckin(reserve_info.get("date")):
if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username):
self._showTrace(f"用户 {username} 签到成功 !")
result = 0
else:
self._showTrace(f"用户 {username} 签到失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法签到,已跳过")
result = 2
# renewal
if run_mode["auto_renewal"] and result == 2:
if self.__lib_checker.canRenew(reserve_info.get("date")):
pass
if record := self.__lib_checker.canRenew():
if self.__lib_renew.renew(username, record, reserve_info):
result = 0
else:
result = 1
else:
self._showTrace(f"用户 {username} 无法续约,已跳过")
result = 2
+133 -102
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibChecker(LibOperator):
@@ -25,7 +25,7 @@ class LibChecker(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -44,9 +44,9 @@ class LibChecker(LibOperator):
seconds: float
) -> str:
hours = int(seconds // 3600)
minutes = int(seconds % 3600 // 60)
seconds = int(seconds % 60)
hours = int(seconds//3600)
minutes = int(seconds%3600//60)
seconds = int(seconds%60)
return f"{hours}{minutes}{seconds}"
@@ -67,6 +67,44 @@ class LibChecker(LibOperator):
return True
def __decodeReserveTime(
self,
time_element
) -> dict:
time_str = time_element.text.strip()
today = datetime.now().date()
if "明天" in time_str:
target_date = today + timedelta(days=1)
date = target_date.strftime("%Y-%m-%d")
elif "今天" in time_str:
target_date = today
date = target_date.strftime("%Y-%m-%d")
elif "昨天" in time_str:
target_date = today - timedelta(days=1)
date = target_date.strftime("%Y-%m-%d")
else:
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
if date_match:
date = date_match.group(1)
else:
date = ""
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
if time_match:
begin_time = time_match.group(1)
end_time = time_match.group(2)
else:
begin_time = ""
end_time = ""
return {
"date": date,
"time": {
"begin": begin_time,
"end": end_time
}
}
def __decodeReserveInfo(
self,
info_elements
@@ -108,42 +146,63 @@ class LibChecker(LibOperator):
By.CSS_SELECTOR, "a"
)
except:
return None
# process time element to get the date string
time_str = time_element.text.strip()
today = datetime.now().date()
if "明天" in time_str:
target_date = today + timedelta(days=1)
date_str = target_date.strftime("%Y-%m-%d")
elif "今天" in time_str:
target_date = today
date_str = target_date.strftime("%Y-%m-%d")
elif "昨天" in time_str:
target_date = today - timedelta(days=1)
date_str = target_date.strftime("%Y-%m-%d")
else:
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
if date_match:
date_str = date_match.group(1)
else:
date_str = ""
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
if time_match:
begin_time = time_match.group(1)
end_time = time_match.group(2)
else:
time_str = ""
return {
"date": "",
"time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""}
}
time = self.__decodeReserveTime(time_element)
info = self.__decodeReserveInfo(info_elements)
return {
"date": date_str,
"time": {
"begin": begin_time,
"end": end_time,
},
"date": time["date"],
"time": time["time"],
"info": info
}
def __loadReserveRecords(
self
) -> list:
try:
# check if there's any reservation on the date
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
)
reservations = self.__driver.find_elements(
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
)
return reservations
except:
self._showTrace("加载预约记录失败 !")
return None
def __showMoreReserveRecords(
self
) -> bool:
# load new reservations if still not sure
try:
WebDriverWait(self.__driver, 0.1).until(
EC.element_to_be_clickable((By.ID, "moreBtn"))
)
except:
# the reservation is the last one
return False
try:
more_btn = self.__driver.find_element(By.ID, "moreBtn")
if more_btn.is_displayed() and more_btn.is_enabled():
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
self.__driver.execute_script("arguments[0].click();", more_btn)
return True
else:
self._showTrace("用户无法加载更多预约记录")
return False
except:
self._showTrace("加载更多预约记录失败 !")
return False
def __getReserveRecord(
self,
wanted_date: str,
@@ -154,7 +213,6 @@ class LibChecker(LibOperator):
self._showTrace("日期未指定, 无法检查当前预约状态")
return None
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
date_obj = datetime.strptime(wanted_date, "%Y-%m-%d").date()
checked_count = 0
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
@@ -162,59 +220,34 @@ class LibChecker(LibOperator):
if not self.__navigateToReserveRecordPage():
return None
for _ in range(max_check_times):
try:
# check if there's any reservation on the date
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
)
reservations = self.__driver.find_elements(
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
)
except:
self._showTrace("加载预约记录失败 !")
reservations = self.__loadReserveRecords()
if reservations is None:
return None
for i in range(checked_count, len(reservations)): # the last one is load button
reservation = reservations[i]
for reservation in reservations[checked_count:]:
record = self.__decodeReserveRecord(reservation)
checked_count += 1
if record is None:
continue
record_date = record["date"]
record_time = record["time"]
status = record["info"]["status"]
location = record["info"]["location"]
if record_date == "" or record_time == {"begin": "", "end": ""}:
if record["date"] == "":
continue
is_wanted = (status == wanted_status)
# reservation is later than the given date, check the next one
if datetime.strptime(record_date, "%Y-%m-%d").date() > date_obj:
if record["time"] == {"begin": "", "end": ""}:
continue
# reservation is earlier than the given date, can reserve
if datetime.strptime(record_date, "%Y-%m-%d").date() < date_obj:
# record date is later than the given date, check the next one
if datetime.strptime(record["date"], "%Y-%m-%d").date() >\
datetime.strptime(wanted_date, "%Y-%m-%d").date():
continue
# record date is earlier than the given date, so there is no wanted record
if datetime.strptime(record["date"], "%Y-%m-%d").date() <\
datetime.strptime(wanted_date, "%Y-%m-%d").date():
return None
# query the wanted status
if is_wanted:
if record["info"]["status"] == wanted_status:
self._showTrace(
f"寻找到用户第 {i + 1} 条状态为 {wanted_status} 的预约记录, "
f"详细信息: {record_date} {record_time['begin']} - {record_time['end']} {location}"
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
f"详细信息: {record["date"]} "
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
)
return {
"index": i,
"date": record_date,
"time": record_time,
"status": wanted_status
}
checked_count = len(reservations)
# load new reservations if still not sure
try:
more_btn = self.__driver.find_element(By.ID, "moreBtn")
if more_btn.is_displayed() and more_btn.is_enabled():
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
self.__driver.execute_script("arguments[0].click();", more_btn)
else:
self._showTrace("用户无法加载更多预约记录")
break
except:
self._showTrace("加载更多预约记录失败 !")
return record
if not self.__showMoreReserveRecords():
break
return None
@@ -237,11 +270,11 @@ class LibChecker(LibOperator):
def canCheckin(
self,
date: str
self
) -> bool:
# have a reserved record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "已预约")
if record is not None:
begin_time = record["time"]["begin"]
@@ -252,21 +285,21 @@ class LibChecker(LibOperator):
if time_diff_seconds < -30*60:
self._showTrace(
f"用户在 {date} 的预约开始时间为 {begin_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
)
return False
# before in 30 minutes, can checkin
elif -30*60 <= time_diff_seconds < 0:
self._showTrace(
f"用户在 {date} 的预约开始时间为 {begin_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
)
return True
# past less than 30 minutes, can checkin
elif 0 <= time_diff_seconds < 30*60:
elif 0 <= time_diff_seconds < 30*60 - 5: # spare 5 seconds for the checkin process
self._showTrace(
f"用户在 {date} 的预约开始时间为 {begin_time}, "
f"当前时间已经 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
f"当前距离预约开始时间已经过去 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
)
return True
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
@@ -274,11 +307,11 @@ class LibChecker(LibOperator):
def canRenew(
self,
date: str
) -> bool:
self
):
# have a using record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "使用中")
if record is not None:
end_time = record["time"]["end"]
@@ -286,17 +319,15 @@ class LibChecker(LibOperator):
time_diff = end_time - datetime.now()
time_diff_seconds = time_diff.total_seconds()
# a using record is definitely after the begin time
trace_msg = (
f"用户在 {date} 的预约结束时间为 {end_time}, "
f"当前距离预约结束时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}"
)
if abs(time_diff_seconds) < 120*60:
self._showTrace(
f"用户在 {date} 的预约结束时间为 {end_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以续约"
)
return True
self._showTrace(f"{trace_msg}, 可以续约")
return record
else:
self._showTrace(
f"用户在 {date} 的预约结束时间为 {end_time}, "
f"距离当前时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法续约"
)
return False
self._showTrace(f"{trace_msg}, 无法续约")
return None
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return False
return None
+12 -6
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibCheckin(LibOperator):
@@ -25,7 +25,7 @@ class LibCheckin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -54,7 +54,6 @@ class LibCheckin(LibOperator):
except:
self._showTrace("签到时发生未知错误 !")
return False
print(result_message_element)
result_message = result_message_element.text
if "签到成功" in result_message:
try:
@@ -73,14 +72,16 @@ class LibCheckin(LibOperator):
f" {details[3]}\n"\
f" {details[4]}")
else:
self._showTrace(
self._showTrace(f"\n"\
" 签到成功 !\n"\
" 未获取到签到详情 !")
ok_btn.click()
return True
else:
failure_reason = result_message.replace("签到失败", "").strip()
self._showTrace(f"签到失败: {failure_reason}")
self._showTrace(f"\n"\
" 签到失败 !\n"\
f" {failure_reason}")
ok_btn.click()
return False
@@ -104,4 +105,9 @@ class LibCheckin(LibOperator):
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
checkin_btn.click()
return self._waitResponseLoad()
if self._waitResponseLoad():
self._showTrace(f"用户 {username} 签到成功 !")
return True
else:
self._showTrace(f"用户 {username} 签到失败 !")
return False
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibCheckout(LibOperator):
@@ -25,7 +25,7 @@ class LibCheckout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+2 -2
View File
@@ -17,7 +17,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibLogin(LibOperator):
@@ -26,7 +26,7 @@ class LibLogin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+2 -2
View File
@@ -13,7 +13,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibLogout(LibOperator):
@@ -22,7 +22,7 @@ class LibLogout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+223
View File
@@ -0,0 +1,223 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import time
import queue
from datetime import datetime, timedelta
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator
class LibRenew(LibOperator):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver: any
):
super().__init__(input_queue, output_queue)
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
try:
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog"))
)
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CLASS_NAME, "resultMessage"))
)
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.CLASS_NAME, "btnOK"))
)
result_message_element = self.__driver.find_element(
By.CLASS_NAME, "resultMessage"
)
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
except:
self._showTrace("续约时发生未知错误 !")
return False
result_message = result_message_element.text
if "续约成功" in result_message:
try:
detail_elements = self.__driver.find_elements(
By.CSS_SELECTOR, ".resultMessage dd"
)
except:
pass
if detail_elements:
details = [element.text for element in detail_elements if element.text.strip()]
if len(details) >= 5:
self._showTrace(f"\n"\
f" 续约成功 !\n"\
f" {details[1]}\n"\
f" {details[2]}\n"\
f" {details[3]}\n"\
f" {details[4]}")
else:
self._showTrace(f"\n"\
" 续约成功 !\n"\
" 未获取到续约详情 !")
ok_btn.click()
return True
else:
failure_reason = result_message.replace("续约失败", "").strip()
self._showTrace(f"\n"\
" 续约失败 !\n"\
f" {failure_reason}"
)
ok_btn.click()
return False
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __selectNearstRecord(
self,
record: dict,
reserve_info: dict
) -> bool:
end_time = record["time"]["end"]
renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"]
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60
try:
WebDriverWait(self.__driver, 2).until(
EC.visibility_of_element_located((By.ID, "extendDiv"))
)
WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
)
)
renew_ok_btn = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
try:
renew_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
)
free_times = []
best_time_diff = max_diff
best_actual_diff = None
best_time_opt = None
if not renew_time_opts:
self._showTrace("当前未查询到可用续约时间 !")
return False
for time_opt in renew_time_opts:
time_attr = time_opt.get_attribute("id")
if time_attr and time_attr.isdigit():
time_val = int(time_attr)
free_times.append(time_opt.text.strip())
else:
continue
actual_diff = time_val - target_renew_mins
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# 优先选择更早的时间
(prefer_earlier and actual_diff <= 0) or
# 优先选择更晚的时间
(not prefer_earlier and actual_diff >= 0)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于续约时间"
self._showTrace(
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
f"与期望续约时间相比 {time_relation}"
)
renew_ok_btn.click()
return True
self._showTrace(
"无法选择最近的可用续约时间 !" \
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
)
self._showTrace(
f"当前可供续约的时间有: {free_times}"
)
return False
except:
self._showTrace("查询可用续约时间时发生未知错误 !")
return False
def renew(
self,
username: str,
record: dict,
reserve_info: dict
) -> bool:
if self.__driver is None:
self._showTrace("未提供有效 WebDriver 实例 !")
return False
try:
renew_btn = WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "btnExtend"))
)
except:
self._showTrace(f"用户 {username} 续约界面加载失败 !")
return False
if "disabled" in renew_btn.get_attribute("class"):
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内")
return False
renew_btn.click()
if not self.__selectNearstRecord(record, reserve_info):
return False
# renew_ok_btn.click()
if self._waitResponseLoad():
self._showTrace(f"用户 {username} 续约成功 !")
return True
else:
self._showTrace(f"用户 {username} 续约失败 !")
return False
+100 -29
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibReserve(LibOperator):
@@ -25,7 +25,7 @@ class LibReserve(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -187,12 +187,13 @@ class LibReserve(LibOperator):
reserve_info: dict
) -> bool:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
if reserve_info.get("satisfy_duration") is None:
reserve_info["satisfy_duration"] = True
self._showTrace("预约满足时长要求未指定, 默认满足")
if reserve_info["satisfy_duration"]:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
return True
@@ -234,7 +235,7 @@ class LibReserve(LibOperator):
# if end time is earlier than begin_time, exchange them
if end_mins < begin_mins:
self._showTrace(
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 自动交换"
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
)
reserve_info["end_time"] = begin_time
reserve_info["begin_time"] = end_time
@@ -261,10 +262,9 @@ class LibReserve(LibOperator):
if end_mins - begin_mins > 8*60:
self._showTrace(
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
f"{int((end_mins - begin_mins)/60)} 小时 "
f"{float((end_mins - begin_mins)/60)} 小时 "
f"超出最大时长 8 小时, 自动设置为 8 小时"
)
reserve_info["expect_duration"] = 8
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60)
return True
@@ -324,11 +324,52 @@ class LibReserve(LibOperator):
return False
def __clickElementByJS(
self,
trigger_locator_id: str,
option_query_selector: str,
fail_msg: str,
success_msg: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_locator_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_query_selector}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self.__driver.execute_script(script)
time.sleep(0.1)
if result:
self._showTrace(success_msg)
else:
self._showTrace(fail_msg)
return result
def __selectDate(
self,
date_str: str
) -> bool:
if self.__clickElementByJS(
trigger_locator_id="onDate_select",
option_query_selector=f"p#options_onDate a[value='{date_str}']",
success_msg=f"日期 {date_str} 选择成功 !",
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "onDate_select"),
option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
@@ -342,12 +383,20 @@ class LibReserve(LibOperator):
place: str
) -> bool:
actual_place = "1" if place == "图书馆" else "1"
place = "1" # the library only have this place :)
display_place = "图书馆"
if self.__clickElementByJS(
trigger_locator_id="display_building",
option_query_selector=f"p#options_building a[value='{place}']",
success_msg=f"预约场所 {display_place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "display_building"),
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{actual_place}']"),
success_msg=f"预约场所 {place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {place} 不可用"
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{place}']"),
success_msg=f"预约场所 {display_place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
)
@@ -357,6 +406,13 @@ class LibReserve(LibOperator):
) -> bool:
display_floor = self.__floor_map.get(floor)
if self.__clickElementByJS(
trigger_locator_id="floor_select",
option_query_selector=f"p#options_floor a[value='{floor}']",
success_msg=f"楼层 {display_floor} 选择成功 !",
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "floor_select"),
option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
@@ -371,12 +427,24 @@ class LibReserve(LibOperator):
) -> bool:
display_room = self.__room_map.get(room)
return self.__clickElement(
trigger_locator=(By.ID, f"room_{room}"),
option_locator=None,
success_msg=f"房间 {display_room} 选择成功 !",
fail_msg=f"选择房间失败 ! : {display_room} 不可用"
)
# find room
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
self._showTrace("加载房间/区域失败 !")
return False
# select room
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, f"room_{room}"))
).click()
self._showTrace(f"房间 {display_room} 选择成功 !")
return True
except:
self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
return False
def __selectSeat(
@@ -509,6 +577,7 @@ class LibReserve(LibOperator):
expect_begin_time = actual_begin_time = begin_time["time"]
expect_end_time = actual_end_time = end_time["time"]
expect_begin_mins = self.__timeToMins(expect_begin_time)
actual_begin_mins = expect_begin_mins
expect_end_mins = self.__timeToMins(expect_end_time)
# select the begin time
@@ -522,11 +591,18 @@ class LibReserve(LibOperator):
return False
else:
actual_begin_time = self.__minsToTime(expect_begin_mins)
actual_begin_mins = self.__timeToMins(actual_begin_time)
# if 'satisfy_duration' is True.
# select the end time based on the begin time
# (because it may be changed under the 'max time diff' strategy) and expect duration.
if satisfy_duration:
expect_end_mins = int(expect_begin_mins + expct_duration*60)
expect_end_mins = int(actual_begin_mins + expct_duration*60)
if expect_end_mins > self.__timeToMins("23:30"):
expect_end_mins = self.__timeToMins("23:30")
self._showTrace(
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
expect_end_time = self.__minsToTime(expect_end_mins)
self._showTrace(
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
@@ -572,22 +648,13 @@ class LibReserve(LibOperator):
except:
self._showTrace(f"加载预约选座页面失败 !")
return False
# date, place, floor
# date, place, floor, room
if not self.__selectDate(reserve_info["date"]):
return False
if not self.__selectPlace(reserve_info["place"]):
return False
if not self.__selectFloor(reserve_info["floor"]):
return False
# room find
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
self._showTrace("加载房间/区域失败 !")
return False
# room
if not self.__selectRoom(reserve_info["room"]):
return False
else:
@@ -615,4 +682,8 @@ class LibReserve(LibOperator):
self._showTrace(f"预约提交失败 !")
if not submit_reserve and have_hover_on_page:
self.__driver.refresh()
if reserve_success:
self._showTrace(f"用户 {reserve_info['username']} 预约成功 !")
else:
self._showTrace(f"用户 {reserve_info['username']} 预约失败 !")
return reserve_success
+12
View File
@@ -0,0 +1,12 @@
"""
Operators module for the AutoLibrary project.
Here are the classes and modules in this package:
- AutoLib: AutoLibrary operator.
- LibLogin: Library operator for logging in.
- LibLogout: Library operator for logging out.
- LibReserve: Library operator for reserving seat.
- LibCheckin: Library operator for checking in seat.
- LibCheckout: Library operator for checking out seat.
- LibRenew: Library operator for renewing seat.
"""
+7
View File
@@ -0,0 +1,7 @@
"""
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigReader: Configuration reader class for the AutoLibrary project.
- ConfigWriter: Configuration writer class for the AutoLibrary project.
"""