From 7b4b2ae86c08f43ab7a5f5d121d887a138ae76df Mon Sep 17 00:00:00 2001
From: KenanZhu <3471685733@qq.com>
Date: Tue, 4 Nov 2025 00:14:45 +0800
Subject: [PATCH] chore(*): first commit
---
.gitignore | 14 +
AutoLib.py | 232 +
ConfigReader.py | 89 +
ConfigWriter.py | 87 +
LibCheckin.py | 40 +
LibCheckout.py | 40 +
LibLogin.py | 181 +
LibLogout.py | 62 +
LibOperator.py | 30 +
LibRenew.py | 34 +
LibReserve.py | 652 +++
Main.py | 33 +
MsgBase.py | 65 +
Pipfile | 15 +
Pipfile.lock | 630 +++
document/manual.html | 847 ++++
driver/readme.md | 1 +
gui/ALConfigWidget.py | 800 ++++
gui/ALConfigWidget.ui | 1790 +++++++
gui/ALMainWindow.py | 310 ++
gui/ALMainWindow.ui | 264 ++
gui/AutoLibraryResource.qrc | 8 +
gui/icons/AutoLibrary.ico | Bin 0 -> 803706 bytes
gui/translators/qtbase_zh_CN.ts | 7898 +++++++++++++++++++++++++++++++
models/readme.md | 1 +
readme,md | 1 +
26 files changed, 14124 insertions(+)
create mode 100644 .gitignore
create mode 100644 AutoLib.py
create mode 100644 ConfigReader.py
create mode 100644 ConfigWriter.py
create mode 100644 LibCheckin.py
create mode 100644 LibCheckout.py
create mode 100644 LibLogin.py
create mode 100644 LibLogout.py
create mode 100644 LibOperator.py
create mode 100644 LibRenew.py
create mode 100644 LibReserve.py
create mode 100644 Main.py
create mode 100644 MsgBase.py
create mode 100644 Pipfile
create mode 100644 Pipfile.lock
create mode 100644 document/manual.html
create mode 100644 driver/readme.md
create mode 100644 gui/ALConfigWidget.py
create mode 100644 gui/ALConfigWidget.ui
create mode 100644 gui/ALMainWindow.py
create mode 100644 gui/ALMainWindow.ui
create mode 100644 gui/AutoLibraryResource.qrc
create mode 100644 gui/icons/AutoLibrary.ico
create mode 100644 gui/translators/qtbase_zh_CN.ts
create mode 100644 models/readme.md
create mode 100644 readme,md
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ccfe4db
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+**/build
+**/dist
+**/models/*.onnx
+**/driver/*.exe
+**/.git
+**/.vscode
+**/.stfolder
+**/.stignore
+**/gui/AutoLibraryResources.py
+**/__pycache__
+**/gui/AutoLibraryResource.py
+**/gui/Ui_ALMainWindow.py
+**/gui/Ui_ALConfigWidget.py
+**/gui/translators/qtbase_zh_CN.qm
diff --git a/AutoLib.py b/AutoLib.py
new file mode 100644
index 0000000..e103583
--- /dev/null
+++ b/AutoLib.py
@@ -0,0 +1,232 @@
+# -*- 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 selenium import webdriver
+from selenium.webdriver.common.by import By
+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 LibLogin import LibLogin
+from LibLogout import LibLogout
+from LibReserve import LibReserve
+
+from ConfigReader import ConfigReader
+
+
+class AutoLib(MsgBase):
+
+ def __init__(
+ self,
+ input_queue: queue.Queue,
+ output_queue: queue.Queue,
+ ):
+ super().__init__(input_queue, output_queue)
+
+ self.__system_config_reader = None
+ self.__users_config_reader = None
+ self.__driver = None
+
+
+ def __initBrowserDriver(
+ self
+ ) -> bool:
+
+ self._showTrace("正在初始化浏览器驱动......")
+ edge_options = webdriver.EdgeOptions()
+
+ if self.__system_config_reader.get("web_driver/headless"):
+ edge_options.add_argument("--headless")
+ edge_options.add_argument("--disable-gpu")
+ edge_options.add_argument("--no-sandbox")
+ edge_options.add_argument("--disable-dev-shm-usage")
+
+ edge_options.add_argument("--window-size=1280,720")
+ edge_options.add_argument("--remote-allow-origins=*")
+
+ # omit ssl errors and verbose log level
+ edge_options.add_argument("--ignore-certificate-errors")
+ edge_options.add_argument("--ignore-ssl-errors")
+ edge_options.add_argument("--log-level=OFF")
+ edge_options.add_argument("--silent")
+
+ edge_options.add_experimental_option("excludeSwitches", ["enable-automation"])
+ edge_options.add_experimental_option("useAutomationExtension", False)
+ edge_options.add_argument("--disable-blink-features=AutomationControlled")
+ edge_options.add_argument(
+ "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
+ "AppleWebKit/537.36 (KHTML, like Gecko) "\
+ "Chrome/120.0.0.0 "\
+ "Safari/537.36 "\
+ "Edg/120.0.0.0"
+ )
+
+ # init browser driver
+ self.__driver_path = self.__system_config_reader.get("web_driver/driver_path")
+ self.__driver_type = self.__system_config_reader.get("web_driver/driver_type")
+ self.__driver_path = os.path.abspath(self.__driver_path)
+ try:
+ service = None
+ if self.__driver_path:
+ service = Service(executable_path=self.__driver_path)
+ match self.__driver_type.lower():
+ case "edge":
+ self.__driver = webdriver.Edge(service=service, options=edge_options)
+ case "chrome":
+ self.__driver = webdriver.Chrome(service=service, options=edge_options)
+ case "firefox":
+ self.__driver = webdriver.Firefox(service=service, options=edge_options)
+ case _:
+ raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
+ self.__driver.implicitly_wait(10)
+ self.__driver.execute_script(
+ "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
+ )
+ except Exception as e:
+ self._showTrace(f"浏览器驱动初始化失败: {e}")
+ return False
+ # init library operators
+ self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
+ 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._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
+ return True
+
+
+ def __waitResponseLoad(
+ self,
+ ) -> bool:
+
+ # wait for page load
+ try:
+ WebDriverWait(self.__driver, 5).until( # title contains "首页"
+ EC.title_contains("首页")
+ )
+ WebDriverWait(self.__driver, 2).until( # username field presence
+ EC.presence_of_element_located((By.NAME, "username"))
+ )
+ WebDriverWait(self.__driver, 2).until( # password field presence
+ EC.presence_of_element_located((By.NAME, "password"))
+ )
+ WebDriverWait(self.__driver, 2).until( # captcha field presence
+ EC.presence_of_element_located((By.NAME, "answer"))
+ )
+ WebDriverWait(self.__driver, 2).until( # captcha image presence
+ EC.presence_of_element_located((By.ID, "loadImgId"))
+ )
+ return True
+ except:
+ self._showTrace(f"登录页面加载失败 !")
+ return False
+
+
+ def __initDriverUrl(
+ self,
+ ) -> bool:
+
+ self.__driver.get(self.__system_config_reader.get("library/host_url"))
+ if not self.__waitResponseLoad():
+ return False
+ return True
+
+
+ def __run(
+ self,
+ username: str,
+ password: str,
+ reserve_info: dict,
+ ) -> bool:
+
+ success = False
+
+ # login
+ if not self.__lib_login.login(
+ username,
+ password,
+ self.__system_config_reader.get("login/max_attempt", 5),
+ self.__system_config_reader.get("login/auto_captcha", True),
+ ):
+ return False
+ run_mode = self.__system_config_reader.get("run/mode", 1)
+ run_mode = {
+ "auto_reserve": run_mode&0x1,
+ "auto_checkin": run_mode&0x2,
+ "auto_renewal": run_mode&0x4,
+ }
+ # reserve or checkin or renewal
+ """
+ Here, we collect the run mode from the config file.
+ """
+ if self.__lib_reserve.canReserve(reserve_info.get("date")) and run_mode["auto_reserve"]:
+ if self.__lib_reserve.reserve(reserve_info):
+ self._showTrace(f"用户 {username} 预约成功 !")
+ success = True
+ else:
+ self._showTrace(f"用户 {username} 预约失败 !")
+ success = False
+ # logout
+ if not self.__lib_logout.logout(
+ username,
+ ):
+ self.__driver.get(self.__system_config_reader.get("library/host_url"))
+ return False
+ return success
+
+
+ def run(
+ self,
+ system_config_reader: ConfigReader,
+ users_config_reader: ConfigReader,
+ ):
+
+ self.__system_config_reader = system_config_reader
+ self.__users_config_reader = users_config_reader
+ if not self.__initBrowserDriver():
+ return
+ else:
+ if not self.__initDriverUrl():
+ return
+
+ user_counter = {"current": 0, "success": 0, "failed": 0}
+ users = self.__users_config_reader.get("users")
+ self._showTrace(f"共发现 {len(users)} 个用户, "\
+ f"用户配置文件路径: {self.__users_config_reader.configPath()}")
+
+ for user in users:
+ self._showTrace(f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......")
+ if self.__run(
+ username=user["username"],
+ password=user["password"],
+ reserve_info=user["reserve_info"],
+ ):
+ user_counter["success"] += 1
+ else:
+ user_counter["failed"] += 1
+ self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\
+ f"成功 {user_counter["success"]} 个用户, "\
+ f"失败 {user_counter["failed"]} 个用户")
+ return
+
+
+ def close(
+ self,
+ ) -> bool:
+
+ if self.__driver:
+ self.__driver.quit()
+ self.__driver = None
+ self._showTrace(f"浏览器驱动已关闭")
+ return True
+ else:
+ self._showTrace(f"浏览器驱动未初始化,无需关闭")
+ return False
\ No newline at end of file
diff --git a/ConfigReader.py b/ConfigReader.py
new file mode 100644
index 0000000..194a1a5
--- /dev/null
+++ b/ConfigReader.py
@@ -0,0 +1,89 @@
+# -*- 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 json
+
+
+class ConfigReader:
+
+ def __init__(
+ self,
+ config_path: str
+ ):
+
+ self._config_path = config_path
+ self._config_data = {}
+ if not self.__readConfig():
+ return None
+
+
+ def __readConfig(
+ self
+ ) -> bool:
+
+ try:
+ with open(self._config_path, 'r', encoding='utf-8') as file:
+ self._config_data = json.load(file)
+ return True
+ except Exception as e:
+ print(f"Error reading config file: {e}")
+ return False
+
+
+ def getConfigs(
+ self
+ ) -> dict:
+
+ return self._config_data.copy()
+
+
+ def getConfig(
+ self,
+ key: str
+ ) -> dict:
+
+ return self._config_data.get(key, {})
+
+
+ def get(
+ self,
+ key: str,
+ default: any = None
+ ) -> any:
+
+ keys = key.split('/')
+ current = self._config_data
+ for k in keys:
+ if isinstance(current, dict) and k in current:
+ current = current[k]
+ else:
+ return default
+ return current
+
+
+ def hasConfig(
+ self,
+ key: str
+ ) -> bool:
+
+ return self.getConfig(key) != {}
+
+
+ def reReadConfig(
+ self
+ ) -> bool:
+
+ return self.__readConfig()
+
+
+ def configPath(
+ self
+ ) -> str:
+
+ return self._config_path
diff --git a/ConfigWriter.py b/ConfigWriter.py
new file mode 100644
index 0000000..bbddb48
--- /dev/null
+++ b/ConfigWriter.py
@@ -0,0 +1,87 @@
+# -*- 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 json
+
+
+class ConfigWriter:
+
+ def __init__(
+ self,
+ config_path: str,
+ config_data: dict
+ ):
+
+ self.__config_path = config_path
+ self.__config_data = config_data if config_data is not None else {}
+ if config_data is None:
+ return None
+ if not self.__writeConfig():
+ return None
+
+
+ def __writeConfig(
+ self
+ ) -> bool:
+
+ try:
+ with open(self.__config_path, "w") as f:
+ json.dump(self.__config_data, f, indent=4, sort_keys=False)
+ return True
+ except:
+ return False
+
+
+ def setConfigs(
+ self,
+ configs: dict
+ ) -> bool:
+
+ self.__config_data = configs
+ return self.__writeConfig()
+
+
+ def setConfig(
+ self,
+ key: str,
+ value: dict
+ ) -> bool:
+
+ self.__config_data[key] = value
+ return self.__writeConfig()
+
+
+ def set(
+ self,
+ key: str,
+ value: dict
+ ) -> bool:
+
+ keys = key.replace("\\", "/").split("/")
+ current = self.__config_data
+ for k in keys[:-1]:
+ if k not in current or not isinstance(current[k], dict):
+ current[k] = {}
+ current = current[k]
+ current[keys[-1]] = value
+ return self.__writeConfig()
+
+
+ def reWriteConfig(
+ self
+ ) -> bool:
+
+ return self.__writeConfig()
+
+
+ def configPath(
+ self
+ ) -> str:
+
+ return self.__config_path
\ No newline at end of file
diff --git a/LibCheckin.py b/LibCheckin.py
new file mode 100644
index 0000000..57544ef
--- /dev/null
+++ b/LibCheckin.py
@@ -0,0 +1,40 @@
+# -*- 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 re
+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 LibOperator import LibOperator
+
+
+class LibCheckin(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
\ No newline at end of file
diff --git a/LibCheckout.py b/LibCheckout.py
new file mode 100644
index 0000000..be4ba7a
--- /dev/null
+++ b/LibCheckout.py
@@ -0,0 +1,40 @@
+# -*- 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 re
+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 LibOperator import LibOperator
+
+
+class LibCheckout(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
\ No newline at end of file
diff --git a/LibLogin.py b/LibLogin.py
new file mode 100644
index 0000000..8cbedb7
--- /dev/null
+++ b/LibLogin.py
@@ -0,0 +1,181 @@
+# -*- 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 time
+import queue
+import base64
+
+import ddddocr
+
+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
+
+
+class LibLogin(LibOperator):
+
+ def __init__(
+ self,
+ input_queue: queue.Queue,
+ output_queue: queue.Queue,
+ driver
+ ):
+
+ super().__init__(input_queue, output_queue)
+
+ self.__driver = driver
+ self.__ddddocr = ddddocr.DdddOcr()
+
+
+ def _waitResponseLoad(
+ self,
+ ) -> bool:
+
+ # wait to verify login success
+ try:
+ WebDriverWait(self.__driver, 5).until( # title contains "自选座位 :: 座位预约系统"
+ EC.title_contains("自选座位 :: 座位预约系统")
+ )
+ WebDriverWait(self.__driver, 3).until( # search button presence
+ EC.presence_of_element_located((By.ID, "search"))
+ )
+ WebDriverWait(self.__driver, 3).until( # select content presence
+ EC.presence_of_element_located((By.CLASS_NAME, "selectContent"))
+ )
+ return True
+ except Exception as e:
+ self._showTrace(f"登录页面加载失败 ! : {e}")
+ return False
+
+
+ def __fillLogInElements(
+ self,
+ username: str,
+ password: str,
+ ) -> bool:
+
+ # ensure elements presence and fill them
+ try:
+ username_element = self.__driver.find_element(By.NAME, "username")
+ username_element.clear()
+ username_element.send_keys(username)
+ password_element = self.__driver.find_element(By.NAME, "password")
+ password_element.clear()
+ password_element.send_keys(password)
+ except Exception as e:
+ self._showTrace(f"用户名或密码填写失败 ! : {e}")
+ return False
+ return True
+
+
+ def __autoRecognizeCaptcha(
+ self,
+ ) -> str:
+
+ # auto recognize captcha
+ try:
+ captcha_img = self.__driver.find_element(By.ID, "loadImgId")
+ img_src = captcha_img.get_attribute("src")
+ base64_str = img_src.split(',', 1)[1]
+ captcha_img = base64.b64decode(base64_str)
+ captcha_text = self.__ddddocr.classification(captcha_img)
+ captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower()
+ self._showTrace(f"识别到验证码为 : '{captcha_text}'.")
+ if len(captcha_text) != 4:
+ raise Exception("识别到的验证码长度不等于 4 个字符 !")
+ return captcha_text
+ except Exception as e:
+ self._showTrace(f"验证码识别失败 ! : {e}")
+ self.__refreshCaptcha()
+ return ""
+
+
+ def __manualRecognizeCaptcha(
+ self,
+ ) -> str:
+
+ # manual recognize captcha
+ try:
+ self._show_msg("请输入验证码:")
+ captcha_text = self._wait_msg(timeout=15)
+ self._showTrace(f"输入的验证码为 : '{captcha_text}'.")
+ if len(captcha_text) != 4:
+ raise Exception("输入的验证码长度不等于 4 个字符 !")
+ return captcha_text
+ except Exception as e:
+ self._showTrace(f"输入验证码失败 ! : {e}")
+ self.__refreshCaptcha()
+ return ""
+
+
+ def __refreshCaptcha(
+ self,
+ ):
+
+ # refresh captcha
+ try:
+ self._showTrace("刷新验证码......")
+ self.__driver.find_element(
+ By.ID, "loadImgId"
+ ).click()
+ time.sleep(1)
+ return True
+ except Exception as e:
+ self._showTrace(f"刷新验证码失败 ! : {e}")
+ self.__refreshCaptcha()
+ return False
+
+
+ def login(
+ self,
+ username: str,
+ password: str,
+ max_attempts: int = 5,
+ auto_captcha: bool = True,
+ ) -> bool:
+
+ if self.__driver is None:
+ self._showTrace("未提供有效 WebDriver 实例 !")
+ return False
+ # begin login process
+ for attempt in range(max_attempts):
+ self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......")
+ if not self.__fillLogInElements(
+ username,
+ password,
+ ):
+ continue
+ while True:
+ if auto_captcha:
+ captcha_text = self.__autoRecognizeCaptcha()
+ else:
+ self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
+ captcha_text = self.__manualRecognizeCaptcha()
+ if captcha_text:
+ break
+ captcha_element = self.__driver.find_element(By.NAME, "answer")
+ captcha_element.clear()
+ captcha_element.send_keys(captcha_text)
+ self._showTrace("尝试登录...")
+ try:
+ self.__driver.find_element(
+ By.XPATH,
+ "//input[@type='button' and @value='登录']"
+ ).click()
+ except Exception as e:
+ self._showTrace(f"登录失败 ! : {e}")
+ continue
+ if self._waitResponseLoad():
+ self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
+ return True
+ else:
+ self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !")
+ return False
diff --git a/LibLogout.py b/LibLogout.py
new file mode 100644
index 0000000..b894fa2
--- /dev/null
+++ b/LibLogout.py
@@ -0,0 +1,62 @@
+# -*- 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 queue
+
+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
+
+
+class LibLogout(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:
+
+ return True
+
+
+ def logout(
+ self,
+ username: str,
+ ) -> bool:
+
+ if self.__driver is None:
+ self._showTrace("未提供有效 WebDriver 实例 !")
+
+ return False
+
+ try:
+ self.__driver.find_element(
+ By.XPATH, "//a[@href='/logout']"
+ ).click()
+
+ self._showTrace(f"用户 {username} 注销成功 !")
+
+ return True
+
+ except Exception as e:
+ self._showTrace(f"用户 {username} 注销失败 ! : {e}")
+
+ return False
diff --git a/LibOperator.py b/LibOperator.py
new file mode 100644
index 0000000..81851c3
--- /dev/null
+++ b/LibOperator.py
@@ -0,0 +1,30 @@
+# -*- 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 queue
+
+from MsgBase import MsgBase
+
+
+class LibOperator(MsgBase):
+
+ def __init__(
+ self,
+ input_queue: queue.Queue,
+ output_queue: queue.Queue,
+ ):
+
+ super().__init__(input_queue, output_queue)
+
+
+ def _waitResponseLoad(
+ self,
+ ) -> bool:
+
+ pass
diff --git a/LibRenew.py b/LibRenew.py
new file mode 100644
index 0000000..5df4450
--- /dev/null
+++ b/LibRenew.py
@@ -0,0 +1,34 @@
+# -*- 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
\ No newline at end of file
diff --git a/LibReserve.py b/LibReserve.py
new file mode 100644
index 0000000..cedc134
--- /dev/null
+++ b/LibReserve.py
@@ -0,0 +1,652 @@
+# -*- 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 re
+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 LibOperator import LibOperator
+
+
+class LibReserve(LibOperator):
+
+ def __init__(
+ self,
+ input_queue: queue.Queue,
+ output_queue: queue.Queue,
+ driver
+ ):
+
+ super().__init__(input_queue, output_queue)
+
+ self.__driver = driver
+ # library floor and room mapping in website
+ self.__floor_map = {
+ "2": "二层",
+ "3": "三层",
+ "4": "四层",
+ "5": "五层"
+ }
+ self.__room_map = {
+ "1": "二层内环",
+ "2": "二层外环",
+ "3": "三层内环",
+ "4": "三层外环",
+ "5": "四层内环",
+ "6": "四层外环",
+ "7": "四层期刊区",
+ "8": "五层考研"
+ }
+
+
+ def _waitResponseLoad(
+ self,
+ ) -> bool:
+
+ try:
+ WebDriverWait(self.__driver, 5).until(
+ EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat"))
+ )
+ title_elements = []
+ # reserve failed without title elements, so we need to try
+ try:
+ WebDriverWait(self.__driver, 1).until(
+ EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt"))
+ )
+ title_elements = self.__driver.find_elements(
+ By.CSS_SELECTOR, ".layoutSeat dt"
+ )
+ except:
+ pass
+ content_elements = self.__driver.find_elements(
+ By.CSS_SELECTOR, ".layoutSeat dd"
+ )
+ if not content_elements:
+ self._showTrace("未找到预约结果")
+ raise
+ title = title_elements[0].text if title_elements else ""
+ contents = [element.text for element in content_elements if element.text.strip()]
+ for message in contents:
+ if "预约失败" in message or "已有1个有效预约" in message:
+ self._showTrace(f"预约失败 - {"".join(contents)}")
+ raise
+ if "预定好了" in title or "预约成功" in title or "操作成功" in title:
+ if len(contents) >= 6:
+ date_val = contents[1].split(" : ")[1].strip() if " : " in contents[1] else contents[1].strip()
+ time_val = contents[2].split(" : ")[1].strip() if " : " in contents[2] else contents[2].strip()
+ seat_val = contents[3].split(" : ")[1].strip() if " : " in contents[3] else contents[3].strip()
+ checkin_val = contents[5].strip()
+ self._showTrace(f"\n"\
+ f" 预约成功 !\n"\
+ f" 预约日期: {date_val}, \n"\
+ f" 预约时间: {time_val}, \n"\
+ f" 预约座位: {seat_val}, \n"\
+ f" 签到时间: {checkin_val}")
+ else:
+ self._showTrace(f"\n"\
+ f" 预约成功 !\n"\
+ f" 未找获取到详细信息")
+ return True
+ except:
+ self._showTrace(f"预约结果加载失败 !")
+ 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 __checkReserveInfo(
+ self,
+ reserve_info: dict
+ ) -> bool:
+
+ try:
+ # check the required information
+ # reserve_info["place"]
+ if reserve_info.get("floor") is None:
+ raise ValueError("未指定楼层")
+ if reserve_info["floor"] not in self.__floor_map:
+ raise ValueError(f"楼层 '{reserve_info['floor']}' 不存在")
+ if reserve_info.get("room") is None:
+ raise ValueError("未指定房间")
+ if reserve_info["room"] not in self.__room_map:
+ raise ValueError(f"房间 '{reserve_info['room']}' 不存在")
+ if reserve_info.get("seat_id") is None:
+ raise ValueError("未指定座位")
+ except ValueError as e:
+ self._showTrace(
+ f"预约信息错误 ! : {e}, "\
+ f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
+ )
+ return False
+
+ # check and try to fix the time errors
+ cur_time_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
+ cur_date, curr_time = cur_time_str.split()
+ if not reserve_info.get("date"):
+ reserve_info["date"] = cur_date
+ self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}")
+ else:
+ if reserve_info["date"] < cur_date:
+ self._showTrace(
+ f"预约日期错误 ! :"\
+ f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期"
+ )
+ reserve_info["date"] = cur_date
+ # check the begin time
+ begin_time = reserve_info.get("begin_time")
+ if not begin_time:
+ reserve_info["begin_time"] = {
+ "time": curr_time,
+ "max_diff": 30,
+ "prefer_early": True
+ }
+ self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}, 最大时间差为 30 分钟, 优先选择更早预约时间")
+ else:
+ begin_time = reserve_info["begin_time"]
+ if "time" not in begin_time:
+ begin_time["time"] = curr_time
+ self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}")
+ if "max_diff" not in begin_time:
+ begin_time["max_diff"] = 30
+ self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
+ if "prefer_early" not in begin_time:
+ begin_time["prefer_early"] = True
+ self._showTrace(f"是否优先选择更早预约时间未指定, 自动设置为 True")
+ expect_duration = reserve_info.get("expect_duration")
+ if not expect_duration:
+ reserve_info["expect_duration"] = 4
+ expect_duration = 4
+ self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
+ if not reserve_info.get("satisfy_duration"):
+ reserve_info["satisfy_duration"] = True
+ self._showTrace("预约满足时长要求未指定, 默认满足")
+ # check the end time
+ if not reserve_info.get("end_time"):
+ begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
+ end_mins = begin_mins + reserve_info["expect_duration"] * 60
+ end_time_str = self.__minsToTime(end_mins)
+ reserve_info["end_time"] = {
+ "time": end_time_str,
+ "max_diff": 30,
+ "prefer_early": False
+ }
+ self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time_str}, 最大时间差为 30 分钟, 优先选择较晚预约时间")
+ else:
+ end_time = reserve_info["end_time"]
+ if "time" not in end_time:
+ begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
+ end_mins = begin_mins + reserve_info["expect_duration"] * 60
+ end_time["time"] = self.__minsToTime(end_mins)
+ self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time['time']}")
+ if "max_diff" not in end_time:
+ end_time["max_diff"] = 30
+ self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
+ if "prefer_early" not in end_time:
+ end_time["prefer_early"] = False
+ self._showTrace(f"是否优先选择较早预约时间未指定, 自动设置为 False")
+ # check the reserve time boundary and fix the errors
+ #
+ # get time string for message show
+ begin_time_str = reserve_info["begin_time"]["time"]
+ end_time_str = reserve_info["end_time"]["time"]
+
+ # minute time for check and fix them
+ begin_mins = self.__timeToMins(begin_time_str)
+ end_mins = self.__timeToMins(end_time_str)
+
+ # ensure begin time is not later than end time
+ if begin_mins > end_mins:
+ reserve_info["begin_time"]["time"], reserve_info["end_time"]["time"] = end_time_str, begin_time_str
+ reserve_info["begin_time"]["prefer_early"], reserve_info["end_time"]["prefer_early"] = \
+ reserve_info["end_time"]["prefer_early"], reserve_info["begin_time"]["prefer_early"]
+ self._showTrace("预约开始时间晚于预约结束时间,自动调换开始时间和结束时间")
+
+ # update the begin_mins and end_mins after swap
+ begin_time_str, end_time_str = end_time_str, begin_time_str
+ begin_mins, end_mins = end_mins, begin_mins
+
+ # ensure end time is not later than 22:30
+ max_end_mins = self.__timeToMins("22:30")
+ if end_mins > max_end_mins:
+ reserve_info["end_time"]["time"] = "22:30"
+ end_time_str = "22:30"
+ end_mins = max_end_mins
+ self._showTrace("预约结束时间超过 22:30, 自动设置为 22:30")
+
+ # ensure expect duration is shorter than 8 hours
+ max_duration_mins = 8 * 60
+ duration_mins = end_mins - begin_mins
+ if duration_mins > max_duration_mins:
+ new_end_mins = begin_mins + max_duration_mins
+ reserve_info["end_time"]["time"] = self.__minsToTime(new_end_mins)
+ self._showTrace("预约持续时间超过8小时, 自动设置为 8 小时")
+ self._showTrace(
+ f"预约信息检查完成,准备预约 "
+ f"{reserve_info['date']} "
+ f"{reserve_info['begin_time']["time"]} - "
+ f"{reserve_info['end_time']["time"]} "
+ f"图书馆 "
+ f"{self.__floor_map[reserve_info['floor']]} "
+ f"{self.__room_map[reserve_info['room']]} "
+ f"的座位 {reserve_info['seat_id']}"
+ )
+ return True
+
+
+ def __clickElement(
+ self,
+ trigger_locator: tuple,
+ fail_msg: str,
+ success_msg: str,
+ option_locator: tuple = None,
+ ) -> bool:
+
+ try:
+ # click the trigger element
+ WebDriverWait(self.__driver, 5).until(
+ EC.element_to_be_clickable(trigger_locator)
+ ).click()
+ if option_locator:
+ # select the option element if specified
+ WebDriverWait(self.__driver, 5).until(
+ EC.element_to_be_clickable(option_locator)
+ ).click()
+ self._showTrace(success_msg)
+ return True
+ except:
+ self._showTrace(fail_msg)
+ return False
+
+
+ def __selectDate(
+ self,
+ date_str: str
+ ) -> bool:
+
+ return self.__clickElement(
+ trigger_locator=(By.ID, "onDate_select"),
+ option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
+ success_msg=f"日期 {date_str} 选择成功 !",
+ fail_msg=f"选择日期失败 ! : {date_str} 不可用"
+ )
+
+
+ def __selectPlace(
+ self,
+ place: str
+ ) -> bool:
+
+ actual_place = "1" if place == "图书馆" else "1"
+ 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} 不可用"
+ )
+
+
+ def __selectFloor(
+ self,
+ floor: str
+ ) -> bool:
+
+ display_floor = self.__floor_map.get(floor)
+ return self.__clickElement(
+ trigger_locator=(By.ID, "floor_select"),
+ option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
+ success_msg=f"楼层 {display_floor} 选择成功 !",
+ fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
+ )
+
+
+ def __selectRoom(
+ self,
+ room: str
+ ) -> 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} 不可用"
+ )
+
+
+ def __selectSeat(
+ self,
+ seat_id: str
+ ) -> bool:
+
+ try:
+ # wait fot seat layout element to load
+ WebDriverWait(self.__driver, 5).until(
+ EC.presence_of_element_located((By.ID, "seatLayout"))
+ )
+ all_seats = self.__driver.find_elements(
+ By.CSS_SELECTOR, "li[id^='seat_']"
+ )
+ seat_id_upper = seat_id.lstrip('0').upper()
+ for seat in all_seats:
+ if not seat_id_upper == seat.text.lstrip('0'):
+ continue
+ seat_link = seat.find_element(By.TAG_NAME, "a")
+ WebDriverWait(self.__driver, 5).until(
+ EC.element_to_be_clickable(seat_link)
+ )
+ seat_link.click()
+ seat_status = seat_link.get_attribute("title")
+ self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'")
+ return True
+ self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确")
+ except:
+ self._showTrace(f"座位选择失败 !")
+ return False
+
+
+ def __selectNearestTime(
+ self,
+ time_id: str,
+ time_type: str,
+ target_time: int,
+ max_time_diff: int = 30,
+ prefer_earlier: bool = True
+ ) -> int:
+
+ try:
+ all_time_opts = self.__driver.find_elements(
+ By.CSS_SELECTOR,
+ f"#{time_id} ul li a"
+ )
+ free_times = []
+ best_time_diff = max_time_diff
+ best_actual_diff = None
+ best_time_opt = None
+
+ for time_opt in all_time_opts:
+ time_attr = time_opt.get_attribute("time")
+ if time_attr == "now":
+ now = datetime.now()
+ time_val = int(now.hour*60 + now.minute)
+ elif time_attr and time_attr.isdigit():
+ time_val = int(time_attr)
+ else:
+ continue
+ free_times.append(self.__minsToTime(time_val))
+ actual_diff = time_val - target_time
+ abs_diff = abs(actual_diff)
+ if abs_diff < best_time_diff or (
+ abs_diff == best_time_diff and (
+ # prefer earlier time
+ (prefer_earlier and actual_diff < 0) or
+ # prefer later time
+ (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"正好等于 {time_type}"
+ target_time += best_actual_diff
+ self._showTrace(
+ f"选择距离期望 {time_type} 最近的 {best_time_opt.text}, "\
+ f"与期望 {time_type} 相比 {time_relation}"
+ )
+ return target_time
+ self._showTrace(
+ f"无法选择最近的 {time_type} {self.__minsToTime(target_time)}, "\
+ f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟"
+ )
+ self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
+ return -1
+ except:
+ self._showTrace(f"{time_type} {self.__minsToTime(target_time)} 选择失败 !")
+ return -1
+
+
+ def __selectSeatTime(
+ self,
+ begin_time: dict,
+ end_time: dict,
+ expct_duration: int = 4,
+ satisfy_duration: bool = True
+ ) -> bool:
+
+ 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)
+ expect_end_mins = self.__timeToMins(expect_end_time)
+
+ # select the begin time
+ if self.__selectNearestTime(
+ time_id="startTime", # dont change into begin, this is the element in the page
+ time_type="开始时间",
+ target_time=expect_begin_mins,
+ max_time_diff=begin_time["max_diff"],
+ prefer_earlier=begin_time["prefer_early"]
+ ) == -1:
+ return False
+ else:
+ actual_begin_time = self.__minsToTime(expect_begin_mins)
+ # 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)
+ self._showTrace(
+ f"需要满足期望预约持续时间: {expct_duration} 小时, "\
+ f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
+ )
+ # select the end time
+ if self.__selectNearestTime(
+ time_id="endTime",
+ time_type="结束时间",
+ target_time=expect_end_mins,
+ max_time_diff=end_time["max_diff"],
+ prefer_earlier=end_time["prefer_early"]
+ ) == -1:
+ return False
+ else:
+ actual_end_time = self.__minsToTime(expect_end_mins)
+ self._showTrace(
+ f"期望预约时间段: {expect_begin_time} - {expect_end_time}, "
+ f"实际预约时间段: {actual_begin_time} - {actual_end_time}"
+ )
+ return True
+
+
+ def canReserve(
+ self,
+ date: str
+ ) -> bool:
+
+ if date is None:
+ self._showTrace("日期未指定, 无法检查预约状态")
+ return True
+ else:
+ self._showTrace(f"正在检查用户在日期 {date} 是否可预约......")
+ date_obj = datetime.strptime(date, "%Y-%m-%d").date()
+ try:
+ # we need to navigate to the history page to check if we can reserve
+ WebDriverWait(self.__driver, 5).until(
+ EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
+ ).click()
+ WebDriverWait(self.__driver, 2).until(
+ EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
+ )
+ WebDriverWait(self.__driver, 2).until(
+ EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList dl"))
+ )
+ except:
+ self._showTrace("加载预约记录页面失败 !")
+ return False
+ checked_count = 0
+ max_attemots = 3 # we only check (3*4=)12 reservations
+
+ for _ in range(max_attemots):
+ try:
+ # check if there's any reservation on the date
+ reservations = self.__driver.find_elements(
+ By.CSS_SELECTOR, ".myReserveList dl"
+ )
+ except:
+ self._showTrace("加载预约记录失败 !")
+ return False
+ for i in range(checked_count, len(reservations) - 1): # the last one is load button
+ reservation = reservations[i]
+ try:
+ time_element = reservation.find_element(
+ By.CSS_SELECTOR, "dt"
+ )
+ status_elements = reservation.find_elements(
+ By.CSS_SELECTOR, "a"
+ )
+ is_reserved = any("已预约" in status.text for status in status_elements)
+ # 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:
+ continue
+ # reservation is earlier than the given date, can reserve
+ if datetime.strptime(date_str, "%Y-%m-%d").date() < date_obj:
+ self._showTrace(f"用户在 {date} 可预约")
+ return True
+ # reservation is later than the given date, check the next one
+ elif datetime.strptime(date_str, "%Y-%m-%d").date() > date_obj:
+ continue
+ # compare with the given date
+ if date_str == date and is_reserved:
+ self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
+ return False
+ except:
+ self._showTrace(f"解析第 {i + 1} 条预约记录时发生未知错误 !")
+ continue
+ checked_count = len(reservations) - 1
+ # 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:
+ break
+ except:
+ break
+ self._showTrace(f"用户在 {date} 可预约")
+ return True
+
+
+ def reserve(
+ self,
+ reserve_info: dict
+ ) -> bool:
+
+ submit_reserve = False
+ reserve_success = False
+ have_hover_on_page = False
+
+ # reserve info
+ if not self.__checkReserveInfo(reserve_info):
+ return False
+ # map page
+ try:
+ WebDriverWait(self.__driver, 5).until(
+ EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
+ ).click()
+ WebDriverWait(self.__driver, 5).until(
+ EC.presence_of_element_located((By.ID, "seatLayout"))
+ )
+ except:
+ self._showTrace(f"加载预约选座页面失败 !")
+ return False
+ # date, place, floor
+ 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, 5).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:
+ have_hover_on_page = True
+ # seat selections
+ if not self.__selectSeat(reserve_info["seat_id"]):
+ pass
+ elif not self.__selectSeatTime(
+ begin_time=reserve_info["begin_time"],
+ end_time=reserve_info["end_time"],
+ expct_duration=reserve_info["expect_duration"],
+ satisfy_duration=reserve_info["satisfy_duration"]
+ ):
+ pass
+ else:
+ try:
+ WebDriverWait(self.__driver, 5).until(
+ EC.element_to_be_clickable((By.ID, "reserveBtn"))
+ ).click()
+ submit_reserve = True
+ if not self._waitResponseLoad():
+ raise
+ reserve_success = True
+ except:
+ self._showTrace(f"预约提交失败 !")
+ if not submit_reserve and have_hover_on_page:
+ self.__driver.refresh()
+ return reserve_success
\ No newline at end of file
diff --git a/Main.py b/Main.py
new file mode 100644
index 0000000..cca8e0c
--- /dev/null
+++ b/Main.py
@@ -0,0 +1,33 @@
+# -*- 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 sys
+
+from PySide6.QtCore import QTranslator
+from PySide6.QtWidgets import QApplication
+
+from gui.ALMainWindow import ALMainWindow
+from gui import AutoLibraryResource
+
+
+def main():
+
+ app = QApplication(sys.argv)
+ translator = QTranslator()
+ if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
+ app.installTranslator(translator)
+ app.setStyle('Fusion')
+ window = ALMainWindow()
+ window.show()
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+
+ main()
\ No newline at end of file
diff --git a/MsgBase.py b/MsgBase.py
new file mode 100644
index 0000000..6a8ca56
--- /dev/null
+++ b/MsgBase.py
@@ -0,0 +1,65 @@
+# -*- 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 time
+import queue
+
+
+class MsgBase:
+
+ def __init__(
+ self,
+ input_queue: queue.Queue,
+ output_queue: queue.Queue,
+ ):
+
+ self._class_name = self.__class__.__name__
+ self._input_queue = input_queue
+ self._output_queue = output_queue
+
+
+ def _showMsg(
+ self,
+ msg: str
+ ):
+
+ self._output_queue.put(f"[{self._class_name:<12}] >>> : {msg}")
+
+
+ def _showTrace(
+ self,
+ msg: str
+ ):
+
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+ self._output_queue.put(f"{timestamp}-[{self._class_name:<12}] : {msg}")
+
+
+ def _waitMsg(
+ self,
+ timeout: float = 1.0,
+ ) -> str:
+
+ try:
+ msg = self._input_queue.get(timeout=timeout)
+ return msg
+ except queue.Empty:
+ return None
+
+
+ def _inputMsg(
+ self,
+ timeout: float = 1.0,
+ ) -> bool:
+
+ try:
+ self._input_queue.get(timeout=timeout)
+ return True
+ except queue.Empty:
+ return False
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..8b55232
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,15 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+ddddocr = "*"
+selenium = "*"
+pyinstaller = "*"
+pyside6 = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.13"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..0bb9f2f
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,630 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "26dffc26812d5328611959b95713a7ed65e20c08c60089b54283b0f406dd08e4"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.13"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "altgraph": {
+ "hashes": [
+ "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406",
+ "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"
+ ],
+ "version": "==0.17.4"
+ },
+ "attrs": {
+ "hashes": [
+ "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
+ "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==25.4.0"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de",
+ "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2025.10.5"
+ },
+ "cffi": {
+ "hashes": [
+ "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
+ "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
+ "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
+ "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
+ "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
+ "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
+ "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
+ "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
+ "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
+ "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
+ "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
+ "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
+ "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
+ "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
+ "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
+ "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
+ "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
+ "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
+ "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
+ "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
+ "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
+ "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
+ "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
+ "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
+ "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
+ "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
+ "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
+ "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
+ "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
+ "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
+ "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
+ "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
+ "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
+ "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
+ "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
+ "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
+ "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
+ "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
+ "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
+ "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
+ "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
+ "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
+ "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
+ "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
+ "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
+ "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
+ "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
+ "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
+ "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
+ "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
+ "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
+ "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
+ "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
+ "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
+ "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
+ "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
+ "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
+ "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
+ "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
+ "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
+ "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
+ "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
+ "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
+ "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
+ "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
+ "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
+ "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
+ "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
+ "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
+ "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
+ "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
+ "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
+ "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
+ "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
+ "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
+ "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
+ "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
+ "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
+ "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
+ "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
+ "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
+ "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
+ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
+ "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==2.0.0"
+ },
+ "coloredlogs": {
+ "hashes": [
+ "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
+ "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==15.0.1"
+ },
+ "ddddocr": {
+ "hashes": [
+ "sha256:5991594d481d33ba0b136022e910f578d6d5b0ca536b44886591359622ab0c70",
+ "sha256:7c44b58ba7d7566d785c65b8526ec5b78efacd121e993dea4fda5f7966897428"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.0.6"
+ },
+ "flatbuffers": {
+ "hashes": [
+ "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2",
+ "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12"
+ ],
+ "version": "==25.9.23"
+ },
+ "h11": {
+ "hashes": [
+ "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
+ "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.16.0"
+ },
+ "humanfriendly": {
+ "hashes": [
+ "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
+ "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==10.0"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
+ "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.11"
+ },
+ "mpmath": {
+ "hashes": [
+ "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f",
+ "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"
+ ],
+ "version": "==1.3.0"
+ },
+ "numpy": {
+ "hashes": [
+ "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64",
+ "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e",
+ "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0",
+ "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365",
+ "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d",
+ "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c",
+ "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52",
+ "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36",
+ "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec",
+ "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f",
+ "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197",
+ "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7",
+ "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9",
+ "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37",
+ "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a",
+ "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db",
+ "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c",
+ "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7",
+ "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d",
+ "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e",
+ "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f",
+ "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a",
+ "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16",
+ "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e",
+ "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868",
+ "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05",
+ "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e",
+ "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff",
+ "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f",
+ "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7",
+ "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f",
+ "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e",
+ "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562",
+ "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6",
+ "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0",
+ "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26",
+ "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0",
+ "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d",
+ "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879",
+ "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef",
+ "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29",
+ "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252",
+ "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847",
+ "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6",
+ "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32",
+ "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0",
+ "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3",
+ "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b",
+ "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3",
+ "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc",
+ "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc",
+ "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda",
+ "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a",
+ "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40",
+ "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032",
+ "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7",
+ "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966",
+ "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9",
+ "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346",
+ "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2",
+ "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a",
+ "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786",
+ "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f",
+ "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc",
+ "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb",
+ "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646",
+ "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd",
+ "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1",
+ "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11",
+ "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667",
+ "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996",
+ "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953",
+ "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b",
+ "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"
+ ],
+ "markers": "python_version >= '3.11'",
+ "version": "==2.3.4"
+ },
+ "onnxruntime": {
+ "hashes": [
+ "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc",
+ "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77",
+ "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95",
+ "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088",
+ "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b",
+ "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435",
+ "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f",
+ "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b",
+ "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7",
+ "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c",
+ "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c",
+ "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612",
+ "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872",
+ "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2",
+ "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e",
+ "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466",
+ "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3",
+ "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36",
+ "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321",
+ "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6",
+ "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e",
+ "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145"
+ ],
+ "markers": "python_version >= '3.10'",
+ "version": "==1.23.2"
+ },
+ "outcome": {
+ "hashes": [
+ "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
+ "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.3.0.post0"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
+ "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==25.0"
+ },
+ "pefile": {
+ "hashes": [
+ "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc",
+ "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"
+ ],
+ "markers": "python_full_version >= '3.6.0'",
+ "version": "==2023.2.7"
+ },
+ "pillow": {
+ "hashes": [
+ "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643",
+ "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e",
+ "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e",
+ "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc",
+ "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642",
+ "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6",
+ "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1",
+ "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b",
+ "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399",
+ "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba",
+ "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad",
+ "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47",
+ "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739",
+ "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b",
+ "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f",
+ "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10",
+ "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52",
+ "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d",
+ "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b",
+ "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a",
+ "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9",
+ "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d",
+ "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098",
+ "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905",
+ "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b",
+ "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3",
+ "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371",
+ "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953",
+ "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01",
+ "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca",
+ "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e",
+ "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7",
+ "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27",
+ "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082",
+ "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e",
+ "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d",
+ "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8",
+ "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a",
+ "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad",
+ "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3",
+ "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a",
+ "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d",
+ "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353",
+ "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee",
+ "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b",
+ "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b",
+ "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a",
+ "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7",
+ "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef",
+ "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a",
+ "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a",
+ "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257",
+ "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07",
+ "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4",
+ "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c",
+ "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c",
+ "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4",
+ "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe",
+ "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8",
+ "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5",
+ "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6",
+ "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e",
+ "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8",
+ "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e",
+ "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275",
+ "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3",
+ "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76",
+ "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227",
+ "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9",
+ "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5",
+ "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79",
+ "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca",
+ "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa",
+ "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b",
+ "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e",
+ "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197",
+ "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab",
+ "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79",
+ "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2",
+ "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363",
+ "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0",
+ "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e",
+ "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782",
+ "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925",
+ "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0",
+ "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b",
+ "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced",
+ "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c",
+ "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344",
+ "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9",
+ "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"
+ ],
+ "markers": "python_version >= '3.10'",
+ "version": "==12.0.0"
+ },
+ "protobuf": {
+ "hashes": [
+ "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954",
+ "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995",
+ "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef",
+ "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455",
+ "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee",
+ "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9",
+ "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3",
+ "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035",
+ "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90",
+ "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==6.33.0"
+ },
+ "pycparser": {
+ "hashes": [
+ "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
+ "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.23"
+ },
+ "pyinstaller": {
+ "hashes": [
+ "sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058",
+ "sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef",
+ "sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8",
+ "sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851",
+ "sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a",
+ "sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3",
+ "sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0",
+ "sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0",
+ "sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41",
+ "sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454",
+ "sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64",
+ "sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5"
+ ],
+ "index": "pypi",
+ "markers": "python_version < '3.15' and python_version >= '3.8'",
+ "version": "==6.16.0"
+ },
+ "pyinstaller-hooks-contrib": {
+ "hashes": [
+ "sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6",
+ "sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2025.9"
+ },
+ "pyreadline3": {
+ "hashes": [
+ "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7",
+ "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.5.4"
+ },
+ "pyside6": {
+ "hashes": [
+ "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339",
+ "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f",
+ "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820",
+ "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01",
+ "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"
+ ],
+ "index": "pypi",
+ "markers": "python_version < '3.14' and python_version >= '3.9'",
+ "version": "==6.10.0"
+ },
+ "pyside6-addons": {
+ "hashes": [
+ "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464",
+ "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0",
+ "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c",
+ "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1",
+ "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"
+ ],
+ "markers": "python_version < '3.14' and python_version >= '3.9'",
+ "version": "==6.10.0"
+ },
+ "pyside6-essentials": {
+ "hashes": [
+ "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a",
+ "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8",
+ "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1",
+ "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998",
+ "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"
+ ],
+ "markers": "python_version < '3.14' and python_version >= '3.9'",
+ "version": "==6.10.0"
+ },
+ "pysocks": {
+ "hashes": [
+ "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
+ "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
+ "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.7.1"
+ },
+ "pywin32-ctypes": {
+ "hashes": [
+ "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8",
+ "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.2.3"
+ },
+ "selenium": {
+ "hashes": [
+ "sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c",
+ "sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.10'",
+ "version": "==4.38.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922",
+ "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==80.9.0"
+ },
+ "shiboken6": {
+ "hashes": [
+ "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717",
+ "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543",
+ "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148",
+ "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61",
+ "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"
+ ],
+ "markers": "python_version < '3.14' and python_version >= '3.9'",
+ "version": "==6.10.0"
+ },
+ "sniffio": {
+ "hashes": [
+ "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
+ "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.3.1"
+ },
+ "sortedcontainers": {
+ "hashes": [
+ "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
+ "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
+ ],
+ "version": "==2.4.0"
+ },
+ "sympy": {
+ "hashes": [
+ "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517",
+ "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==1.14.0"
+ },
+ "trio": {
+ "hashes": [
+ "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b",
+ "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"
+ ],
+ "markers": "python_version >= '3.10'",
+ "version": "==0.32.0"
+ },
+ "trio-websocket": {
+ "hashes": [
+ "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae",
+ "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.2"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
+ "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==4.15.0"
+ },
+ "urllib3": {
+ "extras": [
+ "socks"
+ ],
+ "hashes": [
+ "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
+ "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==2.5.0"
+ },
+ "websocket-client": {
+ "hashes": [
+ "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98",
+ "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==1.9.0"
+ },
+ "wsproto": {
+ "hashes": [
+ "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
+ "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==1.2.0"
+ }
+ },
+ "develop": {}
+}
diff --git a/document/manual.html b/document/manual.html
new file mode 100644
index 0000000..02bdbb1
--- /dev/null
+++ b/document/manual.html
@@ -0,0 +1,847 @@
+
+
+
+
+
+ AutoLibrary 操作手册
+
+
+
+
+
+
+
+
+
+
+ 工具简介
+
+
+
+
AutoLibrary 是一款专为北京建筑大学图书馆设计的自动化工具,旨在帮助学生简化图书馆座位操作流程,节省宝贵时间。
+
+
本工具模拟人工操作,通过简单的界面交互使用。
+
+
工具特点
+
+ 模拟人工操作,不干扰图书馆系统正常运行
+ 支持多种预约模式,满足不同使用场景
+ 支持多账号批量预约
+ 自动处理验证码,减少人工干预
+
+
+
+
+
+
+ 准备工作
+
+
+
+
+
下载浏览器驱动
+
工具需要通过浏览器驱动来控制浏览器,请根据您使用的浏览器下载对应版本的驱动:
+
+
+
+
+

+
+
Microsoft Edge
+
适用于Windows 10/11系统
+
下载驱动
+
+
+
+
+

+
+
Google Chrome
+
最常用的浏览器
+
下载驱动
+
+
+
+
+

+
+
Mozilla Firefox
+
开源浏览器
+
下载驱动
+
+
+
+
+ 注意: 浏览器驱动版本必须与您的浏览器版本兼容,否则工具无法正常工作。
+
+
+ 浏览器驱动下载页面截图区域
+
+
+
+
+
+
+
+
确认驱动路径
+
下载驱动后,将浏览器驱动程序的路径(如'C:\Users\Administrator\Downloads\msedgedriver.exe')输入到AutoLibrary配置界面中。
+
+ 驱动文件放置位置截图区域
+
+
+
+
+
+
+
+ 使用步骤
+
+
+
+
+
编辑配置文件
+
使用文本编辑器(如记事本、Visual Studio Code等)打开config.json和users.json文件,按照您的需求修改参数。
+
+ 重要: 请勿使用Microsoft Word等富文本编辑器,这可能导致文件格式错误。
+
+
+ 配置文件编辑截图区域
+
+
+
+
+
+
+
+
运行工具
+
双击运行main.exe文件,工具将自动开始预约流程。
+
+ 工具运行界面截图区域
+
+
+
+
+
+
+
+
监控运行状态
+
如果headless模式设置为false,您将看到浏览器窗口自动操作。请勿手动干预浏览器窗口。
+
+ 浏览器自动操作截图区域
+
+
+
+
+
+
+
+
查看结果
+
工具运行完成后,查看生成的日志文件确认预约结果。
+
+ 运行日志截图区域
+
+
+
+
+
+
+
+ 配置说明
+ AutoLibrary通过两个配置文件来控制工具行为:config.json(工具设置)和users.json(用户信息)。
+
+
+
+
+
+
+
+
+
工具配置文件
+
config.json文件控制工具的基本运行参数:
+
+{
+ "library": {
+ "lib_host_url": "http://10.1.20.7",
+ "lib_login_url": "/login"
+ },
+ "mode": {
+ "run_mode": 1
+ },
+ "login": {
+ "auto_captcha": true,
+ "login_attempt": 3
+ },
+ "web_driver": {
+ "driver_type": "edge",
+ "driver_path": "msedgedriver.exe",
+ "headless": false
+ }
+}
+
+
+
参数说明
+
+ - run_mode: 运行模式,可组合使用(1+4+8=13)
+ - auto_captcha: 自动验证码识别,建议保持true
+ - login_attempt: 登录尝试次数,默认3次
+ - driver_type: 浏览器类型(edge/chrome/firefox)
+ - driver_path: 驱动文件路径
+ - headless: 无头模式,false会显示浏览器窗口
+
+
+
+
+
用户配置文件
+
users.json文件包含用户账号和预约信息:
+
+{
+ "users": [
+ {
+ "username": "您的学号",
+ "password": "您的密码",
+ "reserve_info": {
+ "date": "2025-10-30",
+ "start_time": "09:30",
+ "end_time": "17:00",
+ "place": "1",
+ "floor": "4",
+ "room": "5",
+ "seat_id": "31A",
+ "expect_duration": 6
+ }
+ }
+ ]
+}
+
+
+
参数说明
+
+ - username: 学号
+ - password: 密码
+ - date: 预约日期(格式:YYYY-MM-DD)
+ - start_time/end_time: 预约时间段
+ - place/floor/room: 图书馆位置信息
+ - seat_id: 座位编号(重要)
+ - expect_duration: 期望使用时长(小时)
+
+
+ 提示: 可以添加多个用户,工具会按顺序处理每个用户的预约请求。
+
+
+
+
+
+
+ 功能详解
+
+
+
⏰
+
自动预约(模式 +1)
+
当您当前没有有效预约时,工具会自动为您预约指定座位。
+
+ 适用场景: 提前预约第二天的座位
+
+
+
+
+
✅
+
自动签到(模式 +4)
+
如果您已有预约,且在可签到时间范围内,工具会自动完成签到。
+
+ 适用场景: 避免因忘记签到而失去座位
+
+
+
+
+
🔄
+
自动续约(模式 +8)
+
当您正在使用座位且到达可续约时间时,工具会自动延长使用时间。
+
+ 适用场景: 需要长时间使用座位的情况
+
+
+
+
+ 模式组合使用
+ 运行模式可以组合使用,只需将对应模式的数值相加:
+
+ - 自动预约 + 自动签到 + 自动续约 = 13(推荐)
+ - 自动预约 = 1
+ - 自动预约 + 自动签到 = 5
+
+
+
+
+ 故障排除
+ 常见问题及解决方法
+
+
+
工具启动时报错"无法找到驱动"
+
+
这是因为浏览器驱动未正确安装或版本不匹配。
+
+ - 检查驱动文件是否放置在正确位置
+ - 确认驱动版本与浏览器版本完全匹配
+ - 尝试重新下载并安装驱动
+
+
+
+
+
+
登录失败,提示账号密码错误
+
+
请检查users.json文件中的账号密码是否正确。
+
+ - 确认学号和密码无误
+ - 检查是否有特殊字符需要转义
+ - 尝试手动登录图书馆系统确认账号可用
+
+
+
+
+
+
预约失败,提示座位不可用
+
+
目标座位可能已被他人预约或不在可预约时间。
+
+ - 确认座位编号是否正确
+ - 检查预约时间是否符合图书馆规定
+ - 尝试预约其他座位或调整预约时间
+
+
+
+
+
+
+ 常见问题
+
+
+
使用AutoLibrary是否安全?
+
+
AutoLibrary完全模拟人工操作,不干扰图书馆系统正常运行。工具不会收集或上传您的个人信息,所有数据仅保存在本地配置文件中。
+
+
+
+
+
可以同时预约多个座位吗?
+
+
根据图书馆规定,每个账号同一时间段只能预约一个座位。但您可以在users.json中添加多个账号,工具会依次处理每个账号的预约请求。
+
+
+
+
+
工具运行期间可以操作电脑吗?
+
+
可以正常使用电脑,但请勿操作工具自动打开的浏览器窗口,否则可能会干扰工具的正常运行。
+
+
+
+
+
+ 下载安装
+
+
获取AutoLibrary
+
点击下方按钮下载最新版本的AutoLibrary工具包:
+
下载 AutoLibrary v0.01
+
+
文件大小:约15MB
+
系统要求:Windows 10/11,支持Edge/Chrome/Firefox浏览器
+
+
+
+ 安装步骤
+
+ - 下载压缩包并解压到任意文件夹
+ - 根据您使用的浏览器下载对应版本的驱动
+ - 将驱动文件放置到工具文件夹中
+ - 按照本手册说明编辑配置文件
+ - 双击main.exe运行工具
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/driver/readme.md b/driver/readme.md
new file mode 100644
index 0000000..c077df4
--- /dev/null
+++ b/driver/readme.md
@@ -0,0 +1 @@
+This folder is used to store the browser driver by selenium.
\ No newline at end of file
diff --git a/gui/ALConfigWidget.py b/gui/ALConfigWidget.py
new file mode 100644
index 0000000..f03e875
--- /dev/null
+++ b/gui/ALConfigWidget.py
@@ -0,0 +1,800 @@
+# -*- 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 sys
+
+from PySide6.QtCore import (
+ Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
+)
+from PySide6.QtWidgets import (
+ QWidget, QLineEdit, QMessageBox, QFileDialog, QListWidgetItem
+)
+from PySide6.QtGui import QCloseEvent
+
+from .Ui_ALConfigWidget import Ui_ALConfigWidget
+
+from ConfigReader import ConfigReader
+from ConfigWriter import ConfigWriter
+
+
+class ALConfigWidget(QWidget, Ui_ALConfigWidget):
+
+ configWidgetCloseSingal = Signal(dict)
+
+ def __init__(
+ 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"))}",
+ }
+ ):
+
+ 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)
+
+
+ def modifyUi(
+ self
+ ):
+
+ self.initlizeFloorRoomMap()
+ self.initilizeUserInfoWidget()
+
+
+ def connectSignals(
+ self
+ ):
+
+ self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
+ self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
+ self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
+ self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
+ self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
+ self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
+ self.BrowseCurrentSystemConfigButton.clicked.connect(self.onBrowseCurrentSystemConfigButtonClicked)
+ self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
+ self.BrowseExportSystemConfigButton.clicked.connect(self.onBrowseExportSystemConfigButtonClicked)
+ self.BrowseExportUserConfigButton.clicked.connect(self.onBrowseExportUserConfigButtonClicked)
+ self.ExportConfigButton.clicked.connect(self.onExportConfigButtonClicked)
+ self.NewConfigButton.clicked.connect(self.onNewConfigButtonClicked)
+ self.LoadConfigButton.clicked.connect(self.onLoadConfigButtonClicked)
+ self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
+ self.CancelButton.clicked.connect(self.onCancelButtonClicked)
+
+
+ def closeEvent(
+ self,
+ event: QCloseEvent
+ ):
+
+ self.configWidgetCloseSingal.emit(self.__config_paths)
+ super().closeEvent(event)
+
+
+ def initlizeFloorRoomMap(
+ self
+ ):
+
+ self.__floor_map = {
+ "2": "二层",
+ "3": "三层",
+ "4": "四层",
+ "5": "五层"
+ }
+ self.__room_map = {
+ "1": "二层内环",
+ "2": "二层外环",
+ "3": "三层内环",
+ "4": "三层外环",
+ "5": "四层内环",
+ "6": "四层外环",
+ "7": "四层期刊区",
+ "8": "五层考研"
+ }
+ self.__floor_rmap = {
+ v: k for k, v in self.__floor_map.items()
+ }
+ self.__room_rmap = {
+ v: k for k, v in self.__room_map.items()
+ }
+ self.__floor_room_map = {
+ "二层": ["二层内环", "二层外环"],
+ "三层": ["三层内环", "三层外环"],
+ "四层": ["四层内环", "四层外环", "四层期刊区"],
+ "五层": ["五层考研"]
+ }
+
+
+ def initlizeDefaultConfigPaths(
+ self
+ ) -> dict:
+
+ script_path = sys.executable
+ script_dir = QFileInfo(script_path).absoluteDir()
+ return {
+ "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,
+ config_data: dict
+ ):
+
+ if which == "system":
+ self.setSystemConfigToWidget(config_data)
+ self.CurrentSystemConfigEdit.setText(self.__config_paths["system"])
+ elif which == "users":
+ self.initilizeUserInfoWidget()
+ self.fillUsersList(config_data)
+ self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
+
+
+ def defaultSystemConfig(
+ self
+ ) -> dict:
+
+ return {
+ "library": {
+ "host_url": "http://10.1.20.7",
+ "login_url": "/login"
+ },
+ "login": {
+ "auto_captcha": True,
+ "max_attempt": 3
+ },
+ "web_driver": {
+ "driver_type": "edge",
+ "driver_path": "msedgedriver.exe",
+ "headless": False
+ },
+ "mode": {
+ "run_mode": 1
+ }
+ }
+
+
+ def defaultUsersConfig(
+ self
+ ) -> dict:
+
+ return {
+ "users": []
+ }
+
+
+ def collectSystemConfigFromWidget(
+ self
+ ) -> dict:
+
+ system_config = self.defaultSystemConfig()
+ # library config is never changed
+ system_config["login"]["auto_captcha"] = self.AutoCaptchaCheckBox.isChecked()
+ system_config["login"]["max_attempt"] = self.LoginAttemptSpinBox.value()
+ system_config["web_driver"]["driver_type"] = self.BrowserTypeComboBox.currentText()
+ system_config["web_driver"]["driver_path"] = self.BrowseBrowserDriverEdit.text()
+ system_config["web_driver"]["headless"] = self.HeadlessCheckBox.isChecked()
+ run_mode = 0
+ if self.AutoReserveCheckBox.isChecked():
+ run_mode |= 0x01
+ if self.AutoCheckinCheckBox.isChecked():
+ run_mode |= 0x02
+ if self.AutoRenewalCheckBox.isChecked():
+ run_mode |= 0x04
+ system_config["mode"]["run_mode"] = run_mode
+ return system_config
+
+
+ def setSystemConfigToWidget(
+ self,
+ system_config: dict
+ ):
+
+ self.HostUrlEdit.setText(system_config["library"]["host_url"])
+ self.LoginUrlEdit.setText(system_config["library"]["login_url"])
+ self.AutoCaptchaCheckBox.setChecked(system_config["login"]["auto_captcha"])
+ self.LoginAttemptSpinBox.setValue(system_config["login"]["max_attempt"])
+ self.BrowserTypeComboBox.setCurrentText(system_config["web_driver"]["driver_type"])
+ driver_path = os.path.abspath(system_config["web_driver"]["driver_path"])
+ self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
+ self.HeadlessCheckBox.setChecked(system_config["web_driver"]["headless"])
+ run_mode = system_config["mode"]["run_mode"]
+ self.AutoReserveCheckBox.setChecked(run_mode&0x01)
+ self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
+ self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
+
+
+ def initilizeUserInfoWidget(
+ self
+ ):
+
+ self.UsernameEdit.setText("")
+ self.PasswordEdit.setText("")
+ self.UserListWidget.setSortingEnabled(True)
+ self.PasswordEdit.setEchoMode(QLineEdit.Password)
+ self.ShowPasswordCheckBox.setChecked(False)
+ self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
+ self.FloorComboBox.setCurrentIndex(0)
+ self.DateEdit.setDate(QDate.currentDate())
+ self.DateEdit.setMinimumDate(QDate.currentDate())
+ self.DateEdit.setMaximumDate(QDate.currentDate())
+ if QTime.currentTime() > QTime(19, 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.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
+ self.PreferLateEndTimeCheckBox.setChecked(False)
+ self.MaxEndTimeDiffSpinBox.setValue(10)
+ self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
+ self.SatisfyDurationCheckBox.setChecked(False)
+
+
+ def collectUserConfigFromUserInfoWidget(
+ self
+ ) -> dict:
+
+ user_config = {
+ "username": self.UsernameEdit.text(),
+ "password": self.PasswordEdit.text(),
+ "reserve_info": {
+ "begin_time":{},
+ "end_time": {}
+ }
+ }
+ user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
+ user_config["reserve_info"]["place"] = self.PlaceComboBox.currentText()
+ user_config["reserve_info"]["floor"] = self.__floor_rmap[self.FloorComboBox.currentText()]
+ user_config["reserve_info"]["room"] = self.__room_rmap[self.RoomComboBox.currentText()]
+ user_config["reserve_info"]["seat_id"] = self.SeatIDEdit.text()
+ user_config["reserve_info"]["begin_time"]["time"] = self.BeginTimeEdit.time().toString("HH:mm")
+ user_config["reserve_info"]["begin_time"]["max_diff"] = self.MaxBeginTimeDiffSpinBox.value()
+ user_config["reserve_info"]["begin_time"]["prefer_early"] = self.PreferEarlyBeginTimeCheckBox.isChecked()
+ user_config["reserve_info"]["end_time"]["time"] = self.EndTimeEdit.time().toString("HH:mm")
+ user_config["reserve_info"]["end_time"]["max_diff"] = self.MaxEndTimeDiffSpinBox.value()
+ 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()
+ return user_config
+
+
+ def collectUserConfigFromUserListWidget(
+ self,
+ index: int
+ ) -> dict:
+
+ user_config = self.defaultUsersConfig()
+ if index < 0 or index >= self.UserListWidget.count():
+ return user_config
+ user_item = self.UserListWidget.item(index)
+ if user_item:
+ user_config = user_item.data(Qt.UserRole)
+ return user_config
+
+
+ def setUserConfigToWidget(
+ self,
+ user_config: dict
+ ) -> None:
+
+ try:
+ self.UsernameEdit.setText(user_config["username"])
+ self.PasswordEdit.setText(user_config["password"])
+ self.DateEdit.setDate(QDate.fromString(user_config["reserve_info"]["date"], "yyyy-MM-dd"))
+ self.PlaceComboBox.setCurrentText(user_config["reserve_info"]["place"])
+ self.FloorComboBox.setCurrentText(self.__floor_map[user_config["reserve_info"]["floor"]])
+ self.RoomComboBox.setCurrentText(self.__room_map[user_config["reserve_info"]["room"]])
+ self.SeatIDEdit.setText(user_config["reserve_info"]["seat_id"])
+ self.BeginTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["begin_time"]["time"], "H:mm"))
+ self.MaxBeginTimeDiffSpinBox.setValue(user_config["reserve_info"]["begin_time"]["max_diff"])
+ self.PreferEarlyBeginTimeCheckBox.setChecked(user_config["reserve_info"]["begin_time"]["prefer_early"])
+ self.EndTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["end_time"]["time"], "H:mm"))
+ self.MaxEndTimeDiffSpinBox.setValue(user_config["reserve_info"]["end_time"]["max_diff"])
+ 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"])
+ except:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ "用户配置文件读取发生错误 !\n"\
+ f"用户: {user_config['username']} 配置文件可能已损坏"
+ )
+
+
+ def loadSystemConfig(
+ self,
+ system_config_path: str
+ ) -> dict:
+
+ try:
+ if not system_config_path or not os.path.exists(system_config_path):
+ raise Exception("文件路径不存在")
+ system_config = ConfigReader(system_config_path).getConfigs()
+ if system_config and "library" in system_config\
+ and "web_driver" in system_config\
+ and "login" in system_config:
+ return system_config
+ return None
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ f"系统配置文件读取发生错误 ! : {e}\n"\
+ f"文件路径: {system_config_path}"
+ )
+ return None
+
+
+ def saveSystemConfig(
+ self,
+ system_config_path: str,
+ system_config_data: dict
+ ) -> bool:
+
+ try:
+ if not system_config_path:
+ raise Exception("文件路径为空")
+ if not system_config_data or not isinstance(system_config_data, dict):
+ raise Exception("系统配置数据为空或类型错误")
+ ConfigWriter(system_config_path, system_config_data)
+ return True
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ f"配置文件写入发生错误 ! : {e}\n"\
+ f"文件路径: {system_config_path}"
+ )
+ return False
+
+
+ def loadUsersConfig(
+ self,
+ users_config_path: str
+ ) -> dict:
+
+ try:
+ if not users_config_path or not os.path.exists(users_config_path):
+ raise Exception("文件路径不存在")
+ users_config = ConfigReader(users_config_path).getConfigs()
+ if users_config and "users" in users_config:
+ return users_config
+ return None
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ f"用户配置文件读取发生错误 ! : {e}\n"\
+ f"文件路径: {users_config_path}"
+ )
+ return None
+
+
+ def saveUsersConfig(
+ self,
+ users_config_path: str,
+ users_config_data: dict
+ ) -> bool:
+
+ try:
+ if not users_config_path:
+ raise Exception("文件路径为空")
+ if not users_config_data or not isinstance(users_config_data, dict):
+ raise Exception("用户配置数据为空或类型错误")
+ ConfigWriter(users_config_path, users_config_data)
+ return True
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ f"用户配置文件写入发生错误 ! : {e}\n"\
+ f"文件路径: \n{users_config_path}"
+ )
+ return False
+
+
+ def saveConfigs(
+ self,
+ system_config_path: str,
+ users_config_path: str
+ ) -> bool:
+
+ if users_config_path:
+ self.__users_config_data = 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)
+ if not self.saveUsersConfig(
+ users_config_path,
+ self.__users_config_data
+ ):
+ return False
+ if system_config_path:
+ self.__system_config_data = self.collectSystemConfigFromWidget()
+ if not self.saveSystemConfig(
+ system_config_path,
+ self.__system_config_data
+ ):
+ return False
+ return True
+
+
+ def loadConfig(
+ self,
+ config_path: str
+ ) -> bool:
+
+ if not config_path:
+ config_path = QFileDialog.getOpenFileName(
+ self,
+ "选择配置文件 - AutoLibrary",
+ f"{QDir.toNativeSeparators(QDir.currentPath())}",
+ "JSON 文件 (*.json);;所有文件 (*)"
+ )[0]
+ if not config_path:
+ return False
+ try:
+ system_config = self.loadSystemConfig(config_path)
+ users_config = self.loadUsersConfig(config_path)
+ if system_config is not None:
+ self.__config_paths["system"] = config_path
+ self.__system_config_data.update(system_config)
+ self.setSystemConfigToWidget(self.__system_config_data)
+ self.CurrentSystemConfigEdit.setText(config_path)
+ return True
+ if users_config is not None:
+ self.__config_paths["users"] = config_path
+ self.__users_config_data.update(users_config)
+ self.fillUsersList(self.__users_config_data)
+ self.CurrentUserConfigEdit.setText(config_path)
+ return True
+ except:
+ return False
+
+
+ def fillUsersList(
+ self,
+ users_config_data: list[dict]
+ ):
+
+ self.UserListWidget.clear()
+ if "users" in users_config_data:
+ for user in users_config_data["users"]:
+ user_item = QListWidgetItem(user["username"])
+ user_item.setData(Qt.UserRole, user)
+ self.UserListWidget.addItem(user_item)
+
+
+ def addUser(
+ self
+ ):
+
+ new_user = {
+ "username": f"新用户-{self.UserListWidget.count()}",
+ "password": "000000",
+ "reserve_info": {
+ "date": f"{QDate.currentDate().toString("yyyy-MM-dd")}",
+ "place": "\u56fe\u4e66\u9986",
+ "floor": "2",
+ "room": "1",
+ "seat_id": "",
+ "begin_time": {
+ "time": f"{QTime.currentTime().toString("hh:mm")}",
+ "max_diff": 0,
+ "prefer_early": False
+ },
+ "end_time": {
+ "time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
+ "max_diff": 0,
+ "prefer_early": True
+ },
+ "expect_duration": 2.0,
+ "satisfy_duration": False
+ }
+ }
+ user_item = QListWidgetItem(new_user["username"])
+ user_item.setData(Qt.UserRole, new_user)
+ self.UserListWidget.addItem(user_item)
+ self.UserListWidget.setCurrentItem(user_item)
+ self.setUserConfigToWidget(new_user)
+
+
+ def delUser(
+ self
+ ):
+
+ current_item = self.UserListWidget.currentItem()
+ if current_item:
+ self.UserListWidget.takeItem(self.UserListWidget.row(current_item))
+ self.UserListWidget.setCurrentItem(None)
+
+ @Slot()
+ def onShowPasswordCheckBoxChecked(
+ self,
+ checked: bool
+ ):
+
+ if checked:
+ self.PasswordEdit.setEchoMode(QLineEdit.Normal)
+ else:
+ self.PasswordEdit.setEchoMode(QLineEdit.Password)
+
+ @Slot()
+ def onFloorComboBoxCurrentIndexChanged(
+ self,
+ ):
+
+ floor = self.FloorComboBox.currentText()
+ self.RoomComboBox.clear()
+ self.RoomComboBox.addItems(self.__floor_room_map[floor])
+ self.RoomComboBox.setCurrentIndex(0)
+
+ @Slot()
+ def onUserListWidgetCurrentItemChanged(
+ self,
+ current: QListWidgetItem,
+ previous: QListWidgetItem
+ ):
+ # dont care about the 'self.__users_config_data', we already
+ # cant effectively update the data of each user, due to the
+ # possiblity of frequency edit. we just let the QListWidget
+ # help us.
+ if not current:
+ self.initilizeUserInfoWidget()
+ return
+ if previous:
+ user = self.collectUserConfigFromUserInfoWidget()
+ if user:
+ previous.setText(user["username"])
+ previous.setData(Qt.UserRole, user)
+ user = current.data(Qt.UserRole)
+ if user:
+ self.setUserConfigToWidget(user)
+
+ @Slot()
+ def onAddUserButtonClicked(
+ self
+ ):
+
+ self.addUser()
+
+ @Slot()
+ def onDelUserButtonClicked(
+ self
+ ):
+
+ self.delUser()
+
+ @Slot()
+ def onBrowseBrowserDriverButtonClicked(
+ self
+ ):
+
+ browser_driver_path = QFileDialog.getOpenFileName(
+ self,
+ "选择浏览器驱动 - AutoLibrary",
+ self.CurrentSystemConfigEdit.text(),
+ "可执行文件 (*.exe);;所有文件 (*)"
+ )[0]
+ if browser_driver_path:
+ self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
+
+ @Slot()
+ def onBrowseCurrentSystemConfigButtonClicked(
+ self
+ ):
+
+ system_config_path = QFileDialog.getOpenFileName(
+ self,
+ "选择其它的系统配置 - AutoLibrary",
+ self.CurrentSystemConfigEdit.text(),
+ "JSON 文件 (*.json);;所有文件 (*)"
+ )[0]
+ if system_config_path:
+ system_config_path = QDir.toNativeSeparators(system_config_path)
+ self.loadConfig(system_config_path)
+
+ @Slot()
+ def onBrowseCurrentUserConfigButtonClicked(
+ self
+ ):
+
+ users_config_path = QFileDialog.getOpenFileName(
+ self,
+ "选择其它的用户配置 - AutoLibrary",
+ self.CurrentUserConfigEdit.text(),
+ "JSON 文件 (*.json);;所有文件 (*)"
+ )[0]
+ if users_config_path:
+ users_config_path = QDir.toNativeSeparators(users_config_path)
+ self.loadConfig(users_config_path)
+
+ @Slot()
+ def onBrowseExportSystemConfigButtonClicked(
+ self
+ ):
+
+ system_config_path = QFileDialog.getSaveFileName(
+ self,
+ "导出系统配置 - AutoLibrary",
+ self.CurrentSystemConfigEdit.text(),
+ "JSON 文件 (*.json);;所有文件 (*)"
+ )[0]
+ if system_config_path:
+ self.ExportSystemConfigEdit.setText(QDir.toNativeSeparators(system_config_path))
+
+ @Slot()
+ def onBrowseExportUserConfigButtonClicked(
+ self
+ ):
+
+ users_config_path = QFileDialog.getSaveFileName(
+ self,
+ "导出用户配置 - AutoLibrary",
+ self.CurrentUserConfigEdit.text(),
+ "JSON 文件 (*.json);;所有文件 (*)"
+ )[0]
+ if users_config_path:
+ self.ExportUserConfigEdit.setText(QDir.toNativeSeparators(users_config_path))
+
+ @Slot()
+ def onExportConfigButtonClicked(
+ self
+ ):
+
+ msg = ""
+
+ system_config_path = self.ExportSystemConfigEdit.text()
+ users_config_path = self.ExportUserConfigEdit.text()
+ if system_config_path:
+ if self.saveConfigs(
+ system_config_path,
+ users_config_path=""
+ ):
+ msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
+ if users_config_path:
+ if self.saveConfigs(
+ "", users_config_path
+ ):
+ msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
+ if msg:
+ QMessageBox.information(
+ self,
+ "信息 - AutoLibrary",
+ msg
+ )
+
+ @Slot()
+ def onLoadConfigButtonClicked(
+ self
+ ):
+
+ self.loadConfig("")
+
+ @Slot()
+ def onNewConfigButtonClicked(
+ self
+ ):
+
+ file_path = self.CurrentSystemConfigEdit.text()
+ folder_dir = QFileDialog.getExistingDirectory(
+ self,
+ "选择新建配置的文件夹 - AutoLibrary",
+ QDir.toNativeSeparators(QFileInfo(os.path.abspath(file_path)).absoluteDir().path())
+ )
+ if not folder_dir:
+ return
+ system_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "system.json"))
+ users_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "users.json"))
+ system_exists = os.path.isfile(system_config_path)
+ users_exists = os.path.isfile(users_config_path)
+ if system_exists or users_exists:
+ exist_files = []
+ if system_exists:
+ exist_files.append(system_config_path)
+ if users_exists:
+ exist_files.append(users_config_path)
+ reply = QMessageBox.information(
+ self,
+ "信息 - 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_paths = {
+ "system": system_config_path,
+ "users": users_config_path
+ }
+ self.initlizeConfigToWidget("system", self.__system_config_data)
+ self.initlizeConfigToWidget("users", self.__users_config_data)
+
+ @Slot()
+ def onConfirmButtonClicked(
+ self
+ ):
+
+ if self.UserListWidget.currentItem() is not None:
+ user = self.collectUserConfigFromUserInfoWidget()
+ if user:
+ self.UserListWidget.currentItem().setData(Qt.UserRole, user)
+ if self.saveConfigs(
+ self.__config_paths["system"],
+ self.__config_paths["users"]
+ ):
+ QMessageBox.information(
+ self,
+ "信息 - AutoLibrary",
+ "配置文件保存成功 !\n"
+ f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
+ f"用户配置文件路径: \n{self.__config_paths['users']}"
+ )
+ else:
+ QMessageBox.warning(
+ self,
+ "警告 - AutoLibrary",
+ "配置文件保存失败, 请检查文件路径权限"
+ )
+ self.close()
+
+ @Slot()
+ def onCancelButtonClicked(
+ self
+ ):
+
+ self.close()
diff --git a/gui/ALConfigWidget.ui b/gui/ALConfigWidget.ui
new file mode 100644
index 0000000..3f738d0
--- /dev/null
+++ b/gui/ALConfigWidget.ui
@@ -0,0 +1,1790 @@
+
+
+ ALConfigWidget
+
+
+
+ 0
+ 0
+ 520
+ 700
+
+
+
+
+ 520
+ 700
+
+
+
+
+ 520
+ 700
+
+
+
+ 编辑配置 - AutoLibrary
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+ -
+
+
+
+ 35
+ 110
+
+
+
+
+ 1313
+ 16777215
+
+
+
+ 0
+
+
+ true
+
+
+
+ 用户设置
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
-
+
+
+
+ 260
+ 16777215
+
+
+
+ 用户列表
+
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 250
+ 16777215
+
+
+
+ false
+
+
+ QListView::ViewMode::ListMode
+
+
+ -1
+
+
+ false
+
+
+
+ -
+
+
-
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 删除用户
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 添加用户
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 16777215
+ 16777215
+
+
+
+ 用户信息
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 100
+
+
+
+ 登录信息
+
+
+ false
+
+
+ false
+
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 5
+
+
-
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 用户名:
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 显示
+
+
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 密码:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+
+
+ 25
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 470
+
+
+
+
+ 16777215
+ 480
+
+
+
+ 预约信息
+
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+ -
+
+
+
+ 100
+ 30
+
+
+
+
+ 100
+ 30
+
+
+
+ 结束时间:
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
-
+
+ 图书馆
+
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 期望时长:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+
+
+
+
+
+ H:mm
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 日期:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+ 2000
+ 1
+ 1
+
+
+
+
+
+
+
+
+
+ H:mm
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 优先选择最早
+
+
+ true
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 最大时长偏差:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 120
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 区域:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
-
+
+ 二层
+
+
+ -
+
+ 三层
+
+
+ -
+
+ 四层
+
+
+ -
+
+ 五层
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 120
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 优先选择最晚
+
+
+ true
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 开始时间:
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 楼层:
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 座位号:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 8.000000000000000
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+ 2025
+ 11
+ 1
+
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 最大时长偏差:
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 地点:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 优先满足时长要求
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 系统设置
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+ -
+
+
+ 登录设置
+
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 自动识别验证码
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 登录尝试次数:
+
+
+
+ -
+
+
+
+ 50
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 1
+
+
+ 10
+
+
+ 5
+
+
+
+
+
+
+ -
+
+
+ 浏览器设置
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 浏览器类型:
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ edge
+
+
+ 0
+
+
+ 3
+
+
+ 3
+
+
-
+
+ edge
+
+
+ -
+
+ chrome
+
+
+ -
+
+ firefox
+
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 驱动路径:
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 265
+ 25
+
+
+
+
+ 260
+ 25
+
+
+
+
+ -
+
+
+
+ 35
+ 25
+
+
+
+
+ 35
+ 25
+
+
+
+ ...
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 无头模式
+
+
+
+
+
+
+ -
+
+
+ 图书馆设置
+
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 登录URL:
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ http://10.1.20.7
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 主机URL:
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ /login
+
+
+
+
+
+
+ -
+
+
+ 运行模式
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 自动预约
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 自动签到
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 自动续约
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 270
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+
+
+
+
+
+ 其它
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+ -
+
+
+ 当前配置
+
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 当前系统配置路径:
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 当前用户配置路径:
+
+
+
+ -
+
+
+
+ 35
+ 25
+
+
+
+
+ 35
+ 25
+
+
+
+ ;;;
+
+
+
+ -
+
+
+
+ 35
+ 25
+
+
+
+
+ 35
+ 25
+
+
+
+ ...
+
+
+
+
+
+
+ -
+
+
+ 导出路径
+
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 系统配置导出路径:
+
+
+
+ -
+
+
+
+ 35
+ 25
+
+
+
+
+ 35
+ 25
+
+
+
+ ...
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 25
+
+
+
+ 用户配置导出路径:
+
+
+
+ -
+
+
+
+ 35
+ 25
+
+
+
+
+ 35
+ 25
+
+
+
+ ...
+
+
+
+ -
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 导出配置文件
+
+
+
+
+ ExportUserConfigEdit
+ ExportSystemConfigLabel
+ BrowseExportUserConfigButton
+ ExportUserConfigLabel
+ BrowseExportSystemConfigButton
+ ExportConfigButton
+ ExportSystemConfigEdit
+
+
+ -
+
+
+
+ 0
+ 200
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+
+
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 取消
+
+
+
+ -
+
+
+
+ 1280
+ 16777215
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 新建配置
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 载入配置
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 确认
+
+
+
+
+
+
+
+
+
+
diff --git a/gui/ALMainWindow.py b/gui/ALMainWindow.py
new file mode 100644
index 0000000..eb9ead9
--- /dev/null
+++ b/gui/ALMainWindow.py
@@ -0,0 +1,310 @@
+# -*- 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 sys
+import time
+import queue
+
+from PySide6.QtCore import (
+ Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
+)
+from PySide6.QtWidgets import (
+ QMainWindow, QMenu
+)
+from PySide6.QtGui import (
+ QTextCursor, QCloseEvent, QFont, QIcon
+)
+
+from .Ui_ALMainWindow import Ui_ALMainWindow
+from .ALConfigWidget import ALConfigWidget
+
+from . import AutoLibraryResource
+
+from AutoLib import AutoLib
+from ConfigReader import ConfigReader
+
+
+class AutoLibWorker(QThread):
+
+ finishedSignal = Signal()
+ showTraceSignal = Signal(str)
+ showMsgSignal = Signal(str)
+
+ def __init__(
+ self,
+ input_queue: queue.Queue,
+ output_queue: queue.Queue,
+ config_paths: dict
+ ):
+
+ super().__init__()
+
+ self.__input_queue = input_queue
+ self.__output_queue = output_queue
+ self.__config_paths = config_paths
+ self.__stopped = False
+
+
+ def checkTimeAvailable(
+ self,
+ ) -> bool:
+
+ current_time = time.strftime("%H:%M", time.localtime())
+ if current_time >= "23:30" or current_time <= "07:30":
+ return False
+ return True
+
+
+ def checkConfigPaths(
+ self,
+ ) -> bool:
+
+ if not all(
+ os.path.exists(path) for path in self.__config_paths.values()
+ ):
+ self.showTraceSignal.emit(
+ "配置文件路径不存在,请检查配置文件路径是否正确。"
+ )
+ return False
+ return True
+
+
+ def run(
+ self
+ ):
+
+ try:
+ if not self.checkTimeAvailable():
+ self.showTraceSignal.emit(
+ "当前时间不在图书馆开放时间内。\n"\
+ " 请在 07:30 - 23:30 之间尝试"
+ )
+ return
+ if not self.checkConfigPaths():
+ return
+ self.showTraceSignal.emit("AutoLibrary 开始运行")
+ auto_lib = AutoLib(
+ self.__input_queue,
+ self.__output_queue,
+ )
+ auto_lib.run(
+ ConfigReader(self.__config_paths["system"]),
+ ConfigReader(self.__config_paths["users"]),
+ )
+ auto_lib.close()
+ self.showTraceSignal.emit("AutoLibrary 运行结束")
+ except Exception as e:
+ self.showTraceSignal.emit(
+ f"AutoLibrary 运行时发生异常:{e}"
+ )
+ finally:
+ self.finishedSignal.emit()
+
+
+ def stop(
+ self
+ ):
+
+ self.__stopped = True
+
+
+class ALMainWindow(QMainWindow, Ui_ALMainWindow):
+
+ def __init__(
+ self
+ ):
+
+ super().__init__()
+ self.__class_name = self.__class__.__name__
+
+ self.setupUi(self)
+ self.__input_queue = queue.Queue()
+ self.__output_queue = queue.Queue()
+ self.__auto_lib = AutoLib(
+ self.__input_queue,
+ self.__output_queue,
+ )
+ 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"))}",
+ }
+ self.__alConfigWidget = None
+ self.__auto_lib_thread = None
+
+ self.modifyUi()
+ self.connectSignals()
+ self.startMsgPolling()
+
+
+ def modifyUi(
+ self
+ ):
+
+ icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
+ self.setWindowIcon(icon)
+ self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
+
+
+ def connectSignals(
+ self
+ ):
+
+ self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
+ self.StartButton.clicked.connect(self.onStartButtonClicked)
+ self.StopButton.clicked.connect(self.onStopButtonClicked)
+ self.SendButton.clicked.connect(self.onSendButtonClicked)
+ self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
+
+
+ def closeEvent(
+ self,
+ event: QCloseEvent,
+ ):
+
+ if self.__timer and self.__timer.isActive():
+ self.__timer.stop()
+ if self.__auto_lib:
+ self.__auto_lib.close()
+ if self.__alConfigWidget:
+ self.__alConfigWidget.close()
+ super().closeEvent(event)
+
+
+ def appendToTextEdit(
+ self,
+ text: str,
+ ):
+
+ cursor = self.MessageIOTextEdit.textCursor()
+ cursor.movePosition(QTextCursor.End)
+ cursor.insertText(text + "\n")
+ self.MessageIOTextEdit.setTextCursor(cursor)
+ self.MessageIOTextEdit.ensureCursorVisible()
+ scrollbar = self.MessageIOTextEdit.verticalScrollBar()
+ scrollbar.setValue(scrollbar.maximum())
+
+
+ def startMsgPolling(
+ self
+ ):
+
+ self.__timer = QTimer()
+ self.__timer.timeout.connect(self.pollMsgQueue)
+ self.__timer.start(100)
+
+
+ def setControlButtons(
+ self,
+ config_button_enabled: bool,
+ start_button_enabled: bool,
+ stop_button_enabled: bool,
+ ):
+
+ self.ConfigButton.setEnabled(config_button_enabled)
+ self.StartButton.setEnabled(start_button_enabled)
+ self.StopButton.setEnabled(stop_button_enabled)
+
+ @Slot()
+ def showMsg(
+ self,
+ msg: str,
+ ):
+
+ self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
+
+ @Slot()
+ def showTrace(
+ self,
+ msg: str,
+ ):
+
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+ self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
+
+ @Slot()
+ def pollMsgQueue(
+ self,
+ ):
+
+ try:
+ while True:
+ msg = self.__output_queue.get_nowait()
+ self.appendToTextEdit(msg)
+ except queue.Empty:
+ pass
+
+ @Slot(dict)
+ def onConfigWidgetClosed(
+ self,
+ config_paths: dict,
+ ):
+
+ self.__alConfigWidget = None
+ self.ConfigButton.setEnabled(True)
+ self.StartButton.setEnabled(True)
+ self.StopButton.setEnabled(False)
+ self.__config_paths = config_paths
+
+ @Slot()
+ def onConfigButtonClicked(
+ self,
+ ):
+
+ if self.__alConfigWidget is None:
+ self.__alConfigWidget = ALConfigWidget(
+ self,
+ self.__config_paths
+ )
+ self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
+ self.__alConfigWidget.setWindowFlags(Qt.Window)
+ self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
+ self.__alConfigWidget.show()
+ self.__alConfigWidget.raise_()
+ self.__alConfigWidget.activateWindow()
+ self.ConfigButton.setEnabled(False)
+
+ @Slot()
+ def onStartButtonClicked(
+ self,
+ ):
+
+ self.setControlButtons(False, False, True)
+ self.__auto_lib_thread = AutoLibWorker(
+ self.__input_queue,
+ self.__output_queue,
+ self.__config_paths,
+ )
+ self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
+ self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
+ self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
+ self.__auto_lib_thread.start()
+
+ @Slot()
+ def onStopButtonClicked(
+ self
+ ):
+
+ if self.__auto_lib_thread and self.__auto_lib_thread.isRunning():
+ self.__auto_lib_thread.stop()
+ self.showTrace("正在停止操作......")
+ self.setControlButtons(True, True, False)
+
+ @Slot()
+ def onSendButtonClicked(
+ self
+ ):
+
+ msg = self.MessageEdit.text().strip()
+ if not msg:
+ return
+ self.showMsg(msg)
+ self.MessageEdit.clear()
\ No newline at end of file
diff --git a/gui/ALMainWindow.ui b/gui/ALMainWindow.ui
new file mode 100644
index 0000000..56f3526
--- /dev/null
+++ b/gui/ALMainWindow.ui
@@ -0,0 +1,264 @@
+
+
+ ALMainWindow
+
+
+
+ 0
+ 0
+ 540
+ 300
+
+
+
+
+ 540
+ 300
+
+
+
+
+ 1280
+ 720
+
+
+
+ AutoLibrary
+
+
+ true
+
+
+
+
+ 5
+
+
+ 3
+
+
+ 0
+
+
+ 3
+
+
+ 0
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 1280
+ 0
+
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+
+ -
+
+
+
+ 80
+ 30
+
+
+
+
+ 80
+ 30
+
+
+
+ 配置
+
+
+
+ -
+
+
+ false
+
+
+
+ 80
+ 30
+
+
+
+
+ 80
+ 30
+
+
+
+ 停止脚本
+
+
+
+ -
+
+
+ true
+
+
+
+ 80
+ 30
+
+
+
+
+ 80
+ 30
+
+
+
+ background-color: rgb(10, 170, 10);
+font: 12pt "Microsoft YaHei UI";
+color: rgb(255, 255, 255);
+font: 9pt "Segoe UI";
+font: 700 9pt "Microsoft YaHei UI";
+
+
+ 启动脚本
+
+
+
+
+
+ -
+
+
+
+ 0
+ 175
+
+
+
+ QMenu::icon {
+ width: 0px;
+ height: 0px;
+}
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+ 0
+
+
+ false
+
+
+ false
+
+
+ QPlainTextEdit::LineWrapMode::NoWrap
+
+
+ false
+
+
+ Qt::TextInteractionFlag::LinksAccessibleByKeyboard|Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextBrowserInteraction|Qt::TextInteractionFlag::TextEditable|Qt::TextInteractionFlag::TextEditorInteraction|Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 30
+
+
+
+
+ 16777215
+ 30
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 80
+ 30
+
+
+
+
+ 80
+ 30
+
+
+
+ 发送
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
diff --git a/gui/AutoLibraryResource.qrc b/gui/AutoLibraryResource.qrc
new file mode 100644
index 0000000..ba03dfd
--- /dev/null
+++ b/gui/AutoLibraryResource.qrc
@@ -0,0 +1,8 @@
+
+
+ icons/AutoLibrary.ico
+
+
+ translators/qtbase_zh_CN.qm
+
+
diff --git a/gui/icons/AutoLibrary.ico b/gui/icons/AutoLibrary.ico
new file mode 100644
index 0000000000000000000000000000000000000000..e48e78f5886d9f39b2dbdf71b92e637c785117d9
GIT binary patch
literal 803706
zcmeFXcTiJN_$`VRDFT8>2SMpIp(T_cO+W-xq)9VCK&1DUfJpC3k=~>W0@8~Rr1#K!
zZ=tu)0wH<)%DeC0JM(7VyqWjk`{T?xd++tFv)A{1>rCe4guT&Jc}U7gN<>8TP)$|o
z9TCypo6EaI4{qO_4*mX)5fKr~!SvNEH8hC$Zl)d(-3qb(Pt2{G5#!B4M06`Qn27iW
zM{{#6@E;TM+KOswH)kIXc{LS94ymU;9Iv5fFk22O4giM$hoXbME6m>YjkAM=n>oz+
zW>!Q5000BT#6(0zL?lH(ViEv=sKk4K-b)>=8~lF*)s$|KmJ%Q!Si}Mf6A=?NgIPkw
z#h{ij2?GCMpNs}P0Tx(3(VOaW})a{0R!B``51A$_i}_8aYz|)JbkYS
zln{BNV#Fa}#GwmwcCmJ_zgYm>j1;WxEv)UWTyBPj|8t0ogQe?#!sTtP)Rg{{#qkr=
z<Y2*1pa55{}J+k(YV-&{%<6bK(M$aP)r11
zVJQX!nTtu7LnQzrP$*Os1O!NZJKyfHQ5_oe{
z;~Ob&+|0E9W2gU%4khjXO@)iy4gLoGUO`(4>I%KV|IhpV?{xepLg9bp@4r+aZlbiU
zt?Z$$ZqEOuUQ|R(LIel|h=^DM%|*>EL}B70FbNA03lXRoSV96S0g?n-0>Gl8AW=(z
zxhM!GX<;S~GdBmANtgj(ATtrLr6@o|Qrt}ZzgVE=FcI^c2!Mp8xQG~75+*8cX#oOA
zT8c?n0-zuf3sC?}0tSGIN&sQvFfo{pq1&T`oK;j~1
zfSb*m0nN<8mN1Yd3??E9HaE8fio?t-VNi3RafRAB-qZ;oDFGA#Nl1u`-W1|L
zcK<&b*@(j(Ao$+%HB8ISQqs&x2MD}rF$Cg&ah@a83hL_c|Hym)35ET?b|3Knci;c{
z;Q9aBePTDh5dlI0P^g)NB^V%X2@|&jfz4q6AQWsSZYBwn1d71S#3Utd35ldI@mzfB^sjAm|OL
zC=g&SZY~1009c5@B+NvCH=TIX-x4>>mjA!={#WNA5Ymcw?TLuK5UDB2zxPawzb0$o
zlKw16^mPQ22|M>NF3tF-F}z6ehn2@H^H`(t*I-zrDf^NB(-)gayc_{dxU%|P7+De;
z8T#uYuHFw{59D*v(XrS-`RR18L^^m9|wPN@Np4>a*Gd{?T<#@7k
zAq}I|kHpf4)T0`DH;M8wjS&2Il1x7&3LE3*pK&BDYlHSG8(GM7O*)ZVq(+Dg%4XUa
z512yaPH-uxUX&Le2Pfe6RhCvw9Qa?Gta1ms4O;aUtYK>pG^N_1YJKB59>|`yM;$7E
z-=R>Tu+!#s^dq&S9U0_>bKh)|6>C$L$vH?7pE$gyNMY(qk1E67?efA`f(B#Y-%Y(b
zv;WS+1y-gaka8y=jM^YtK?8)_na1WZ(;a+t+NS2V~?9+Vpx)eJF
z8r^Zm3M}`Ng|I|-eznHuDc(CYw0yPa)&r-a#II=eCB+S;_u0Po_tL{5;
zxYjCLA{m4d_Bn?UYuCjm;sSR=BvoU{>vsv$<
zT<>mB4BKca2|>7-$vyxn+@OI}^`sqLP~M4)x`XqYR>5)`B$~)|GYB_Q4cQzPUUT@E
zj76+rcnM&v&-8$n&*e|8-hH|L6pGLy}?$a_ebfI{LMIS4W+0={LR
zas*ZtBz5F>Fnu-dWOJ7n3+t3klsG^xsdWqr;{})MY^O
zn8Qv`yx4roV4Bree3nK`Z0gSEs7{Pj%BUq32KRH;Vdv75B5N<(_rfp<)paH7yVLPq
zvcM36R|J<#!8txue^t0Eg?r>uAQO;OvHvpj(yj@-4wgrCQ=5L`x+T>0;qe-L(8?SS
zXj0s|x4s-VnCWc)X~h|MvZ9hMA7Z-xx!105+&>nDRb^$ZT_qCF!Cv)+
zV}|2avtB#-(paJCl?$R(so6)}f`R)T!gY>OcaTe+Nd)GC+NW8_L$(umABo0r{OpFq
z%hm0{c8@#95|epY+(8C%Lk<6g^Cwmz@$*uc<8d}7s(9(y`owzg>B^*6{;JCc-s#SV
zg{MzRkQ+4~|Io^a@9Eyd{)_YSjhG;mwYw2&*`2CtdsBL@4vWvsVtv%XMp*u<2iO~A
z^4oxK&uTX9`huIo4rsWGd`I^QtKI}rL@9dEG^ed?(0fyEgV2q_4=oDA@!$N;4Zkqi
z<=}e;n>WvX5YIGf$%TKq=s5>Rk22tw*;Zx?5dJ32dEQ&;e&?OV>IZeD*Fhnd_S=ne
zgy}tEd=W+RUE)Bwxk@Z?gYm`TDSQc=6|+0<6uvUNI8KPYQaRT$(#C)Ze_72OcJFZz
zu8;)OD&b&Zc8q2Ul_}|BtILMOqf;9LG`yHYgXJR(4zIaogjQNiZu#cLeiiR|aL2+5
z<@D8#)XXE0=IEha8t(djd%)Kn+$Ws1{{78@o%Pdsg_g4Mf;(2nCr_qi^}!Q)N3#fJ
zBJQ<+tkMMA92~+WhM1+(BfA@e=-^ACcpE1O!zg8NNz>~ynqt&mOxPH|sbvEfm9gbT
zC^o(`$1Nn$ZNWDKh|CzsJD0dzSUWm~62>#2H>E4$Db9_bffu2;10l?Ue93
z&NFdc+NbueTFY4;?@l^U&p@tyJ|158JFM+CRrOrC&d@TGJ{9*>wNtgCn%%PW1Y#+?
zd%MU*1|vO!w-P1Tn}njwPdu*k@{k#+3nZ1B5)Sxuqic)X+WxegyzgtzuveVT5it^|
zy3O*Xmp=l`9(vUX()%?t42gl$PUuV_7j>)wBAXK@O5XOrCb~dTlyrVE2?L!`xeNbd
z0ogJqn@2g646)h_S(e&fJtV5Do}c8HK!na>)uGXc)E7Dc{A*^K`H5r#jM(2HXs%k;
z@AVcjh1?oLt{=Ygk49zDiTl=wiVy2Cg7j=%vO#va->?jMXy976=6}fn{(B^|*%5|x
zi>nt^cbK~x$j}6{xu%}IAJv-F9{zcKH+!>0f!A50$Zv&kmMy
z@Y3PJkf%!KnYcmJT|A^Qt)xkF!AG|KgxGm|ZTycH_$uSNykLj=rzkwVH|&M)Pqmt=
zJn+IxZh>#Hp2xJPRdXKvbEkSKjo5Lom|xM_rZScllVSR?8+G6V-=B>8*LfI^{m$C)J6*V3;$A~o%=LVRR(W%H
zBb%Y#ir4iYtZ*;oR$RE{BL+!WmbruRRXNim$u*IGbekYaQe|Ip)92i+DYBbqAMFW0
z+=x+Ee$uYKUw++ESjliw-M)dV!ZGWLR0_xQf>p
zE_oWh2X`($_M?;~EA7b<&O6=zQ~GqCPblqsM*gNt4>|(E^X;n5ZBJoHTvKMlAxVTi
z>K+1oDqnL%`Be|r*MiX;+XrxRZ#hRd^k7h0^Jb}JTvim}=Si{@YXsu5!IE0(-H|Th
z!qS8LW^y{UbF4;1ntOpq9+{aebiF68eXqlj^mQ}0<$ZI7e;StgvW39R6^G$v1>MVk
zxd5p1~o{f@vUY`pP7Y|{nLGu15Bk(=ef*E5J>34=B$A+@ssHc-DCT<<{^_R
z=DTw_wZ@@GGDrCQd3oc`7PjrvAamiSvx?`HeJTsU{<2VDqt{`k`*RH~pC+Q8MlqR0
z$e^89o9fXA;S^_cj!Dk<8sR6Zt2RE2K9ZJ~bB|&1iyYwTy~O52Hec1CD8;4+7#iJI
zi-@L}A?70>UCXvRarCLX#U;}}N3~V`A6qP2MgcL*=%t#;p>)*;o8>)pQ|SSec8Pk-
zxc)-9nB7|~KliMYeP!W=#wu(+cxA8r#p4T~^^=1K{VySO`0c(-1N}Y{TD8*SShV0@
z4m=y>Z*P{EtxGWf?I(fTFz7E$c$7>oVuqlyG@aU~997L;9KRsqiGV!H+q??Q+hB
zp{=!PQ;vAtepfz6rP!;H;>tJHC=>;6#Y7R1)V9j(Z&^5XV8_~@0Y?KP|JX_vP12#z
zEgnD9LaANs-CcR!5AV_?TYV#>rwYF1biQ0%=4v-ME
z`6&SWLGdS!LZ?Pssast-#au~2`kKD+ZQx10kCuXfFHG%`9X=JJVhPB+*YJo=-|r0t
z28HKig`S)*lN#UZRdI-$`jEYZ>IqIq2IFK-N;=$(PgIIRY1Vph1u+B-w$;ArG8^JS
zXLP>by|2w$hivw~`+TC(1o;dE1bO(#y~Fj54(St)h=#ACf_iCI4msH#02mXkekXfS
z894fcv+14kj+CqAJ@t2GF(yf#w|^S2Z5;PhEV+6O&1gGwV#S&$?nOpJQktJ)FD_nd
zS%esLtIoOvy9=ap(sL0~2t{{tb|z=b!s|HT&{N0y0iCrLZ~MB>&arm28NH82qAv00
z@^Xn<6K1ku$3+8J|ByHMj!=S<
zA3GA~zbEl^fWxYnj^%7_V~nm8($fKGHO>cjF}E7COMdJo9wu+$0`#Nf4(qS98VUcz
zGO0&^!qIo8HXU*Aa|6CBEW3nS{45yVVcVwoCs0YYvfhlQPeUJ=)r
z#udwMv~9m$xD|<{9W+
z_%`N+mOqq;OI_CcG*d}4z;)4ykq9C?8id?^Ty*}3+vn^EVfOqHohh_O5X1U}`6OCs
z{|pMw{ykMAradn*Lqjqx
z;0{uAdSjzOhWF}c%mz(&>M3UTHG+eS7z-Y$F!bEkvbSzKs>mh!Drep`H2mx6@~yx4jc?XuOVsUXjdnChH%krsyUpQCUXj+lPz&w$X=Jt)B5$+m@^2m2EOO
z(zZRJ8U6a@(0!UB6Tu<5x~drjha
z7{gPmb;~!)p4Q(m^9dybiBdto)V)5m<5*?9dhTGUADJk{@_OGtx_enHV@2J?>Sf5B
zY4YA~jgqY-LMfLD^vo+b@ip9XsajAO{fQM)%8sJ!E>$F6-s8OxoM~6g93Sa3=*bYW
ztVk+uhp(jBCR!o4%76QZj3w%CWROU*f|=`@2AbQ3Z}|zQ9q{cWHZ!>=BW~@xiTqwH
znrqj&TtQtmI$8fdd$im@mqQzamtt9~ZP%yU-AbDMKCa{y1^!$%CjqU9#M1A9AbK6%OC4+FoHb#b>xpi!Jne-
zb~H+z5f^#|r!TXGHoThC2T!w#C2Gj(uY)?m?DyJl-zk1ktNpBt9GvsvSd?K;%9SWX
zZq%#u0MojcS+dLAI{4OSgk)|L-L|;-$!~UGK@xHxZTf;R(l9}C_5&*_ck!ecb3e!%
z&(?T0H9%;TUcBIeL#Tc04i^cRbY}HCDre?`ccbtSbqn0}dkGm8cRYvEbYn?K1EpGd
z0;DU54P};o?(;k^No(<&5U-)`3ha@FUBV^8cG-?8Ng#WAHx#!SwA-D
zcZ%O+Rzn}wBIwu_U6;JmO~K)Nm)6r^&+h&COyn_jvA^^g|Kho~O(ZN5s~_ngdRKm`
z19$ZIc~c2O&~ElS4I@V7;Tk*>`b(o@&^`fX
z7?JjL!D2ntnsZ4rvkt4=upVJL8{K;s<+^|2L$32|n>#(m=X~lC0|E9cOFg@*lc&!_
zy4;i4ux1s!83|!sJUzdIVLPTYsz{qHdSskF0X9hWMShbr>S@m--hUC&F@^zbWo?BS
zIPeR;DHWn$T)raDUG)FQZ*GmvIJ`YeH{Y^|{d+oi5`STNKvzLM2_HG?5{u@6x^jh&
z?1oebv`0lnDtYw(n<#waeU#(Tgpj)9V1aZwzK*JWtL-w^(6p)LwSQ_KC`%Xm7J86b
zei`#!QYy(f3%(DLMEt!H@~!YhPGTWb>$bkWmnGexBV87g;ocHw(H@o^U&
z&A6{2wFpTGhndDLn&G7Vk9*I;{IE*3V*s86Vc!ZtTuR;R!8dKBMTPB$J(LoC7xcXZhvhQ|ENY=bo^P
zZ?knPWjckTZ)jclPuOqKCU6P-QW9|@Z;g9i*ub&6^pmp9bL!R&E8W4=7urVYj!8nn`+krp^i*v-Xy?LTrKxY;TO}{6JxkQugm1i&06p}ddW|y+rc3z
zHa1r4f#?&3+%dYbRxVJ3Ax0AtPDLaWXiClV1ma6Ap}<+y9S>2S<^2NM4Ok7>N^Ggz
zs{dDp7h2KJLt7Sz_`-UppADqa)_9C_SR*g5)0Z2A{+|5VYCFaAF6%H3Gs`KS9d&zs
zQ7X)VuSGd?BK|a}O8>q#H+?cOyz>=t3sNcWkUJbzZ~CE6hi^dLi$s--iV~`|<&fkT
ztKt{aEP!F-psg0Wqnj)+mwv{vwNYg3O;xK{k_#%Uv?8fFBd;j3IGFL}Bhl^Q_@OL|cm7OKB!{
z>GiYH)nAW|F?Y;+{f3#M@}ZnD#~js9wC*pFSQCn>ZyERzit#%nRuIQbf)2}^5xFA_
zxcw%Wo=Lrz;np-mn;UYW;Gco_pz+&e@&rAF7}Xb^4=4QKnWm?orsbjz&Nlj8`rZZC
zq@VEeW`@$uT%~buDM}-TNB7Rpz2zk{?lb`J2UAN#59)FKaUJ5eGmfLU>^qWy)s|ud
zuZLM9U0*e4o?lF{NTnmUv~~9ry`#1B^5^B)D
z(Wq0}@8`qwT{_l#D|0<3gVMd~Z&3^;l54q@U!KPj^4p>-+UPHL5JmeJqA$KW$I^M?
z@c4Fr6xwS!qmLb^P+fIvxTY5SAYiuWwepLID2-Rnp;?(EZ|iNgj(<#CeCqA_ckB0L
zY-P8zYKAbJ$mS0EB6ppJhQo@sDw^T?D|fQ!muJYhOCj&rn9wkiOdluF`<>Yq9DAc+a4HL0$D-Zc!Qjtmktf2z<)
zSs`Ojx7vyr>6v=qtFAQjyr!#@Y^yhITA3S%-MB6na&HmC2kkU;E=H=P{u3y6NU3R)
zU`NT7(+ybmiiT*b+=;FGmd(TW!6B+LlM4>@Z}7z*0#4^uNnD0CcJ|+*Mx6W=gJN7J
z5@a*&L~`3iII&xb8EVwa1Wi*GDi2Y!7NSzJ%;K
zO_RDibpFbr`1qpYK#mE?*Bs%4>jOA>QU2wjrX={;1
z0lmn2?Nnm<+=NmWAM%TB%NnbAknEr&-M=4<6ZZkMl*{_kx?Y?Wt*e~=6M@sCy_%DY
zQ=Vp5-r~o_{0t#AQDI;ixvgLKoln;8T`)Cc2hDAVYfj&d9WU~(jSY4Du}pB9_7jE6
zh1S2)#k%v%{
z=6|Px34(<`pffPf+V&ou2I|ti4otMTc4#u>*h{G1JjXY9gCf5x%9<>-!%#aF;Dk-;mb;}cHPP`k6Aa0(vUHk
zl5f1*!Ju<;4APPQkr%$_MWU!+^p0n0ggsxewA0gGo}yL`?tCE)=6aw1uW-
z4Web`{2hyiPQHo4y1pTUPtUaTw$cT#h(2&Qfe-HmZuj#wFZyA=SA2x4(?QRdsuwaC
zpWSnkO!mM^!fQ`w9uwHsqADNWD=;Uwgp*iumttq5-(G&IoKwAcrDBS4x@226Jp5cl
z*Z9Z2n`9lHKqr{cLzt8GJ38_f`YnPpnERL-@mh*bQ|;-8z@r|fyKkJByvc-Z>_wZ=
zVY6&obZ80Lhbh^Es>;2T9^eu(GSf>YQH9G_S;lpV$D_oHAxJjU1Js!9rT2ge;{j!}
z(%*$zq`5))>boljS{`|p1eu7j*E9$6)26?x>LOApqMU)CGv
zFz&tnTASp&sQp*X@>}nk)rU%Te7*GB+>uij`G2)C4*N%qa;kT)>vKr?V+9*nP(=k>
z!TTFdUdwEuG1q<%y23kmW$zDzO3^Yt6~V&}guoyv_NJZJ7&5<3coox&jngO}pOu>B
z@~u^29I)1^TMo+-kM2%xOLf&H>2GlfQiQBSXLhKfn>HPEXdC;~`ElRwJ32q~!w%8h
zQviK?2JXTISsq%)G;LVl?hYFj?I=6Z$lHt@%eo>-LJPi}TWztBH@v^_VH4{aqCQU1
zgSqFZPfoZ3-6G`*SfRZv&57Y&ZaZzY|MPK+XLG13&@bpZ$?OE4>O4!=p|>}VAl){)
zYf47DQ`jms%aAs7pqxfC0Z^bEZ)_?lr@HTMp1N|4v$fmjsh)M_{=^@?9xQC2>><0D
zkG~&TkJ+iWBdrt&2mMlLyN(-Zqv7lTr)$l;uRSM{SN~{nE-aI2h(<6Kh?nb^ewg77
z?9>{@9L~>MOL^XF_&%t7-TOj+GnT)c-3j@t^GNkmjLWN)A)-c?<&Nm8A?gWl%aQ)>
z`qdxFjQ8uePl1YgSeLGB#!=Dch)2cBnmc4-A}V>X*$DJ?k4jX4>x*fOWI8)Y9!ZF-
z)x$LP7%v&N320iGqu=|cil={n<^WXe{`lG}PFuS$2+>3NRWZapwK`BRti^;_#l4;TNFju^cg+t)){A+8sIpO(
zp$OR3I=h~sx9di-5Gu9hNkGOww+;=oMG6UOr(14=QtBKY0{?N4`kZnD)eWotLN1-L
z{2*>^uXT9~)p8|X-EAnqk3*uAYybpQSo(V&($g=qN&B
z=FUEc;OZ8{n-W*Ev2JQDs>T0e6c&4$8`(I>8;&?a$AeoAWfYMj(j#=tPgCA(dHa*l
zT%aCy$vJEN&2&dstU7yGbQRa`#G?Wzi%;iiM<>SLe>WkkQ7ZGEO-z)j%P>0Yz$(kg
zQ$FVJ__*q|++VZzqG|W7pJW0e|9V}f4~-gKXwedSk28n83?%Jc>iTfeDl{Vix-sEm
ziwE7>-^reM11D+!qqrW~OKzVbnys;d;r<-<#u#0N%k|p3YZH!X%PFjjTTkJjRJSO_
z%Sr4u7ryefE7Wx8M24>Ny_?sU-9CmIXDv<;A_0`c5{xe7m4Lj8aaRw+&Q_D9Zw{0E
zBAcc!`%)#~{@^!m5{W;x9U-_@9DGL+dga)U1)mUZnb0KoSKNX~&qBiG_rqqdl!}JA
z(kD(UEc>IpP|5tpjvalF%$TNS@`gg7pj|ke@FJ40yL?<=1RAJk9vy(qbu(!`JI-Dh
zae-`OrEKEWwEYPE;>6c>i#8mVS8$!qt@>8mJ>~rBaQmx{gM6zmA>35%t&9;9XNhdg
zIr*lFPuc_?Kaie^d+iOEvjtz^ZKgD)ma
z3^gC)laI56XI&z&pDHPne2CEgAqD)C(gSY13M*C85l$%3LJDq0_-7D^{r$b-gwFcP
zs3P_d06JNMC8u(;=xN=iX!e?+y)0X%FMn0e_eOE%WYFaBK#oqcd2LNjmd>>EtFPC-CuH#X=HrNy
zV%zh9^HG=GVyP5ye#KYk!qHSu(56SRn25L0OwAc$bB=J;ALHxg3ROAca&eLnc()7V
zYjSVWkKwg~ITAwVFWWu_HC)FVxCZY@Iy984Pno(CYNYXLAXC1!9Xh?W2z+yCtCR
zt8F*Z+9E3J{`$T20l#0IyN`0TY0uSu_L(Hbyu10gKat>wrEQ&ko18I;vhA;WUX<|R
z=Rq}MX;kwMy(S&ich<10V4m8dt2U`{EjbmYz$2fRsgCdR)z<|t@~O^Fteiz0f2D4R
zhbKN4n2d0}^7Ku2H%q{GZdEVg#Q8k53B24QR{u;q?DqDke=+*5isJT*r~13QPW0$-
z*N{|o%3{?D<-S8=Y`dN)R)QLF{FDW82F;>#vMFNQF<3ryVTdW}Jl%CeAS6M}D=OB+w6-u9J<|#+qa7uF(eBfOmX;^wWRa+5#R&}fwY!gKK*MH
z#Vbw00xL#jEbglyl`MAd+n;pgR&Ivkk!k5v`f)SOVg3e28L-7hv1cS3D>y_
z>pH5t%lvualy#mAIVFrtNuB@WBZ?Z8xK3JLxo!^~6=S8Qp=e|e3BnaTugpZ32UNve
zg=WfYpKcY!&P`|#fFah~K1Rne8t8W73t@EhUi59qbFkgZN7{K4m`IMLI+kC2^NX*$
zsyt~^u04*v^+~~bGiYqA<@C^ozg1cmH}RuBX^M{T+<1$&W-6A;1jZg
zj8SCktq0F`ZyzKTq*XPAI(<1(j4|<4ZWL~M@w$*XB&@hKif)0Lpngn3&%aD=L3weh
zeXg+f%{
z{et*KedlPD-F<|dE1EbaO&BBh4<9?1&k-S~jWrB9YitZVokM*iTxR+i)V3ASd>YdF
zXbUTIiNk82Bdx6Wvjcg*>3klL0}+mW@rM2CwUDL-KRLN-i3p*AIZ;ZDh;}{M-&AGj
z>w%09?)jPBGiQw{4gh&RS(A>*ZO)L`rwu*qnKT%`m*bcYNh^w(N}P
z*iLcRzJ*!=zw#xD;#Sv`=_jB4ZB5JPD!|>%+(f`y1zf}kmVYUAySkaLc$SZSVRnO`
zQuXN15Ipg})HCGP?%x0I%2S=AWJe{^O#Hj^oIo4KWdfKF7aH|O37so8V8M*@_I`;p40gZ@OJOyvm{7dj
z;B~K^iPH^p?_XHs(!+~9br7!{=6H1MfuGF5YUhAxKa+RjzZ0cE_<2|B
zGAr4&@z2X%iTq%Pfd-R_NnJ)S-8L9jHk!coie=M|`A5l?>7&Ci&t={|
z=_6*hk7^$&FvW+bE5Aqw0<2>}C?~r>=I3WE=6ipBc}m4*{=U0srvGJSD+d2Qr`*9=
zzI@0aqyFgeWRN#(ipo4=LqVThO^xMnUO@cJf{|NBAF@bSYzNuk8`2&)K99%}?L-;+
z9x}6%`D?$4j3}`FYb|Vtvj(IK0PK0bT?_$M>O0NGX!m@JRCGVTErM_>WQd+aRH=S^
z-v6q8s;Z`>^G8u0mupm36#l6Mf^BYFtLcO%xoOgHALqYbobKisrh^~Xt|PC0$e<*S
zdxBg+hh1`Obw}*A%2+WedJ6bbWMV_E**9ykh%sZSeHLJ0fZ8Z`)VWL?fvzo_yS(SH
zX6tVlB?b&fgHb7-WS&f4OrLzE)oo}ZiVAb+(4>nIhg_#@^Ia^be!KVRZb5mr@D}{-
zn&`{9Ah=fO^%IJ~fy^K&s}<6Wa75>MlKbZwdcS?#*CzGnBr~i!v+E5~SvAc26QlQc
zkj}Y=!;L-Qx^FIv)OT-axj&h^j#V&?2A}4VxEyk1u?`txxnioH7*M2+yDY5}r{J{x>?jTmSYPJKHq0C#Dtl
zzUEhHD=Z48ld_y34)wbF&7&6u$B%HjtgltlOYrkPWgb%M5zJr{-`-T^+c)Q%x6&0{
zL`%D5%m~z5fHv-_8zYRLZ7{%DyL`4;3!!^2hZC*I9@l9Ai|1C$@9lZXP5+UK5s;NO
zi+IlMLjx)BjJfw>r)`jJWlA?~r24GiL!FtE%DK5PufXW+BJ^^GqpHH
zu|wHTIt10Sp-a9oH|j4C$_QWEh9`9RlJ0xRzoH^1)3M=@_*O^f(@WAtVoynBQv}f5
zJaNYabu9Heh1sL7BDz*jd{1~CQw!9va{Y1wI%DC1}qU%s%actr)fQwGaK!dm3HLdx9@M>$#=KXyvQC-9X%GvkTk_-x9(Mn%Gwy>6(g;v*0;xkPc+K1~!8hOhC!Hbna{Qn|#@qbs6@~
zD!X9953O?_ZRd(l^I@|JI(4)q_K@Tpe;+&bKSJKSds?Lm;$IFQN=ysk`dnT`^7X;s
zj-8e$6G88H?rQ-leyzz+3ybe(HmffitS%`bGIEP`$p^fyA3`xK^&i^X?p>)$)1Q!A
zvlX{Zc9q**fQwkJf8ye|hPBD_52z{ozSvMA6(noKh1@Rev$=PY(F!FV_xAGVBXwAG
z?dLFtNNQRs>GM``8p`F0A%8z!1-JL;%|5W6E<%S+Xv}*P$)WS6Ce_qrmek)?I>L~_
z8?O=4d|i`iwYulL*Y8LuKz#1Ls3*4TVB6=LE%S6`lk{{lzb);%>Ui)Q1!9=p9p1+Z
zjP5AqqbjuGGh2RTsNYv0rRE;}J}>09R>3yZPuBk$u=-Um`y_rkk43hAy{YV@@&nogq=lV0TJ
znV-0MjUGwuOx;m=t^cx!Tm=!?(j%=^gK$TOD&gzg6P1Pgn@8e{#`Ry3kU~q4M?Yk=QN1$6d+p?7dVk
zogOtju@rtam5hhz>CIq%={g4N5FK8EJR)(BNARkXvS0_op%K9cNpP}d
zwt}p&hY%qAFPn_?IeUo2WiypE@xDRD)v&|1Sk2PWC#SOV&0YbkCt`f8@3n{^GYnfdi#GPmt1FxgUqPtao>;vR|J
z!Jc209r>*bclleXT>iMm$IAVBajk-7SRDm6BYnyecfJ}PZL8tvX
zzks@n5Q1LEj9o@6cWU%*ltFH7!p@uXrfn+8guOCfalMwtf2b
zA+aweA~^k+Q|U*ap@F@%ug4ORI+cQ6xpUKAS>i=U_&30yONLdZPwSw^`MzPcJBH+Y
zfhfLWTHw9fueyHPoS^r77VE3Rl{A2piC2x=6^6lP=(G5IDcw#Pt=EehGeY(6lJ)Lu
zv3#uDN2!%!)5y7pwIxJ!iX%!C#^Np0kL?o%W6%3nj^`_i8
zj6uO2CS-xR&mSY&u%d6V)L8pqqrP&qZZK)C@gshK}Cjbni`
zYrYydGT;A_7cU}Ml@IA*#IjmTsnaxN
z1oDyJbCg{b&u9xKYivtug^=A_qtTYx_xb0pC?}4zk=|)`zV`;}(q!&)*lx|LUV_=Z
zgJWp&Rg}H=KbC)~xE=A7$NiSPx96d_sZ|5i06mE@tZUS&Pi}Fy968HFv$RA#NLM~_>
zd?t>&!nHe}gtqA?a9ZQ)4HzQ!SL0}f5kqWO_5*ANz<(Q&4=mJN02HW2XJdA}*)!ww
zi)`z3n6mv4JLIb4L>nS1F;QuTbf&8YqTz4Eii*aY1El
zHM0x5-UxpH#xk-w%oSf!VkU>MS{RaIstjk)y8Ga$XQx5)`=OYmDxY=Zr+
z=&y=I^+=an3ExCaSB{Wp;rIGdv_#
zuTKFs#u23FQc;2mw9#UwCy{mR3ybxv5R7a7E6ZS(7^<4an{O|ew2>z={fNEczCjWk
z-?Mnk5M;X9sBM3fIPftiM~R%!e#5^pb$kgTM-CXOH!)(ly$avQt;h?U3nLm7qEZi5
zI<9dF43(kJhUf7_=$v;hrZfCZIssPpzg=1z(|tLuz}Dq{;?8W6Xm07;im~DVwW~Ef$b6*^ttge*gNL!uVH_!fqsE;nA4{dgnyT
z=I7Y8Oz^u!UB)51RN1SmNP4in?BtfNW@vS8+ZW;hHiQlE=1rX~nc&_Ym2CJ(XY#bl
z^-`(yZt{iqC}vS75^h-{E7&;~wzyQ1`=MC_H-?|QRAxxQx;
zk6t~`oqzjFZb@&gPwL%^$cz%(=EQ{B1NC3tfveA7&aL
zw}A>tu9`b$ZEFD~61vI0Ydk8#WeCq7^Yx7o09_m}2)PEHC2dq`{UP?yBu~NhLWn0e
zw3k+^>cLu!QG&%jyfz1Uk0TnW;wvhIWj7gS9Y1LlLP&xWAycXV7o{A%*6aCiKa*ra
zR%yFGI`7cDsO0K5Iy~x!M!|Qz?pv4iv1J)&6z-u|rUc@@gG|zC9L&=q%=_n_iNWqM
zw?caDnZj_!--|0~o~zLMZ9?7F)=5Tvj=hcPGdEFO_)D@l%!7#hLD+PE
zvB}PF_4sG^882|=Q^LxP#(2yJy61rf#L8TkW^aG&eDx6HI<>ayI0E`dL+{xWUJ1sB
zqn5RTS2prar5@;c`Wvj#D@&yScH`pK)&TA*-y*nI&h9)x*%1}oe{KW&KLAxgs=wBs
zC*k}w0sWE8g=5-Gs^sZtmglf@Pkk8S+$ONXqk6d~Y+ZVRTY}ildHz44hSki}Ge2LJ
zqE)58-@jMvSs|FSg5$97D|)Z0S3hdO(>ZDO|9QR`x;f{o@p{%=OWUGUC{f-QoeZ63
z>C;XgADwO^#;?&iqq{Uc|F+DYoPIk1ybPNtH!*L%B%jM7cVaaQE9xC9_}Cj^^)G@v
zn55@bQ04lB-a%KwRo}I7&hN6gU2wkRIlRZHsZ~>TQnBPght$qdAeZETJ^vWrZQp!8
zR>tbqPx^cIBrNtntyW^kDtEHdP;6|=Lio$9?eMw!ef4Y2R4Tdq3XnXp50TfHJ*AST
zg;9LmTUA!@Sv&*qkA5J5TL~ObYSM?^;JvPl;T4zOcg@p!gX8nuUP^vis+<1y-1@IFuIKOJ+4?XIN3C+%N
z^}(6SdmGw)y||4_Wu%i;6qjBDCd>^C)KHYIE1doHdd_YLWb^3GxH{{An4&%ZHqks4U0*kGo*F25$p1=NwgoOV#
zBr-yktw*n?yOA+*kM!^Fr}63S58zj;Q1xinrsCG|=VvQSN6@U0M{Zkx(T-e1wp^Bd
zx61s1L#|b6p*Pr_ZA;6h%EbScbIRcwIKs$b&DblR?}6sWe~88bXn^e#uvkp!X~&s<
z6teXhmpE1CCVbRwL(ye0nMU*R*j%k$i1t~W!`m5UP
z!Dr#231R`))_5N3K7lj##m?cheWx$k>T^&}gN`5Z-_Jb&r&4Wo-DGZDAh&~^(Yh_!
z)A1-%#_U`S94I+beSMVcBezXe)_iB6vrnhx#6mQy
zc;O@H3{OKL**b3H?(9p6o|MnBbB@tnkEvf0?ixom7-+21dCkiL_^Lu
z$l0GZGn~)jr^LK8yHKuLezZes0KMo=Z)W2jdGjMN&oH=v_UtRn9zAU@f%CR4EOk`R
zy5z@jxEvr5kYnGfayd_rc4u4Zjy*AjIPkdyVG+e6^e(p%K2P&chJNYYG}l~s-a{-K
zU((AkM!E`-IUYzX=i}v9#P}{%!2i-K80P%TZ8maUfJhW9Agxe(N33Kuq*X9-m0E>kD6`t1FqWLbvb!>dvp8*!Q))395{WR>$;FO-&qRpVRsa^Ua%$DqeS#U-T?fiLzwE}qR>Tlr(8s2aY!>O(|XDl
z0g>W-_RotUWR{mUeT>d^k29Yef1khOtlCDX7KIG)=>$u*qKZ_aYky+#c
zyDC{}Q{V2pPdbP9^3nXCarQA@9kR;6{c5|<`A|+mWx1wDI#z;y7PW*k+emF;!C
z<+kQ}!})dNA@~JvUUZAlJsyl(Hdoi5zFZ+rOc(Q93l7{C&ZlhcH_khr2j-K*UKoyn
zzcfE794nnh^zYibI484Hbm~iT={f6cny50u#2z_WRfDndPd(yvXrxJVSDQ~Ac$$r_9)0Z86
z;>8=_SmOiyBw}DU^LPD%&w7ZJy3ycS^-_@bL
zqn94hX>B{A_`71if6iB$&KbiV^*qO~e_5o}M9+2IPm<5Q2kdnfZojhc!Qiv0*Oxs?;TR$SOU8U64d{lh
z??FgGFGxr2M|&10{Ii7JeY;Ob$+g&TmB)<>8|BWDA=kVoj#M{c%fL>MF@*{QE}j>e
zv)Z~ZtUD|B^%&J%uysc`%rE>-e|~P9pg>k)zs~O-+08yF{hW!${V7T0J^RIz@Ib|m
zjl-InY8q%OU6?6
zwa7tnw-{j_D{oQsJ@KSx&=C%mS9RQ7b>}zTsuah)>G?Cv9z!#*V)HMZS222mztKf(
zgYV+ksgg@Z*P>fpMcD?HUG@7p@1o4$dNZK;|5#Uu{;BgtdLF^bqKjX*9_i!;^&KJE
z40RUIApY!RoiychmwNQ^IkH@7^}*fXd>gI;_oDk(-g*K1Liq&VG8i3kC2!
zg?>l(L6`A(uhi!l1`vR|;kCe2Ov?H33h6A8a$I%^|=xE8&j!?GV
z@1+UI$(b|mpaQZSsi}dmNF|s(D$s|To1!P|sM*s}{>t^p@pqu
zC{Hu1M#u2v*|#AARgz!gjQK6mzbM6^N=a7TI>795k`4v=VI5%1dtDA2WG`a)OETu>
zaPV(PXGHA_@(d~JqQM&S9O?BV>6zi8{AZfIfLsxQJz8Sfspa-pf9P-U_rq@ij?v3D
z52^Ehhdw(2W!w1pVRynJI>#^SeOj>yspnI0yq~WvtZT=d$6)|63zh%#@Bbgh@U0e+
zpI4Q|voyPjQIN?00zWEnz2@<=bvP@{R(C9f`@gaYDxIuc^LQb!33@-5Vu9K>{Uc^)
zCmW>sWqi^6Ei(jDi1`$7SE+0-MiZF12R$Z1Id43bbC8y~uP?b8!ZPV^3!cvr6gjxC
z6<456e$a=(1SXElpN9ggWhwofC;JrrD?gJHF8|JR`>twhE&MBOl0H~q(R(K+Uy
zyy^Mo%Mf5#x^UdllW>eB^*iS;{YWx}f+7T(bBSIAN}Y?w*+Cv=`oa@g
z(;*z9P7e1>aZD7|QQEZPH2?Tzm!akPPx@Y%@{P|oox}SQWjCr9sUT#0ITb;&Oa=Y$
z%Pxoc3rysdq)_xXyS!uAm%RK37a_EP(Q)JxZv`Egg!f>1$!lukbn<_OGjBOGDWb27
zdsnnbMNp&M5QuB$hse%59}`=N6{8v{gFEaG+>R{WwJ&19bH40#4*w)nghCwX4b{fs
z8_9pY#&`U*Df@RN9Ng*Fr1P_sJw&`kZ~2uuq6X~R@GI(9f2MKcC7;B+fmdM>V#JgP
zAa0a`-w*c%D3GmAi&x(5y6}B~c;s)DshW}i_+YN$o
zH1y5A`s_w4dCAyAYJ&v~W*LyJ>_8y#)I8z1@+QwAU}uJ&*Q9)b(K~drV=$;z>G#t4
z^HS{#MCW`r{ScN;o3QU}4gmLlv>^GjHuk$6!J%Sb^J<~{`G@U&e5Dl&^XZur9bjmp
zh8(`vONZcCyYd?w`?oDWh>5jS?BsKVQhIl2ex`ytritRdyoU@c@@^`6=iQcZp)HaO
z5jMhR|S3hQ#AA{Qa{)AhuO0jTAd;0#Nd{HZKDS!^vUAe$fA~cI;?AWL|!8CS8Zw;~Ugen4E+v
zfLMxE4m^SG2AfW*y>I(UKbY*eDme-43dVEjsVTEmrsWX0g>*>~Po!lCMs6kk*hZmo
zBZ-7TZ-;s7vPN!IvQs$BEdA=lRCLK{xW0!AO}4iIe~IGV6vIh!8m6E{qdjVwGrJrX
z7*SsAvoHR&TUiCWRu$7F&^&ExoD6h?ot9`%-)`AMLNT=vyM3w2PCM)NXxML>B;-fj
zf3P4OgxQ^C;J>&QaQXwcz>4pc$iQrv)FM53yZ?<>jPOAVK+h!8DgTjMFa^*^#lzjN
z6K)&7>h@dgSX|3(NS(R;c==IOy}tj!qhaQ6gzGhWk)>PMPVTYeOzBYW^p}jo^3EyT
z9t}DD^$BN3Za4D|HnGcVKZ{){3&^qxuDK@u02TY*CyW0Ghe=-f?C{^=Yn7-=QftmMCk^@C#A1;$t4Zha{>O7aP0m7C
zpO0c}?v1?FyC37f^a2lJ>S@8}&@x{ft*O`eWybW&EhrO;
z-nsr>az0{^rbl_X*Ejvk>NuszcgVW)tofU~ZRfAN6DZ}V<4KeW=zaCRxc`iGF<~G^
z_49&P_Kjn1qAh=)BgNM=gp;@xOA;|%4aa4
zF;?m2%fE@Ao{oO=mdJ7Y==9W~PtBr?Zl*`&w4Jo#{f}}@YUcS$)?RvD^Sc4(3~vCy
zdjpoqlf&>~gp>36#fckF5X2qNO%9#@hl2Z(s~ZCo=Ta$$Mxhpkba;K^D#&d+2Zjk6
z+gKlCYRP?X%yso*8RXSHBz_4`Nsq$Xz*Nbx>tq0pXBb{xcIwkEOvLJVbxk=L2oI;1tIdcb(mwz?pKW5?hWBypi8nqi4V`rF6^+TU^uZy62>96>HiNV)A
z;d_+(55;qd=lxT^;dBm@$nqI=y|`nPd%7t1dFzY&pEutN)f0YlQ_d&;Wk;=r^F+Jv
z0RVST9PxAq+QqaP@?i|pNM!8BoJH`pd${{NbR{8?1@J!0L7ew9Xj}aac@`fhi$!CzC)V1$MWc`
z2H-e?!Oe^c_fdJi9OHdKda|RG;biG%JJuOSr*ewXAaks&)c8#w6I9OaRGR&P?w~Wa
zc+LLV;G@$4bhB9I@WQX{%8-*%j@$Y`W!?q}^Ea5_w@FXnPjzfn>EP|cBR^-vfw~I<
ztMBY5j3rON^nS)@4^Zc}Kj)OobH%7(E1-6neyJbb9YJ@&8PPkK&S?h~<99iaTh5Iy
zloO{5qgl#Dm%EX=rN`wzi8_i`mC=)Uh3|$6Tir0uI<4`)6*|x!lsMG3gs%c;6CnR%
zdDqRdo|AFD40#g|RX?m8lxaQr7%{w}8SN8Z1@WpYvYp!Osfh0(XBEPY438O|Z+6s8
zvm+gVDpkw?SOB0@AmmuuPAxKlmTgoCb=%O_^K^RF1M6#^9?VF9bbH@i7|;Jn|`lhmw8K^mi{;QzE2jwdj0CKY%eIIN8e^f$MP
z4fWjd1j;n
zS1Q6{X_TvSiK^5=Rzoi&g_U`${u9`&Shzoet(eJg^INXnY1<#}FTC%RRagu>G+SSA
z+B{oz`CWV-&_{Z1Z+bG@U$?!hQjLkM)%2s{UkDfEW|MTyEYqR_F)rGQ>MGbgHK_Au{qLJDQ9-1OnWl@e9?n1Os;sBb=0X|
z(X=q5Wip!nyiRXY?y78GQli3;c464;3}xT&!vw-S?Q^*%_=Bmm1J7h&T{fyk_-h_c
zD5nqp0-5WuP^9>E?ZwNhDRJhDQvI%}?E4l|kVOl9)j=9nuJ0Nh;-;rZ!bBeqx^Vu%
zlZ`m7WpEba<+gJRZ+_K$mV-2xrI+8xuPn3Jj^m%>*W^Sfz1H%uvdV6IdyQlDAqW$t
zdTCwn=M_Hre10zx9YHVolwKn&`9ZgenbhfVwj^NYC@Lq7e;FTEv#z#q;cgYx{-%Ez
z-ITi;1yml5
z9%zpIJB|$q2uF|+T7Kg*dE1HRQ?k-%p-`W;AJY{P-!V<58;q2VS*QlMMi{m940WgJ
z^p;R`1aJWH;!B^;V3VW+y(kw0c(Y$}o_>hUq}j
z(PlW{0Z4yuTQZU&<6~(&(}@YMgRBoZEF;fHZlU5MYHjo%Ha5)rDwSEdUI*+Bf-dYW
zcZ!=QzD@HTbH#7`ANdor*WEka<&BzOPZp}jPrCWEJF=`VJso&nb|{1$M4QeezGh}5
zzJ>3#Ugt7v$k87y!gxw`5&-7$?#o
zxizBwgRU+(7a4h3*_uS57uA;_o;K-drnpRQ?L)%{+RX82T?04j<*(ZFVF=v|nS39T
zZ|7*aAzP=>%DDz(=V>%syfdsfE2~m+KJqznm6n9>a!Ljor;BCQ@|AGG{}rxotn{PW
z$=8$7<%n^0&aCNYvLjiEiyikhizbWpO;5K!^$o~nKOIsl6Ij-Cv#}|e0m!%&I+;tU
zup}CeiVr|dWIdg#q~)N$ZY4Hr;r=KsAksOV6?~oKv24o7Q1{^IPQWWOA%$f-+F)m>
z6X#!KY(cDlC@^kJ&er#JjZ2EE)ja=tI4m+#Wk<0z%7on^L_{Wsp@6ARPI1j{-n
zy01@HUz5kK!=ionx;W$ed1C(I-c7$V5U*G==z0`RXYD_53)ksyv)bpAN71mDG>0`C#|?bj%1>HT+fGs
zP8=&l4y)g{Yk8)fI!OLG@b-ciAdgB!_2p)9!+=OeYIX$4!=7F&SY4K*R3UzxkzyhJ97GVz5B1PJ^aOov8pVRexmmsQ8!Qi(TSl(TWPw->^uV_8wum
z=Br{SjHUj`c@GSEQaeOXG3}a<>96d*QsWlOg7mq|Y<7(*SC)Ouuy2(64`NIW&_Egk
ztV#N%lY;antYW0x^Q{j-wET(c@4_8ix6-Y5vLiq=&;rkge*%Y{g)^cZ%@&uA@r`kz
z>NBOYaIhyS)l4dS*U1S@yg+p|_(Ol5dp1t%(pAZME
zk^RL(R$2v})>8rC7>^M%QnPjok6Dgyhmmz^TXr~fXhoK~C*kKyX{sjI@-
z2Y>Dsh|k*xqOYAT$K}9kiEQ?6KFa#_9(5c2szcw9
z{J1>y6?5IM*5mP;8R@n>JzS5oM!#Wd;a=ux;-1Lt4t#i?Ir|@LUCZeZW%FlW=X+h+
zh-qEI-JYi|B8;6k9dJLuB|jO~y>SmcrPo<@@5Iy#c0|j#`0jc~`P`y&uwQ!)`K)k!
zSY{y&i;LxkAc)gB@QHDMMTTFC`u7oMoQYFpV_$T3Ilt&nZ-u7-Tbod_66SkC-dyig
z8IiOLy>mLljQcTg4wU`8u>&LF^!|ni$N38n5p+HQ!9De~IP*i*QKQMe<2CiJ<)Qny
zQv_Aw;{5HRlx=+EiG4;X9(fwskdd-#0|l{Qba
z%eoih!Ts&`9yBg_Yzc`ybjGfae(HJQK)^0X@O_d4HBD)LTeU?o4ufykqG#HR=1ie%Xd7nIcQ#&+Nw!y{mcuz;oY`;TWPcJ?5F`LZ|KI
zwk_LUR|65b26MddQXaR#eEyX_+LZJ_9
z&mrlu;PAh7YJkW?y3cWfayttmu%9m6?^|VhlI`)42Bc4^nC1jn8mJ`&Zh3>72)YH1
znXypRnAzF)qRU!#`#~!&t?MkAi?$BzGy`|
zo{|)!`!DIG^}<`Y^g?RXYpUZF|L$(TPKn7*gZ_+>j`(9TlTb3SG7pSA2^(6OTqe_vg9+#*+)-uc&XE~awg
z5_kW$uI3l{m?npmPhcw>dSz(6)Dx5cvo1z{QYWD_pJOyB53IjoKgZ51(z7ahPF(M1
z;gRG=_$m1Wo$K7r6IU$QzUJ^G
z>$-fIQa$W1^!9`PAJ554M(_~wRg|f0OmP}m$(`%Z|06n4o0b3SzQ$!%gdfJ(CRcus
zkoSeVEsotIGLf4)&TWszz8
z{r2+u0pq?P
zJB_Uy{pjbn4wY4J3&W?s7mT!L?qwK}tUpXvtrh0?+yjv4gIUk@X)9#K;Vbo*mwcBi
z5fGD}5>8f1n~nAQVHJ4M&&&mUy~v_zc$KhpE-yJ_RQo|KQ{J|N%9
zftnn$()R@jPgKtFu~%kZJVrh1m=Pw#itxclrpwdzy_TacoTVMcgOSh`yRVZ*Qcr1qgc~1
z06b=Wnw?OJ!Q%Mm`Crb5|2>3*JCs}B!%ix}v{dRg&xVpx>T|{>Xz@L+CmgW%FFJ5X
z0$u#Tc}HoNw@b8Uyy7hO>GqjN)QuFj%`4^3
zJpw9#v&60{d}_8#aDPeVr>=2n4$tYw-`ls{B~vaK;Uo~Mo}=;`32e(erfmtH*(tNi
z(z|Ov_tMgS#N@uTR-$SB{$s=y(U0H93Fao)X@&G-f9@DAR)(Fp58=7fA;P~?X4!H)
z_xNzm_Eo$G0CdZ&7djHC@!hltcxZ>wJTQO0pM8Qs
z;M+o9hGQyN@eFmDL*0I(CIZr?e8(1sL4TVMSQ&^vmU*DTxQirChGe@wnT@KkN8$LP
ze#UH;Z5R+fPWm`yCPBY3|Bd&QHqh$*;mMa=SeL_`eZDR3lRp1_<$WTe9NnCr>lwpq
zl%T)GzTos{y7Z<{(bgwZEc!n5I0kN{$@wk+5Us3G2G%(ha~%8Qdgpf5=3hq7p+|bZ
zwk`Atm8B?w>
z0mT#34+RZ7l5oF5_p2a)QPjt%3juIX!j9>+Qx22-GJ$cs!(D++BrpJ7=+wu$LI9Kgvgc-(-JTcre%tp2up^<$)oHusq4OI1Cdi2svwwdj>bBXZ
z-{mAcF9!71xxy{gms8TD^y>jLI0@1q@6K#y|Z
z=kNn#{^qieLvG{xbNiD%`5Ea+Ujpxk85A6NUd7`4L(g#!!1>w24f#0U<#Eg_xdnwa
z=UICpV5#8E!g+8?&G22m=#NUrSG?Ex`}=y|^oL!}ch+LefAGuK
z#rd%QacE48O~+g4ju6|qg6k(XXJq3!-MqYDQoetb1A)VO`ugjd?}$>r=JTd!eA9pY
z`I5tH{F`5U>?dw|np<3~uN6A-i|=pwccR@2l#UMr
zXWrvqz-d35^me|y;%Oc7{0#@LdrM3o=RD=w8ZEJ_GhB5ub3Ol<|BIf#>C3PB`^L|I
zkdq4^Ec)v9G2OdJn-qV`Pu8%s1zAn6Izv<|zu4LHVf9zZi1%P+IfB_E?&cROhiunk
zcE^f=v_Ir}t3s3-cFO7ek8Z>#9$xa6pV|1Zh`s&WTb|SMD*r4ew}MfsM_y`*i+zn`W%B{83es?v9u&ec^dO?X5|~
z6_u~htgT}GG9d3!?@wIv5MO%qn?1S+d+`^7{QhYDCeM=j5iPNa{POLNuzCHS&5Nk9
z5SV=K0XQ4F4NC3p_gRX!R)3a#ZYpEimMiKr_F;_D@=%`QvqdlDIepCddGTdc`G)ni
zVBX30>^H;Fcc0|TQ77i!kRbTwoRxjIjH}WeY>Jz;IYD9mGx~*BSRNpWlwy~vFfTeN
zdza*UxYg%QT)Wk{b6ntyo!ECeoOLEC4LWpf|1cmOE1LN{B&28MbSL{1ss8rSIiy35$O5_caxra+nb_
z8*8=S6*cNYBVv?x6d~T|Rs!G(9jvu3NM<3aU;jxC`5YWcql4ukEJjEjkFrx{o-eJC
z`!gv$|8zxYc9a(yp2G3syauA6Sv`b8d+ZLCc%V%(qL`q;ww
zVIAE&(Kzfnp&Wj>{8(*N#YBX`FViS?h$_jCjVA|O=e~q*78Ofe6%je~@p*suLfJB-
zux{S?X|cVE**`nYyQbeY-@opiFug0Mq44a|Yp>05tLE7or*q)!Od>0*`4C=B<5O3{
ze+sJl)i_e+SZ^r{$4q{6Z0wr4cCK!hW<)WGtI8^uS3SG!i)=
zmz@LyKOtdfD&|U)K}j>D#!b&DYn&mvQ1fNYTjp%9e76cF`)V*AY)vaSix~wS+c=6r
zxkHwQ_f0oyB7j3_u{8H1%hE>{`+v_TZG2fCJr_D^wLbp*oB670490w1M2ZfK<^Wq^^x;bRQ2ivmAvD?rzCU_peDnAq=rwyeOXzn*mAwrMVd3yRO$_ZnrPr*-tt2nrjJbH=z6F5BsxEL
zo|V^v2{*!UxarpWg{#si4Xa9aIp~csw4XtLpeEjdU#tu_{Lj$Ih55eOeFe}>^2aG<
z;-O=~IAfFMz@+<>hPQ}P)NPMsLPs^s#DU|Fn2Eg_|0~W^L{DLjV{)Z{lTn@_8xDD89SrjCLZ=
z`zw@&$l+Wy0J^=qV=>cy*?*VK^EZFex$^BV#mowebBAT5n}(^exk%a_m-FC!gd6j#
z=0DU}*>g&w;k32vX}9*|%2xe(2IMVZaLK)2e)!FdMmM;;kry%G)~7`&g6T&*uU9S+
z(F#N?*jJVPV#{vRKr-3%?~aBWYByC%`#F$2me+Meud@tyi_D-0~41|In0t<;#LNbk
z)m5Q%J+FC7px@C{?0%iRA7Ie&*cU*<)>UsG-PJGFK!eRGk_QqNaKND@dYx2G2svK&7OYT2Rd}FT3
z@I?2?m|ZJm%`^0?%U;K#Pba%PcPiQkXZ>m~QfIjI@&
zv?IfgYm6FRzS+&V{G}P~aQ{m534&%qYPAXzNBMtA5MCU%);W2#cvd0NPc)L{T?uVA
zjvzG6KJm~IU6{*`M|9PYYcfv8yuJHSBh}wJq7)g+nEicu=p&Rxv~=1Te!(*-OS@WK
za@py*ArpRj$YWWb&*MG?f~*B
zK2^v~#0%E>_UH
z0iX3=Y3QDM=abH*UtZ^baS>V`^tbrM7-{9+0`Zw&qVF(PVRV{hN4_u39r-uja*KNW
zJ$JkZ66+lX=A&+H5SN4JcxC-*!o7N*p=x@1ON0xqUW|vIdiI$ez#4Y7@6|EmaC!$L
z_IoCgr_tU9a6S0h5Ay|7LhE<$0N}0sJgcQG9T@=$p~oiC1e
z60GpqXLSPa_YE-rObK93Ip9JD(hD7z&OiiSiPiFw`VT$B-xHIUK!gLXL*C4
z=4{7Pzal7RA1V*MsJ>Ow@=)7i#ntvbo!_^i^=7?%f8^6leB!&*O>qX*aESvcpMU!I
zJ0X8su6*Yw^aPW4K~E70V&T4h$M@4HgC)nD@54<0u;%Z>?$h|~Up;~M+AWIwTyYp5
z@Ai0K+o}z6d*W^#wwJp&O$X~8DQ#)4D+%lOb^qhD#k@bLb9+an>NJTROO8y38|Rk?
z)p+j=0K`+He%zn}G?P2z}j^5?*r
z;x6jPb31O59q4~?dsBi=N_
zUf(^ts=iX|
zqcdZ|V`3aK+?&T?sqgY#IwP~H^3on!>nX3dGw`U^N5@ogF88rkcVHCMw=Qk4uH%v(
zNznTST!%jD-G(P~)v~^L;57pCB|mLFApcoczT;Cq_~oK$o(IsySc=27IEBgi9*^T}
zD;N)@Zuj7C;hOPJ;YLKg!+^T}&+2cnFKzQn6dpn9JUfTJH~!;XE^!t6o<7}a-?8S;
zR@#le+fu;z;|}>ToIZwOM_^O0HGVm8#@Zf=k>JU(Dr~&Y9bY1l;QKD4768>k7a~vQ4^!N1V
z`}F+Jh>@|W#ST$4%1$@Vh{Cd0H-2en*w=kSzGxx!<|)eRhz5U?pEtf}I|yGPO`==S
z@43_-+UsdcEG2oRaWY85%q
zkd7JcwgqU;diUiWfH6;3IiEE8Z}jNQm&xDq;D6Hh0A)^e?)K3?2*;dASl*LTP3U;men9N6oZ<`KO>cmZ}rduEjlRv?@2d3LuK3PlQ3IvyUvMtZ;X_Q+l;|953
z1K-bhJLu1HE?_Epy6u+`QSm3x78jk-;UB$4FX%<*+_Z)m+gf70$XoH{iGWuq<%Baq
z^JM(|io?IR<7V>I(}9o6PYcrEa^~<`fiD*6JYSY_C|lp`J=!}<|GDVw^>8f751cHe
zL^(ZmD{+qRuiy2Z(=k@{wf#Wa0Ci(5%5-)R9cOfNlfgZP$?yZZ)qJS(#?y|ET{M2z4>r^B
zJM~b$OD7o&?3^r^_U1G1v%H(a`p6}eMHl7=aww~dqA%sHsNG*`@0YXm@Y&A>4;
z^%U^G38@xLH(1k?{4`%e=|gf@?`NtG1@bhD+P)=1`7UyTaoE0-UJSdh_0b`>yWVh&
z{ybaM#qTw$UfhUF)9U&ram$?#bPGNU49HnPAwILXalfbK0iZ^PD0(kNPU_?r_}KbR
z4uUVQ&N!IP`r>PxK85oVPVxSUUyu_;3d*oLjbnl!`M0_WI_4j;j}|>`62SY9Q4c#!
z@3Kbl@~z)z2)Ij5ac!1|_Wkolcn}a(EYDgKX->#AX+GvzQAHIVLW#YAl6}PJ9(0?S
z+q2JJH_0dN14tZO#vnUL@@5MMG8ifIA)naqNr94yIsYoq20g`LRL^AMXWu?&eVMos
zEuRvHDb*R?ztH)Sa|d6{DJo3%#p<+t=;O3IHP7(*9t$mk=-y{r8%^Tj&P!{`nJ?@|f`+WW_P6_*B{FkgmWHFa{RMaT}=AX@d)N41|{4SEOQ
zb4Yl*t@qQsGdvJS2r5ZRcdTmKNpp(xSXuc)J3qW@#TS01lKq<@7X8-|=#lX?TEKe+
zcvZBQ_0i5BYA=%GQ7qTh`=vdRrJ~w#>-m6;uX}^{`E#tU_}#e1jAA-m+41IeF(K!-
zLQigf82?*4M`nUo@ie&8?i~FeMY?Gu@Y*sTx5%Q3L#oi@2>y+ktkGxmL;1_%vE;`@
ztbd?Ss+PgcY-=Mcc^gONzZN>{uXBR5ORnG(f9d|c+wq3%uJLcL{3RGY(apBro#`x*
zys|W(cu^a`;a>!3u;N^kzwdwEavl@#PWt{f<{SRJ>BW`@n|1ob#u=&9=LRlGRP2S$
z@?FDPBJW)h=ZTLVYouS%;qYCXesgczR1V<@_W3`@Kb0?LxL!`vx8WRt=Wn#$nCq(X!|A@ku%|{@
zLPGqBdu)Hd>XG8WFW+rwN*(Zxc+qEGw`cT>{e%vsH?y_l?3HXEM)|u|z1Ax?2U+m>2DI_M
zpX&E()SBsOu-4h`avH-o!MQ)>{0Gg%xLQHZCrVt6^5^dU!UKk`#L53qZS{!Y=m7JV
z=-UTcVTyCE7>=(?G&?doE=ufU_tq#dfZxgJEHO?
zXKvovfff{okm!EVKO|OYb(4|IB=SgygyNS%<+eu*AA;VIQR^sKdV!Y_or&WX^5Oj5
ziYhr{qkKr`(?}q6bCGj5!hvm|6snLI0L&?UF+Lq4y^|=B^E2of@N&W{vTL2+9>IQN
z$NhVMq_|ga=bI_MQO%g=O?ERn5=@926!kTJ@@DD!Zj|Z6yfzoGl?&u0j9Jcatb`{^9x3)ec?HKMe1>I;q)6{Bx*5
z=QMRVa^kRUzBGOcANyPGuzj)a!+`$nPsBtc<}cM~BU}|pe5e%DLU9EpdCe%PKINI^
z&@5zkeXvs{Kbv1YPvg^XZ*(=BR+I}KrpZ&C!%MAsF7m{BW5|EshG;vk=-#0a^%brmS@o7i!YbtL;+bpR50Y(0Vk7P%{^Ez^Y6%Ft!{ctCb
z!`=AC)t%eV+x-gv=fD2dcXBNLV|HB?gQtarukA43naF!W;@=awIAXTjTiVP<;5*KX
z=ieM=H}t(UXFA6G#nCQ4ciDYfqpaf)<6QeQnE~{&BOIUdH!UyJ$0ZN@+j@DP;zzx%
zOOOr;_hW`*{8q>~wexRCym@{1ZJx9?;!AY#675gdqS)oUi&d`7jyQQM^-JcGVevZ8
zN#|e5&yrh^-pn{M;_8RzMck_Pgz^gg-8p_+2iJIie^Aa$Xsn@MN}*qkgYVMu-;k>}
zzW+dHq4h@|;w%q?xDq@0)prj|wUa!@STDt#=J;>;z~t4Kw0E2ctQ8GwEYf
zPJlheE}knrPjr~;h5Yw%kDM&=zS~$mc|LLuBlf!Gjcly}Eq>%X!~3xNJRIz}8{c8-
z%Jye`v!gv~&$S;2^ldg4O|jlMwTJR|`$bBJH~)&h$??xcy&wDyU2nyeEdn5p`4f#I
z>nINV+3Ongggag&`kkE1+TQvL3BIqF_2qar{_ZzY0=~WRqFsm8WDU+a;k0
ziR`=PBdtkv-2ooO3vwJ2xKx!RMFn_cPzGHyF}$
zyMHe`xuq^879fr=o75!8TM?H14GO*;oieBmiwN#Gwid*nC)UdvzJ_qe$Q6KRm4Xz!K6wQ
z^=;A2{e4(i{TfQeL?IA#8w?#k&ws0GGH}tF18b&Sq-JO(<2>_Q4j8_vZfWrLN+zrM
zPgo28syzFdWpWTq8@tXw$I*1bdnv7qJg+rcOCgxf&*~y~-bHq~bW%EDye*`G8&VOh
zK9q$|p8xZWt%c7o}a4ZSKEJf
z(SaYpan!2PkIUUR9ft{NSO17**Q5x|89~oY@^M$Y47ziRf$Gr>bEM|3bJJ9gDHj7H
zpr$pT@$x0rw4ZV}%$+^eY(r1{;XY-A$?h(8uy!cl)_6s@aSad1C
zv8DV#y}8(Ws<)0@F8hE>!q|qO*v43o4hPY0vCa!+J+$9Qmf-;q4hK5k4e8jdy({^0
zncMOoH#Vc<+_`t>bTuA74>O($Hu=JPn{H7t7YNA7A3d$RNFe?Z@K8(yY{VEfxsxcx
z5V1G5i~apUDr#4YOv@FReI|A#D$O?83XkeC&jv9C#VP?eeQgwuM&Gs;z8~{1#<)K~
z3=K#9#!d!KpI9G_v!dOluB&yL%t)oqR17OYN0r5B!*hf+gKDSxY=ypBD0fWus^Q&R
z%2_`i9cc^VnoYCo4i8i#_XKq
zCLJ9sVmw|n*ljZ&FPQUUlyAZf4Ko2QY^_6|Gk0o}vkvkg<~W7fE20WnQj1hY)f#P-
zy&`h@T@CzFzlrRZVFBrCvF6yX&g&1(gGxqu8EPH*!_Cf!Y&XLsm4BXyN|qJ#khC
zgVuNT9?774tV00v2Q{&RFM|ZoGr#<+Vy}f-;LlBW^13yx
zpR}t(A%U8H0pFb232{RKu0#VyFVI{z^i`9GC9VO4zkk!3F`3Mu#^vfnYIM@{LbOwmA|Stcy95BvDnM_Jn)|5tzQqzR$=5h&f1=rvF1Cx
z9dvn-Z$_pHITTtorm7ar{&U^N+;(-PgVtR+<*rRG^zQwWct3El7(%s0?O8=7XyK)L
z;`zuNl`LXB&)FyTzK68W0#10u@H*)^Os>@Mgz_6q2bcLu(Espy(+AF%rO%It2*eKp
zHXr$~1`3oAUrRch?zE9h1l+g1gFPN|OxEHl$k4|
zgCoj8gQL6XIx|tNb!1NVhCuPLQ&8iJoKf0Hd>KC4Gw5PJ5B$-3soTt5{ne
zAHupeIpRd(Z&%LKG@DH>+kAoP!nLGTp9k)UdQhp~@Qljzr;3pgUZLNm0FzVBABn>I
zL1T1H8r;Ar5pG%vL5R1y0S|#RNuX2@)XI<}@G%bc$r1aje{ho6go@UJq36?sU4F5pmBrdKo22LbG|4uS6CIrmj?nQQqc8iV?Em)|l<_BOn
z=_C)|LD9+%!|$I+=QEQ_C}St9f=PMsC&!W-ArBt{EdJ21GCnuLzLq()50{xo*8|N?
zg`Q%neotPQKrKLa{^mosMG%BU+v!nl)J%6S(dtwYiRA|W2_{YqU#F6{BfWN!`k1>%
zBoa0&!9R5IeAFwo(vlr@`Gu2a{oY)-h^PFK`bsM9tL*E_i1U36z+&6C%unYbi=SDy
zy?(DUMSnHB;p%9g0ML@Z|;_HRUC_`^dUx+LXPE^7bgm{lPbD
ze5uvWli`kF_065O&7EY>?se8j*T&|}=H_1dw!O6y?CSc}vfX?;9n<(gM1l(JR!woA
zumG_T>di2)(Maq%AvE7+Q}>1G1NNsZdIXg(@?~!W?!??9=LS2(uPyCB?l9E#=&Ed%
zaaVd@x`2(ALwBv^(LWD8d0OWhFxFIg91M9|wi
zyOM3w)IxOLYrwDCIO|893CgHO4%epeFS|e*1Ihn|G-=iTIr63
z-NX+&AA(ZUR2K}$BTG)a(Hup6xASA`Cf!zEa=c&TMe?c66PO`09FE;<@)t(L4
z{>X1ME?}UxMl^ccC;XgCiOse0IUuVQVOy{*7&jc~bHd&tNL{DY(;fvR;|Bs~?xw{m
zEX5`$YdG76A6FY4>oMk`MO`%Bs7&dt7~JZfYrm2`%@p3&6C#tM`|_aM+lN-pIEE%)
z^i&0}dEP?~-MRB3zI{n#E)6p5fvyjXGQlfZEBvMJ1V
zHe}--=6|%8O`*e|7BEbV@bI!xzd>r|`Vp(o(H)cJ`*R4J)+B0k
z9@P^?-yilw1L;AyarDNRcat1-I8-)jyC`gs^V`Q}Sat7kQ2)Y%(p9i*a^PCLI)6>f
zkz!Nanejo3oeW-RdM{kE<;0&7EZX%{XvXnC1`6NeMg!wL~6D*pp0E_;#Q9vO_&R2?_bFXUL*7zgkV1A0(QeKBU(7Z7wQxuOh>yY}YJj!^#dr;HmZZdnd(|
zZ^O;j;vA2<;*?15(R4}%A`el$9@K+b7Cfi^DQjDQ_u?1oD6M>XgpreWpsCFJxiON{q#$DdYL?poFu4Sb0iQTs@ZmVTnYRNc=psTS(0ejfB^@0;k(k`|cgc+6x4v7C`JuFq#vC;le
z+9lF&hdQ?Q9jwU{(~o`|c}@Z6nJxMLPCQ>bAR%hVp5=;t`oIsE%*TAYJB@oa<^@^y
zkZUSx=2%OQ%m>Y^l(-G`5xvWP(GEMw_hmYTzD<4we7-0)ssz>n3?sF=(!{o}Lw*BU
z{4(~k++ub2o<#S&%dNGJ$aKAADMQXgk!n(l`sT3^X5(C=y9T8#E1!*HDB^k3951X7
zmTctCoBU1`Kl|cwflM)R7ejt}o*s|;%x
ze7Z3>`ryvlaE!Yh^s9Bf(XiZxBsmHxoR+&|qE+Wtae=nTwH4m2xTc;G$xtw`_3!;fvl~!s*j~49AY6@wM7wm_|zy
zc`uJSieIyO#VDX~Ldc@i?|1PF-%AYxt3GtQg@X+0%qPIU-^IJ&q~gY&q}B)=Md3fQ
zUt~wd-v6Maop4Hk#~V%*$J5JI);H7@tC>RHQ2SzKZHgKlgj+X<7XI
zoB!k)E-&`p%XF%G(JPU34U?C*46M)=`kJR!a3`r+JuB3)7}o`=(Jo2>dgJ&XH-TMZ
zN@noZ4`GcFyL)d9isbDQ;9j~+>h=%X;-V|shC`k1x&_u>c@n^QC7-yG9D((CXSNU!#adjM*^)V}=-8
zojfeYLVhZL5zPOcGssm#h)WcrzX%;Mao^#&Rq*LhX;ii*{=wYmY+U0-0`~!6K#IXS
zuW-mU^HZ}fj~WlFs$BOgx&E>X9z>n~*Aihalw1W5@ubX0S)W0`OsL(zKBqgX@0fFv
zJ2TfWT0R>d01Q(MTO?Qu4$I5)>hS74EAQFzG_3h=70jd@)VnDz9qf?LomwBf
z%(QZ-_HS0@sIHrNMPyB3ea!N2YUXWBfk5|KMfOcMI;LN^;dwL8@7`uUN3f7<<
z*=G3Z=k_)aXAPfP(T+z(B34bt%L07jk`j>V{*-)CAYr*Sm!V(X?N^3_X0N&X2Er_?
z?451H(&fSGut!VP>^H7TS5b45OI?^&qUsMpKcKP`9+HpKa8LA@T>y1WGHngIp%z=o
z1j<{zZ1eKIiH7a(6oDI-@-eb@`epga1|I?Csz|
zG&33Dps~4Wu*g}(vp2U20OXu#x9T6M{da%6<*%b+RV7!dmqL?pcEFUq!eAsu$z^w5
z0w#c6QL?&%r+cf8FF?~eu8O>DZXLJsFSep|zG+xQc5QkJa8J1u31YKQkUEA|X!CJl
zOVuA4XZ+jJsiy)r%|a+AcCAQTbHC<<V_gnKu&va+@TcYc|&fNVX<{^O{_HgmXrg+@0~*wZK0s~Xp6d0|{zRPD2{#@AkU
z13))#Joe%!<|X#^^gsV)iX6oC7rfp-nQe67yB6ER@ecs*p*j3Hvts78uRIN(5xZg1
zPgcha*$^y2qS}WMp5MfQYAP~4bB1Pc&YGxH#jcW^{fEdYNgqu=Y>?(uBQzN*_sS%*
zE8f;yLUKI3mSQ#KvCnhVHTMfB*s`V_`h8Z-Z{E;nZ(1W*^S2h!X?CFQq&dq%c
zr<$P_2BGp6Z&uQ>;NNA^U}^j3ZF(2IQ$#JwpZyFwh@n|I4V@~-qoN>TO>109pHha<@y*p{!2pNrYAo^f<;TAChw*W=+p2%2*9aeL47==!^+7K^
z0^v(afTo{)&|bBZ=e{*4y%2akuJv2!R8e_X5J(35U_oZ&iiy{D!WPu@KKON@&CiMN
zyd$eSWVEQbxzaVjIwos_G=gz0Y}-zRj}Hc-4{A^t!!Z)=vx*LIxbF6{4Q|&cWTMqD
z!!|T%TZ?Ft0ZF-Jf6KhLw%P4-W5l*SZ~n%rWw`|S^bQ?FhGdz4sQDxSpS(-gCv4kH
zcBzEhSjQ6VU7hvTl9;cL?m>i-g>DFkEc?Efm~5I1wuEFexT4CYnlvLzcZM3iQY2sg
z3>IAANRd6>8V;PsdGMnPGRN*dK01srUz0L%NPxQeF->fy;iv#)zs25o
zHJ@Ru9^Ih{08oU(rg{d?WAw6F1DGx2o*M08Hw5d*tlDh?NB!MWH8GS_AWReTn4ZMQ
zy5mBkc#-7pvGzDffIvz{@9p=~D(!bk8a*K_amnu7uG%*MdYvcLJ5g0%U$-#d|Aku$
z+p|i|v|qg&6t^wTlLu%?Dn=a50M8oM!G)bYp(mD$d0FHgRB-EF2skR2A{EwxyA9Ux
z{pM_~7*2Go+sT?HCIF;qd(#s~Gzp*7vNzql#LoCqQZQE;9@_?DW$!F}X{_1(#I^2l
zqr`)1B~fG=f?qwo!Qz_qvg399jW8&sd`i4ZaO-2@(5H1`)PPr?U29h_$$nlEm7&7r+diI{Av=)zPaboGkF`w+q
z^*>_7)dV`b;*IVL9{hAj?8@~$bbme23vL=%wHOs9t*2n$aBy?CvC(g0Ec|~_F!PD;
zQvc!Hva@hc%!KTQzxdxm<5XFmN4AN|AmurlAKWS@$hXYb9Nr-dyxV_&He|gyx$ij%
z$@Djuiof3*?l+`#M_NMK@!g80OnpqJ$t|n(NH;2N#dER?pfq{#D9}RI?1!@zJIKH@;O*
zep1glv=IX;jyNB)~?ul12HGnxAP>)Dd_6G5_Uz~{QM?wN?3NB#XmonsO_IkSn
ziEIhN_{yB=XPob78qgpVb$|f6S>%
z2fGc24{Bi|sk_>%i>t_mQJ1Uyd4Vg2&5wpIRu>h1V36JUFn5^NcU~)V@iJr&@+Lub
z7t@;*iW1w-*@}K_)JL@owa=sy=)29-gVg*u#EZm(2@m1bYFcoA2XlybWr8LgK(laz
zA?CE#^G2^c!d;l%4LgA!1?|Y)fGf>i^
z^G$_jHYt95-^dU9%Oh@8VO>DTsr$g;DLjVZP&49IbZg?hA1qL2`CCad_t@PBSidQ&
z13nW|5~YxOxPyN5-db4elbU*(VJhJ#ogLHRbQCWI=JeF-3acQdg*s&36%kh^Nv(Oe
zODg`YA1y}%OWkTt&BIv9=+tDyq0QPDb^vb4iRnqvH@kgsll~3j)h*fWJX~p#V)e
zKd-?HH8&(s{w>np1t#(AMS!Z)dKbQ$de%*Kzyss2c_6c?A%1^nDj}ewtW7cQbBh!e
zdu$EmP6k_u2U>~kdXQ8u*Jb}sobu%sFgra81A1-4={kpMwo8I#8rzdu5`-cZKX|>=
ze6*q<(E8%mRPjYbP_0HM=-$D%-ld$j%xV+(A9^v#m_2$n+27bX!4o{lSfWyQ>H;49
zqg2q2W{u%yW=d9BZ`{~U2<`R||EM%rz4huT2D6sBWm@1jR)+4CA3Nq4u=T=Og^e_|
zVMA;OJrDqd1=8&g&
zR-Wa{Ww%Wkx7BW|1b(Bvi=1XU7Q0iwyhN2N{y2P#SK$dn)9H)y(f)gJYP0uYF=W59qz#rY1hhNYv@Y+9fs2`@F$Tjw
zif84nOXU>e7#3yk8FCeH(@#1b=vn1K2>wlf6^HSZN$%dSr^{ji0Krqiaf
zSkJA=wIkFDC1c;`78RkQrm_1p;i_%Vy%M+qErdY3&pM#Laflp
zvh+;8_Kwoh((d?IBqTM)r;aKOdX>2a2XptjGtegd31)1*66fYC5Vml%mn74~0#${sY-TB~KXT
zK%(y=joj8;x^w|%ke!EO&d5n(hZ=X=r3qxc_pV!=(+cA;vKP)@Xh8qVw6bV_F=RT<
zgp85@Zru-ooqFzPc8|)ACcfSPFC`RN@8BO#KVKw66oYs{KF_B%e>OxuX+x1{4)WsRN*sse5mPx>^)AD
zkvReHzsL2MWJhNcO$^g{$sq&ik#A2`E2@BdUIpO8{(9kuXqC<1Ob)WsSmxSP6bY~u
zdU;k+RVBa%{u5#sru?^;vCi=n*1lYAlTQ1gSS~&91|Q(&zucy2`~dVuDvRN;cyKTM?yYl*X@OPB-@rx1;Csmf&
zrG;yW=?Yp2Y?UdyKxD0A*y^4U)R=n5z>)D^O$MIgD!KmOH(;+jG3``1JSHbUJ9Vsl
zJV!)-ng-2P$v-FiRfMr?+{fF;}pOuf+jck|jAK*ds4Z_Sst6n@c0wwGJMFuRMYR
zo_KOAn|otuqUNCPEtRpy!P6nrJhs2>R|KIq=X*k8YXgkIIxJbI3I6L)H8D&&sPE0`
z2e69g%QWNQ*wt_(yQz>s%~x*=8W&^c7*V&pTc~AE**fm{F>g@)t1`%sx`eF(3Nzxc
z^cVc*n`GO@+vl-aWy2%l9q=*g6z?(mV*S%iYEnhH&=H
zI=JwJh~ZymX2UAuq3^}j(IMe~dHBNnrxzTCyuu#~P@h#dO`*s_J2LO1gKP`GKeUvhx&av0Q{OyQcqgniX_
zZ+c#633puN3e!t}zbH3zBmCfQLPnp|0ph(+R#C$6@ry4F%*
ztxntd(;+vaB=RrHt&iYM<0&R-2G<{&h&3El6%iY+;-4+NTt8ppMApk>IH)9TdJrEA
zT}z9!FnW0Dzoh6ExJ^Z=r2QQn;c^%*jwPNs8(zL>3R+-daT&0xabKFa_)9mVqJbz~
z|0ju;aXzGDSh4+^@9Ph3{z4f*UiHj(PL4|_PN4>5`$L
zG7IMsW$!@S`Z{IYYcZ<#c(V0
zHsET+h2((CbY1*4%v&pXSxn?aIS!_93GvLvdjj{agbWgEe0D7y(>K)`By2}vc_hKR
zZf3sVo%F6;6&rplqOiw1pXP^dboX73GAzb;dc0*AI+}e$fyF%zKNL0vkB<@`4!zIX
zAk+jN7ao&O-w5#YjDCCui>MuhW+8=XJo@x0e^O33Cy*Kk(tyt&OM_lSs6WrCl)w}9
zC0OD+AHl&R@3C7dgK{N0TtPt~0Srr(bf&s>!_2C0@a9^GUmg==sn2C$9qiD6KZj&_
zDNMjy>QR?ofpYuQv^w6jG|ng``vC{#=+B?Y`P8u0CX&*(r%VjrZkY(IbuMuMYxRG>
zEc!Ms!>S_rSkU_~ui_71LBhym2*PN^@Pkw^NCWp_&l=en>o}IY!vE1c7OW>ECR8{7
z%%^5)u@g}=^$Z`{6Vx({`dZQ2_H*ha?UJEInuL3;_VhS6zL?7?u6|uPlx&_J?`=Oi
z*ti(f@Q?Y(j4cbwsostMsh?SjcBDo-B4c-VQaw_KS2bp3@U4d@*AYIQJrR({&?l3s
z9HY*lZ@uJAT;cCa72KgyHh;P$c_FJgCCE$KiT4p7rhINmr}F)lnJO?6z?suXvEX|j
z<1&k&vis5N2-
zt-$H>&WGW9usuZy>Dv_1?J{U`PH3gb-)w&gWf*LkE&!j`O1eZhZ6UGNEE=(8CiUjf
zKZ0ZFqnOuw~KLyw#!)|9An-Rcr83PkiJTa@>FD3h?Mt
zb6e^B`k41G;A@R@633w!>cr(74}MTxdsJY01LhF?HuP*(bV2!8Ks?
z)rsI`*$s3(gm*cA=rGr)RwnrJ5%Do*m%e0}$FgYk-*^{%Bm4@p$~E$MeEJlZXXqcb
zxD7(7qSnCoE#+;R%2aPbgJCYgz~6OQT`fuvn)LnUv}%|iNn)^-`QYt|G^FL@+nY&a
z=r|v!pjC8b7@rK`F7T=fW7nI?SE2-B4nz-<4}8u7rhoMo@c&*xd9d9Ezuc^6ID%#g
zeD*EOW=gG||CjaDn>+CjgD1H-M0}kE7>=?HydNDOYaUb!?Wd%p2l6{3@SRKAaCW}c3$v>tWRIB|>tx#BrcEk`DYE~{4f2-{gSdDz$F1PS+A
zg_XiRIgR`RXCX*8hlEty={mf?l}MX_jNt8DB=^i8l(;0Vdyi2AKfP;#WHQ0mPX`)_
zWHXvKZ?HJ#H4K4W3`|)H;T)?kN~cA1YdEQ25xC);Fvkc^K`}^oy!Z0jWb@*PZcEep
zpZ;3ItA|S0!y`g%O=%rPKq;PeY8qnX%CGisnGU=5``Nmd53s~XeKJ?I^|u?BtSSbB
z;svL^I(xw&ys@Mfm8Cz#EoIR!630AxnyoxiW-#_h&5-*G*vU%=TgjC5BBdjE?qG!e
zCOi=EXl+8fnIbkLmDjZTA}FYwqbP`O$Zo@jUR8nxPR3;6()WLH&(VHiORel{VMtya
z{nd~ayR+)LvyaVk!O1T)1i5-6q&1AC-S2q9!(|x1_~ONwtLRo%ql4Bb3SboX!*?&(
zyp<5&u8~cTq6Ai)UoCVSIbf6U4B$c8u2r8YL%}LT8rSRREBG}x7((qd#z~BN9Nm$N
zt+g?rRBPpav#n-kn#vlyk73NcKjB|lA~DH(_Tg#E=$enV_){YU(w};Sat-2kXA-%c
z(4l%ocQ{-v=A-64GAr%-;OT9@p|PlLrAjpKo$*Q1xj@!XJn5wKr#kf{d)NiKnFpAB
zc*;L%H7n5Z_aW>V-NRI+p`~NQZ%FXva=>Y#&}ONWcu2&-&s&}uLvc?d
z@!wgEc+nbErmm9G^5tY&f7>sc^keUp<4VW`Js>gJ?O;kA61(P$)3u-(`0Tz!a~Xv$
z`}X~WY7wmQ`S2?iL(Y?1o&asn}Cct)$u8;X*am0$1==WRePYbwEjVF7&
zWXHnuFzA&Goz(f9oFXIASt#7XRyY}f4E7GHjhWAZd?S^yDT}1E3yx9?{n4SfKt-Pv2`5hRxDI=qKj7pwmUC9fNm9$L22cQ@eoFQPqqnLZcWq>D-aYD9
zg^=O3$jIZnp5U$iIb;FxI~T;3W7_g4QmBejW355*cVQiRxMT48VqSfQ)({{UNZ~_KPh(pl4i=cmc@@dzx+hJe0f0$#r
zy7j*8nm&QT>UUhgd4!_z66(D#+j}H(h70n0NOW)j(
zi2OZMZ9B++H6iE*R~GgWQS?0_I6@|+it>MkL${AJN)ygmH4U7`KA3L}dyBQE=c@y%)qY02GDsfqk4zYX&b_Jn
zxo^;1e*INZe38Zvv)a}crY*RTHb?`S_;EiaZ9^r#=fg2kG>~EgAt+NWZcU`oT&C!P
z1izU3``nwcb7}DZ#&<&kG_tMe%px!L|e)}6%Tp|8m&(xcksWSbm8-yPq>Y=3o-u(lFLn&$^rHrTK^
zCAI%w%(@Z3@f*Eg!FL))A0DP`DW{E7eup|~ZDne3u70i4@$De%^;w1zb9k8)w*jbN
z(xL5%=4y?T>iVur+k*