diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 8b55232..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,15 +0,0 @@
-[[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
deleted file mode 100644
index 0bb9f2f..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,630 +0,0 @@
-{
- "_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/requirement.txt b/requirement.txt
index 5ee4f33..711bb84 100644
Binary files a/requirement.txt and b/requirement.txt differ
diff --git a/src/Main.py b/src/Main.py
index 47026b1..7f79f81 100644
--- a/src/Main.py
+++ b/src/Main.py
@@ -7,26 +7,17 @@ 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 QTranslator, QStandardPaths, QDir
+from PySide6.QtCore import QTranslator
from PySide6.QtWidgets import QApplication
from gui.ALMainWindow import ALMainWindow
from gui.resources import ALResource
-from utils.ConfigManager import instance
+from boot.AppInitializer import initializeApp
-def initializeConfigManager():
-
- app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
- config_dir = os.path.join(app_dir, "config")
- if not QDir(config_dir).exists():
- QDir().mkpath(config_dir)
- instance(config_dir)
-
def main():
app = QApplication(sys.argv)
@@ -35,11 +26,11 @@ def main():
app.installTranslator(translator)
app.setStyle('Fusion')
app.setApplicationName("AutoLibrary")
- initializeConfigManager()
+ if not initializeApp():
+ sys.exit(-1)
window = ALMainWindow()
window.show()
- sys.exit(app.exec_())
-
+ sys.exit(app.exec())
if __name__ == "__main__":
diff --git a/src/base/LibTimeSelector.py b/src/base/LibTimeSelector.py
index 27234c5..b8df859 100644
--- a/src/base/LibTimeSelector.py
+++ b/src/base/LibTimeSelector.py
@@ -9,6 +9,8 @@ See the LICENSE file for details.
"""
import queue
+from datetime import datetime
+
from base.LibOperator import LibOperator
@@ -29,25 +31,33 @@ class LibTimeSelector(LibOperator):
super().__init__(input_queue, output_queue)
@staticmethod
- def _timeToMins(
+ def _timeStrToMins(
time_str: str
) -> int:
"""
Convert time string "HH:MM" to minutes since midnight.
+
+ Example:
+ "10:00" -> 600
+ "13:30" -> 810
"""
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
- def _minsToTime(
+ def _minsToTimeStr(
mins: int
) -> str:
"""
Convert minutes since midnight to time string "HH:MM".
+
+ Example:
+ 600 -> "10:00"
+ 810 -> "13:30"
"""
- hour, minute = divmod(mins, 60)
+ hour, minute = divmod(int(mins), 60)
return f"{hour:02d}:{minute:02d}"
@@ -99,11 +109,11 @@ class LibTimeSelector(LibOperator):
for time_opt in time_options:
# Parse time value based on context
if is_reserve:
+ # Reservation context: parse 'time' attribute
time_attr = time_opt.get_attribute("time")
if time_attr == "now":
- from datetime import datetime
now = datetime.now()
- time_val = now.hour * 60 + now.minute
+ time_val = now.hour*60 + now.minute
elif time_attr and time_attr.isdigit():
time_val = int(time_attr)
else:
@@ -114,9 +124,7 @@ class LibTimeSelector(LibOperator):
if not (time_attr and time_attr.isdigit()):
continue
time_val = int(time_attr)
-
- free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTime(time_val))
-
+ free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTimeStr(time_val))
actual_diff = time_val - target_time
abs_diff = abs(actual_diff)
@@ -125,11 +133,9 @@ class LibTimeSelector(LibOperator):
(abs_diff == best_time_diff and
((prefer_earlier and actual_diff <= 0) or
(not prefer_earlier and actual_diff >= 0)))):
-
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
-
if best_time_opt is not None:
return (best_time_opt, best_time_opt.text.strip(), best_actual_diff, free_times)
return (None, None, None, free_times)
diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py
index bd4f07b..4ccf570 100644
--- a/src/base/MsgBase.py
+++ b/src/base/MsgBase.py
@@ -7,9 +7,12 @@ 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 logging
import queue
import datetime
+from managers.log.LogManager import getLogger
+
class MsgBase:
"""
@@ -29,6 +32,18 @@ class MsgBase:
implement queue polling to retrieve and process messages.
"""
+ class TraceLevel:
+ """
+ Enum class for trace levels.
+
+ This class provides the trace levels for the logger.
+ """
+ DEBUG = logging.DEBUG
+ INFO = logging.INFO
+ WARNING = logging.WARNING
+ ERROR = logging.ERROR
+ CRITICAL = logging.CRITICAL
+
def __init__(
self,
input_queue: queue.Queue,
@@ -38,6 +53,10 @@ class MsgBase:
self._class_name = self.__class__.__name__
self._input_queue = input_queue
self._output_queue = output_queue
+ try:
+ self._logger = getLogger(self._class_name)
+ except RuntimeError:
+ self._logger = None
def _showMsg(
@@ -50,11 +69,25 @@ class MsgBase:
def _showTrace(
self,
- msg: str
+ msg: str,
+ level: int = logging.INFO,
+ no_log: bool = False
):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
+ if self._logger and not no_log:
+ self._logger.log(level, msg)
+
+
+ def _showLog(
+ self,
+ msg: str,
+ level: int = logging.INFO
+ ):
+
+ if self._logger:
+ self._logger.log(level, msg)
def _waitMsg(
diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py
new file mode 100644
index 0000000..47091fd
--- /dev/null
+++ b/src/boot/AppInitializer.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2025 - 2026 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
+
+from PySide6.QtCore import QStandardPaths, QDir
+
+from managers.log.LogManager import instance as logInstance
+from managers.config.ConfigManager import instance as configInstance
+from managers.driver.WebDriverManager import instance as webdriverInstance
+
+
+def initializeLogManager(
+) -> bool:
+
+ app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
+ log_dir = os.path.join(app_dir, "logs")
+ if not QDir(log_dir).exists():
+ if not QDir().mkpath(log_dir):
+ return False
+ logInstance(log_dir)
+ return True
+
+def initializeConfigManager(
+) -> bool:
+
+ logger = logInstance().getLogger("AppInitializer")
+
+ app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
+ old_config_dir = os.path.join(app_dir, "config")
+ new_config_dir = os.path.join(app_dir, "configs")
+ if QDir(old_config_dir).exists(): # old config dir exists
+ #we rename it to compatible with new version
+ logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir)
+ if not QDir().rename(old_config_dir, new_config_dir):
+ logger.error("重命名旧配置目录 %s 到 %s 失败", old_config_dir, new_config_dir)
+ return False
+ elif not QDir(new_config_dir).exists():
+ logger.info("初始化配置目录 %s", new_config_dir)
+ if not QDir().mkpath(new_config_dir):
+ logger.error("创建配置目录 %s 失败", new_config_dir)
+ return False
+ configInstance(new_config_dir)
+ return True
+
+def initializeWebDriverManager(
+) -> bool:
+
+ logger = logInstance().getLogger("AppInitializer")
+
+ app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
+ driver_dir = os.path.join(app_dir, "drivers")
+ logger.info("初始化驱动目录 %s", driver_dir)
+ if not QDir(driver_dir).exists():
+ logger.error("创建驱动目录 %s 失败", driver_dir)
+ if not QDir().mkpath(driver_dir):
+ logger.error("创建驱动目录 %s 失败", driver_dir)
+ return False
+ webdriverInstance(driver_dir)
+ return True
+
+def initializeApp(
+) -> bool:
+
+ if not initializeLogManager():
+ return False
+ if not initializeConfigManager():
+ return False
+ if not initializeWebDriverManager():
+ return False
+ return True
diff --git a/src/boot/__init__.py b/src/boot/__init__.py
new file mode 100644
index 0000000..393e4ca
--- /dev/null
+++ b/src/boot/__init__.py
@@ -0,0 +1,6 @@
+"""
+ Boot module for the AutoLibrary project.
+
+ Here are the classes and modules in this package:
+ - AppInitializer: Application initializer class.
+"""
\ No newline at end of file
diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py
index 976636a..97f190e 100644
--- a/src/gui/ALConfigWidget.py
+++ b/src/gui/ALConfigWidget.py
@@ -8,7 +8,6 @@ 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
@@ -21,7 +20,7 @@ from PySide6.QtGui import (
QCloseEvent, QAction
)
-import utils.ConfigManager as ConfigManager
+import managers.config.ConfigManager as ConfigManager
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
@@ -30,6 +29,7 @@ from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
from gui.ALSeatMapTable import ALSeatMapTable
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
+from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -81,6 +81,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
+ self.AutoDownloadWebDriverButton.clicked.connect(self.onAutoDownloadWebDriverButtonClicked)
self.BrowseCurrentRunConfigButton.clicked.connect(self.onBrowseCurrentRunConfigButtonClicked)
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
self.BrowseExportRunConfigButton.clicked.connect(self.onBrowseExportRunConfigButtonClicked)
@@ -949,6 +950,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if browser_driver_path:
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
+
+ @Slot()
+ def onAutoDownloadWebDriverButtonClicked(
+ self
+ ):
+
+ dialog = ALWebDriverDownloadDialog(self)
+ dialog.show()
+ dialog.exec_()
+ selected_driver_info = dialog.getSelectedDriverInfo()
+ if selected_driver_info and selected_driver_info.driver_path:
+ self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
+ self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
+
+
@Slot()
def onBrowseCurrentRunConfigButtonClicked(
self
diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py
index b0fb9d2..f1744b5 100644
--- a/src/gui/ALMainWindow.py
+++ b/src/gui/ALMainWindow.py
@@ -19,7 +19,7 @@ from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
)
-import utils.ConfigManager as ConfigManager
+import managers.config.ConfigManager as ConfigManager
from base.MsgBase import MsgBase
@@ -59,6 +59,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
+ self._showLog("主窗口初始化完成")
def modifyUi(
@@ -113,7 +114,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
if not QSystemTrayIcon.isSystemTrayAvailable():
- self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标")
+ self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING)
return
self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon.setToolTip("AutoLibrary")
@@ -186,6 +187,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__alConfigWidget:
self.__alConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
+ self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event)
@@ -298,7 +300,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
+ self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.setControlButtons(True, None, None)
+ self._showLog("配置窗口已关闭,配置文件路径已更新")
@Slot(dict)
def onTimerTaskIsReady(
@@ -330,7 +334,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
1000
)
self._showTrace(
- f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
+ f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
)
if not is_error:
self.timerTaskIsExecuted.emit(timer_task)
@@ -346,6 +350,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alTimerTaskManageWidget.raise_()
self.__alTimerTaskManageWidget.activateWindow()
self.TimerTaskManageWidgetButton.setEnabled(False)
+ self._showLog("打开定时任务管理窗口")
@Slot()
def onConfigButtonClicked(
@@ -359,6 +364,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False)
+ self._showLog("打开配置窗口")
@Slot()
def onStartButtonClicked(
@@ -375,6 +381,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked)
self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
self.__auto_lib_thread.start()
+ self._showLog("开始手动执行任务")
@Slot()
def onStopButtonClicked(
@@ -382,14 +389,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
):
if self.__auto_lib_thread:
- self._showTrace("正在停止操作......")
+ self._showTrace("正在停止操作......", no_log=True)
self.__auto_lib_thread.wait(2000)
- self._showTrace("操作已停止")
+ self._showTrace("操作已停止", no_log=True)
self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None
self.setControlButtons(None, False, True)
+ self._showLog("任务已停止")
@Slot()
def onSendButtonClicked(
diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py
index 4fee821..36e8950 100644
--- a/src/gui/ALMainWorkers.py
+++ b/src/gui/ALMainWorkers.py
@@ -44,9 +44,11 @@ class AutoLibWorker(MsgBase, QThread):
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
self._showTrace(
- "当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试"
+ "当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试",
+ self.TraceLevel.WARNING
)
return False
+ self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO)
return True
@@ -57,8 +59,12 @@ class AutoLibWorker(MsgBase, QThread):
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
- self._showTrace("配置文件路径不存在, 请检查配置文件路径是否正确")
+ self._showTrace(
+ "配置文件路径不存在, 请检查配置文件路径是否正确",
+ self.TraceLevel.ERROR
+ )
return False
+ self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO)
return True
@@ -67,22 +73,28 @@ class AutoLibWorker(MsgBase, QThread):
) -> bool:
self._showTrace(
- f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
+ f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}",
+ no_log=True
)
self.__run_config = JSONReader(self.__config_paths["run"]).data()
self._showTrace(
- f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
+ f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}",
+ no_log=True
)
self.__user_config = JSONReader(self.__config_paths["user"]).data()
if self.__run_config is None or self.__user_config is None:
- self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
- self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
+ self._showTrace(
+ "配置文件加载失败, 请检查配置文件是否正确",
+ self.TraceLevel.ERROR
+ )
return False
if not self.__user_config.get("groups"):
self._showTrace(
- "用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
+ "用户配置文件中无有效任务组, 请检查用户配置文件是否正确",
+ self.TraceLevel.WARNING
)
return False
+ self._showLog(f"配置文件加载成功, 任务组数量: {len(self.__user_config.get('groups', []))}", self.TraceLevel.INFO)
return True
@@ -108,14 +120,17 @@ class AutoLibWorker(MsgBase, QThread):
groups = self.__user_config.get("groups")
for group in groups:
if not group["enabled"]:
- self._showTrace(f"任务组 {group["name"]} 已跳过")
+ self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True)
continue
- self._showTrace(f"正在运行任务组 {group["name"]}")
+ self._showTrace(f"正在运行任务组 {group["name"]}", no_log=True)
auto_lib.run(
{ "users": group.get("users", []) }
)
except Exception as e:
- self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
+ self._showTrace(
+ f"AutoLibrary 运行时发生异常 : {e}",
+ self.TraceLevel.ERROR
+ )
self.autoLibWorkerFinishedWithError.emit()
return
if auto_lib:
@@ -154,7 +169,10 @@ class TimerTaskWorker(AutoLibWorker):
self
):
- self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
+ self._showTrace(
+ f"定时任务 {self.__timer_task['name']} 运行时发生异常",
+ self.TraceLevel.ERROR
+ )
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
@Slot()
diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py
new file mode 100644
index 0000000..7b01648
--- /dev/null
+++ b/src/gui/ALStatusLabel.py
@@ -0,0 +1,246 @@
+
+from enum import Enum
+
+from PySide6.QtWidgets import (
+ QLabel
+)
+from PySide6.QtCore import (
+ Qt, Property, QPropertyAnimation, QEasingCurve
+)
+from PySide6.QtGui import (
+ QPainter, QColor, QConicalGradient, QPalette
+)
+
+
+class ALStatusLabel(QLabel):
+
+ class Status(Enum):
+ """
+ Enum class for representing the status of ALStatusLabel.
+ """
+
+ WAITING = 0
+ RUNNING = 1
+ SUCCESS = 2
+ WARNING = 3
+ FAILURE = 4
+
+ def __init__(
+ self,
+ parent = None
+ ):
+
+ super().__init__(parent)
+ self.__status = self.Status.WAITING
+ self.__icon_angle = 0
+
+ self.setupUi()
+
+
+ def setupUi(
+ self
+ ):
+
+ self.setFixedSize(36, 36)
+ self.setAlignment(Qt.AlignmentFlag.AlignCenter)
+
+ self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
+ self.RunningAnimation.setDuration(1000)
+ self.RunningAnimation.setStartValue(0)
+ self.RunningAnimation.setEndValue(-360)
+ self.RunningAnimation.setLoopCount(-1)
+ self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
+
+
+ def isDarkMode(
+ self
+ ) -> bool:
+
+ return self.palette().color(QPalette.ColorRole.Window).value() < 128
+
+
+ def getMarkColor(
+ self
+ ) -> QColor:
+
+ return QColor("#FFFFFF") if self.isDarkMode() else QColor("#454545")
+
+ @Property(Status)
+ def status(
+ self
+ ) -> Status:
+
+ return self.__status
+
+ @Property(int)
+ def iconAngle(
+ self
+ ) -> int:
+
+ return self.__icon_angle
+
+ @status.setter
+ def status(
+ self,
+ status: Status
+ ):
+
+ if status not in self.Status:
+ raise ValueError(f"Invalid (class)Status[enum.Enum] value: {status}")
+ self.__status = status
+ if self.__status == self.Status.RUNNING:
+ self.RunningAnimation.start()
+ else:
+ self.RunningAnimation.stop()
+ self.update()
+
+ @iconAngle.setter
+ def iconAngle(
+ self,
+ value: int
+ ):
+
+ self.__icon_angle = value
+ self.update()
+
+
+ def paintEvent(
+ self,
+ event
+ ):
+
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+ center_x = self.width()/2
+ center_y = self.height()/2
+ radius = min(center_x, center_y) - 3
+ match self.__status:
+ case self.Status.WAITING:
+ pen = painter.pen()
+ pen.setWidth(2)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ pen.setColor(QColor("#969696")) # grey
+ painter.setPen(pen)
+ painter.drawEllipse(
+ int(center_x - radius),
+ int(center_y - radius),
+ int(radius*2),
+ int(radius*2)
+ )
+ case self.Status.RUNNING:
+ gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
+ gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
+ gradient.setColorAt(1.0, QColor("#2294FF00"))
+ pen = painter.pen()
+ pen.setWidth(3)
+ pen.setBrush(gradient)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ painter.setPen(pen)
+ painter.drawEllipse(
+ int(center_x - radius),
+ int(center_y - radius),
+ int(radius*2),
+ int(radius*2)
+ )
+ case self.Status.SUCCESS:
+ # draw the success green circle
+ pen = painter.pen()
+ pen.setWidth(2)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
+ painter.setPen(pen)
+ painter.drawEllipse(
+ int(center_x - radius),
+ int(center_y - radius),
+ int(radius*2),
+ int(radius*2)
+ )
+ # draw the success check mark '✓'
+ painter.setPen(Qt.PenStyle.SolidLine)
+ pen = painter.pen()
+ pen.setWidth(3)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ # white when dark mode, black when light mode
+ pen.setColor(self.getMarkColor())
+ painter.setPen(pen)
+ mark_size = radius/2
+ mark_path = [
+ (center_x - mark_size, center_y),
+ (center_x - mark_size/3, center_y + mark_size/2),
+ (center_x + mark_size, center_y - mark_size/2)
+ ]
+ painter.drawLine(
+ int(mark_path[0][0]),int(mark_path[0][1]),
+ int(mark_path[1][0]),int(mark_path[1][1])
+ )
+ painter.drawLine(
+ int(mark_path[1][0]),int(mark_path[1][1]),
+ int(mark_path[2][0]),int(mark_path[2][1])
+ )
+ case self.Status.WARNING:
+ # draw the warning orange circle
+ pen = painter.pen()
+ pen.setWidth(2)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ pen.setColor(QColor("#FF9800")) # orange
+ painter.setPen(pen)
+ painter.drawEllipse(
+ int(center_x - radius),
+ int(center_y - radius),
+ int(radius*2),
+ int(radius*2)
+ )
+ # draw the warning exclamation mark '!'
+ painter.setPen(Qt.PenStyle.SolidLine)
+ pen = painter.pen()
+ pen.setWidth(3)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ # white when dark mode, black when light mode
+ pen.setColor(self.getMarkColor())
+ painter.setPen(pen)
+ painter.drawLine(
+ int(center_x), int(center_y - radius/2),
+ int(center_x), int(center_y + radius/6)
+ )
+ painter.drawPoint(
+ int(center_x), int(center_y + radius/2)
+ )
+ case self.Status.FAILURE:
+ # draw the failure red circle
+ pen = painter.pen()
+ pen.setWidth(2)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ pen.setColor(QColor("#DC0000")) # red
+ painter.setPen(pen)
+ painter.drawEllipse(
+ int(center_x - radius),
+ int(center_y - radius),
+ int(radius*2),
+ int(radius*2)
+ )
+ # draw the failure cross mark '✗'
+ painter.setPen(Qt.PenStyle.SolidLine)
+ pen = painter.pen()
+ pen.setWidth(3)
+ pen.setBrush(Qt.BrushStyle.NoBrush)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ # white when dark mode, black when light mode
+ pen.setColor(self.getMarkColor())
+ painter.setPen(pen)
+ mark_size = radius/3
+ painter.drawLine(
+ int(center_x - mark_size), int(center_y - mark_size),
+ int(center_x + mark_size), int(center_y + mark_size)
+ )
+ painter.drawLine(
+ int(center_x + mark_size), int(center_y - mark_size),
+ int(center_x - mark_size), int(center_y + mark_size)
+ )
+ painter.end()
+ super().paintEvent(event)
diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py
index dde52b9..63fec09 100644
--- a/src/gui/ALTimerTaskAddDialog.py
+++ b/src/gui/ALTimerTaskAddDialog.py
@@ -121,11 +121,11 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
)
task_data = {
"name": name,
- "task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
+ "uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time,
"silent": silent,
- "add_time": added_time,
+ "added_time": added_time,
"status": ALTimerTaskStatus.PENDING,
"executed": False,
"repeat": self.RepeatCheckBox.isChecked(),
@@ -158,7 +158,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
task_data["repeat_minute"],
task_data["repeat_second"]
)
-
return task_data
@Slot(int)
diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py
index 99195bb..f054b15 100644
--- a/src/gui/ALTimerTaskHistoryDialog.py
+++ b/src/gui/ALTimerTaskHistoryDialog.py
@@ -47,16 +47,16 @@ class ALTimerTaskHistoryDialog(QDialog):
MainLayout = QVBoxLayout(self)
InfoLayout = QGridLayout()
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
- TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 14px;")
+ TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 12px;")
InfoLayout.addWidget(TaskNameLabel, 0, 0)
- TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}")
- TaskUUIDLabel.setStyleSheet("font-size: 10px;")
+ TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('uuid', '未命名')}")
+ TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;")
InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
InfoLayout.setColumnStretch(0, 1)
if self.__task_data.get("repeat", False):
- RepeatLabel = QLabel("重复任务")
- RepeatLabel.setStyleSheet("color: #2294FF; font-weight: bold; font-size: 12px;")
+ RepeatLabel = QLabel("可重复性任务")
+ RepeatLabel.setStyleSheet("color: #2294FF; font-size: 12px;")
InfoLayout.addWidget(RepeatLabel, 0, 1)
MainLayout.addLayout(InfoLayout)
self.HistoryTableWidget = QTableWidget()
diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py
index 3fa2b65..74060a6 100644
--- a/src/gui/ALTimerTaskManageWidget.py
+++ b/src/gui/ALTimerTaskManageWidget.py
@@ -25,7 +25,7 @@ from PySide6.QtGui import (
QCloseEvent
)
-import utils.ConfigManager as ConfigManager
+import managers.config.ConfigManager as ConfigManager
import utils.TimerUtils as TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
@@ -221,7 +221,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]:
- task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
+ task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
task["status"] = ALTimerTaskStatus(task["status"])
if "history" in task:
@@ -245,7 +245,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
try:
for task in timer_tasks:
- task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
+ task["added_time"] = task["added_time"].strftime("%Y-%m-%d %H:%M:%S")
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value
if "history" in task:
@@ -309,7 +309,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
)
elif policy == self.SortPolicy.BY_ADD_TIME:
self.__timer_tasks.sort(
- key = lambda x: x["add_time"],
+ key = lambda x: x["added_time"],
reverse = order is Qt.SortOrder.DescendingOrder
)
elif policy == self.SortPolicy.BY_EXECUTE_TIME:
@@ -378,7 +378,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit()
-
@staticmethod
def getTimerTaskDetailMessage(
timer_task: dict
@@ -386,10 +385,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
return (
f"任务名称:{timer_task["name"]}\n"
- f"添加时间:{timer_task["add_time"]}\n"
+ f"添加时间:{timer_task["added_time"]}\n"
f"当前状态:{timer_task["status"].value}\n"
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n"
- f"已执行次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
+ f"已记录次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
)
@@ -414,10 +413,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
result = msgbox.exec()
if result != QMessageBox.StandardButton.Yes:
return
- task_uuid = timer_task["task_uuid"]
+ task_uuid = timer_task["uuid"]
self.__timer_tasks = [
x for x in self.__timer_tasks
- if x["task_uuid"] != task_uuid
+ if x["uuid"] != task_uuid
]
self.timerTasksChanged.emit()
@@ -447,7 +446,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
QMessageBox.warning(
self,
"警告 - AutoLibrary",
- f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !"
+ f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !"
)
return
# repeat tasks ask before clear
@@ -464,7 +463,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText(
- f"存在 {repeat_tasks_count} 个可重复性任务,\n"
+ f"存在 {repeat_tasks_count} 个可重复性任务,\n"
"删除可重复性任务将同时删除所有已执行的记录 !\n"
"是否继续 ?"
)
@@ -563,7 +562,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
):
for task in self.__timer_tasks:
- if task["task_uuid"] == timer_task["task_uuid"]:
+ if task["uuid"] == timer_task["uuid"]:
task["status"] = ALTimerTaskStatus.RUNNING
break
self.timerTasksChanged.emit()
@@ -575,17 +574,37 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_task: dict
) -> dict:
+ # only these status are valid
+ valid_statuses = {ALTimerTaskStatus.EXECUTED, ALTimerTaskStatus.ERROR,
+ ALTimerTaskStatus.OUTDATED}
+ if status not in valid_statuses:
+ return timer_task
if "history" not in timer_task:
timer_task["history"] = []
- executed_time = datetime.now()
- duration = (executed_time - timer_task["execute_time"]).total_seconds()
- timer_task["history"].append({
- "execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
- "executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
- "result": status,
- "duration": duration if status is ALTimerTaskStatus.EXECUTED else 0,
- "uuid": timer_task["task_uuid"]
- })
+ if status != ALTimerTaskStatus.OUTDATED:
+ executed_time = datetime.now()
+ duration = (executed_time - timer_task["execute_time"]).total_seconds()
+ timer_task["history"].append({
+ "execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
+ "executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
+ "result": status,
+ "duration": duration,
+ "uuid": timer_task["uuid"]
+ })
+ else:
+ current_time = datetime.now()
+ execute_time = timer_task["execute_time"]
+ execute_weekday = execute_time.weekday()
+ delta_days = (current_time - execute_time).days
+ for i in range(delta_days + 1):
+ if (execute_weekday + i)%7 in timer_task["repeat_days"]:
+ timer_task["history"].append({
+ "execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
+ "executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
+ "result": status,
+ "duration": 0,
+ "uuid": timer_task["uuid"]
+ })
next_time = TimerUtils.calculateNextRepeatTime(
timer_task["repeat_days"],
timer_task["repeat_hour"],
@@ -598,6 +617,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_task["executed"] = False
else:
timer_task["status"] = status
+ return timer_task
@Slot(dict)
def onTimerTaskIsExecuted(
@@ -606,7 +626,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
):
for task in self.__timer_tasks:
- if task["task_uuid"] == timer_task["task_uuid"]:
+ if task["uuid"] == timer_task["uuid"]:
if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task)
else:
@@ -621,7 +641,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
):
for task in self.__timer_tasks:
- if task["task_uuid"] == timer_task["task_uuid"]:
+ if task["uuid"] == timer_task["uuid"]:
if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task)
else:
diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py
new file mode 100644
index 0000000..59571c5
--- /dev/null
+++ b/src/gui/ALWebDriverDownloadDialog.py
@@ -0,0 +1,576 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 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 threading
+from typing import Optional
+
+from PySide6.QtCore import (
+ Qt, Slot, QThread, Signal
+)
+from PySide6.QtWidgets import (
+ QDialog, QLabel, QComboBox, QProgressBar,
+ QPushButton, QVBoxLayout, QHBoxLayout,
+ QMessageBox, QFrame, QLineEdit
+)
+from PySide6.QtGui import (
+ QCloseEvent
+)
+
+from managers.driver.WebDriverManager import (
+ instance as webdriver_manager_instance,
+ WebDriverManager, WebDriverInfo, WebDriverType,
+ WebDriverStatus
+)
+from gui.ALStatusLabel import ALStatusLabel
+
+
+class DownloadWorker(QThread):
+ """
+ Worker thread for downloading web drivers.
+ """
+
+ progress = Signal(float, int, float, str)
+ downloadFinished = Signal(object, str)
+ downloadError = Signal(str)
+ downloadCancelled = Signal()
+
+ def __init__(
+ self,
+ driver_manager: WebDriverManager,
+ driver_info: WebDriverInfo
+ ):
+ super().__init__()
+ self.__driver_manager = driver_manager
+ self.__driver_info = driver_info
+ self.__driver_path = None
+ self.__cancelled = False
+ self.__cancel_event = threading.Event()
+
+ def cancel(
+ self
+ ):
+ """
+ Cancel the download operation.
+ """
+
+ self.__cancelled = True
+ self.__cancel_event.set()
+
+ def run(
+ self
+ ):
+ try:
+ if self.__cancelled:
+ self.downloadCancelled.emit()
+ return
+ self.__driver_path = self.__driver_manager.installDriver(
+ self.__driver_info,
+ progress_callback=self.onProgress,
+ cancel_event=self.__cancel_event
+ )
+ if self.__cancelled:
+ self.downloadCancelled.emit()
+ return
+ if self.__driver_path:
+ self.downloadFinished.emit(self.__driver_path, "")
+ else:
+ self.downloadError.emit("下载失败: 未返回有效路径")
+ except Exception as e:
+ if not self.__cancelled:
+ self.downloadError.emit(f"下载失败: {str(e)}")
+
+ def onProgress(
+ self,
+ downloaded: float,
+ total: int,
+ speed: float,
+ message: str
+ ):
+
+ if self.__cancel_event.is_set():
+ self.__cancelled = True
+ if not self.__cancelled:
+ self.progress.emit(downloaded, total, speed, message)
+
+ def stop(
+ self
+ ):
+ """
+ Cancel and wait for the thread to finish.
+ Must only be called from the main thread.
+ """
+
+ self.cancel()
+ if not self.isFinished():
+ if not self.wait(5000):
+ self.terminate()
+ self.wait()
+
+
+class ALWebDriverDownloadDialog(QDialog):
+
+ def __init__(
+ self,
+ parent: Optional[QDialog] = None,
+ driver_dir: str = ""
+ ):
+ """
+ Web driver download dialog.
+
+ Args:
+ parent: Parent widget.
+ driver_dir: Driver directory path.
+ """
+
+ super().__init__(parent)
+
+ self.__driver_dir = driver_dir
+ self.__driver_manager: Optional[WebDriverManager] = None
+ self.__confirmed = False
+ self.__selected_driver_info: Optional[WebDriverInfo] = None
+ self.__driver_infos: list[WebDriverInfo] = []
+ self.__download_thread: Optional[DownloadWorker] = None
+
+ self.setupUi()
+ self.connectSignals()
+ self.initializeDriverManager()
+ self.refreshDriverList()
+
+
+ def showEvent(
+ self,
+ event
+ ):
+
+ result = super().showEvent(event)
+ if self.parent():
+ screen_rect = self.screen().geometry()
+ target_pos = self.parent().geometry().center()
+ target_pos.setX(target_pos.x() - self.width()//2)
+ target_pos.setY(target_pos.y() - self.height()//2)
+ if target_pos.x() < 0:
+ target_pos.setX(0)
+ if target_pos.x() + self.width() > screen_rect.width():
+ target_pos.setX(screen_rect.width() - self.width())
+ if target_pos.y() < 0:
+ target_pos.setY(0)
+ if target_pos.y() + self.height() > screen_rect.height():
+ target_pos.setY(screen_rect.height() - self.height())
+ self.move(target_pos)
+ return result
+
+
+ def setupUi(
+ self
+ ):
+
+ self.setModal(True)
+ self.setMaximumHeight(240)
+ self.setMinimumHeight(240)
+ self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
+
+ self.MainLayout = QVBoxLayout(self)
+ self.MainLayout.setContentsMargins(5, 5, 5, 5)
+ self.MainLayout.setSpacing(5)
+
+ self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
+ self.MainLayout.addWidget(self.BrowserCountLabel)
+
+ self.DriverInfoLayout = QHBoxLayout()
+ self.DriverInfoLayout.setSpacing(5)
+ self.DriverComboBox = QComboBox()
+ self.DriverInfoLayout.addWidget(self.DriverComboBox)
+ self.StatusLabel = ALStatusLabel()
+ self.StatusLabel.setFixedSize(32, 32)
+ self.DriverInfoLayout.addWidget(self.StatusLabel)
+ self.MainLayout.addLayout(self.DriverInfoLayout)
+
+ self.DetailLayout = QVBoxLayout()
+ self.DetailLayout.setSpacing(5)
+ self.DetailLayout.setContentsMargins(5, 5, 5, 5)
+ self.BrowserTypeLabel = QLabel("类型:")
+ self.DetailLayout.addWidget(self.BrowserTypeLabel)
+ self.VersionLabel = QLabel("版本:")
+ self.DetailLayout.addWidget(self.VersionLabel)
+ self.PathLabel = QLineEdit()
+ self.PathLabel.setReadOnly(True)
+ self.PathLabel.setText("路径:未安装")
+ self.DetailLayout.addWidget(self.PathLabel)
+ self.MainLayout.addLayout(self.DetailLayout)
+
+ self.Line = QFrame()
+ self.Line.setFrameShape(QFrame.Shape.HLine)
+ self.Line.setFrameShadow(QFrame.Shadow.Sunken)
+ self.MainLayout.addWidget(self.Line)
+ self.ProgressBar = QProgressBar()
+ self.ProgressBar.setValue(0)
+ self.ProgressBar.setTextVisible(False)
+ self.MainLayout.addWidget(self.ProgressBar)
+ self.ProgressText = QLabel("")
+ self.ProgressText.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.MainLayout.addWidget(self.ProgressText)
+ self.ControlLayout = QHBoxLayout()
+ self.ControlLayout.setSpacing(8)
+ self.ControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
+ self.RefreshButton = QPushButton("刷新")
+ self.RefreshButton.setFixedSize(80, 25)
+ self.DownloadButton = QPushButton("下载驱动")
+ self.DownloadButton.setFixedSize(80, 25)
+ self.DeleteButton = QPushButton("删除驱动")
+ self.DeleteButton.setFixedSize(80, 25)
+ self.CancelButton = QPushButton("取消")
+ self.CancelButton.setFixedSize(80, 25)
+ self.ConfirmButton = QPushButton("确认")
+ self.ConfirmButton.setFixedSize(80, 25)
+ self.ConfirmButton.setEnabled(False)
+
+ self.ControlLayout.addWidget(self.RefreshButton)
+ self.ControlLayout.addWidget(self.DownloadButton)
+ self.ControlLayout.addWidget(self.DeleteButton)
+ self.ControlLayout.addWidget(self.CancelButton)
+ self.ControlLayout.addWidget(self.ConfirmButton)
+ self.MainLayout.addLayout(self.ControlLayout)
+
+
+ def connectSignals(
+ self
+ ):
+
+ self.RefreshButton.clicked.connect(self.onRefreshButtonClicked)
+ self.DownloadButton.clicked.connect(self.onDownloadButtonClicked)
+ self.DeleteButton.clicked.connect(self.onDeleteButtonClicked)
+ self.CancelButton.clicked.connect(self.onCancelButtonClicked)
+ self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
+ self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
+
+
+ def initializeDriverManager(
+ self
+ ):
+
+ try:
+ self.__driver_manager = webdriver_manager_instance(self.__driver_dir)
+ except ValueError as e:
+ QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
+ self.reject()
+
+
+ def refreshDriverList(
+ self
+ ):
+
+ if not self.__driver_manager:
+ return
+ self.__driver_manager.refresh()
+ self.__driver_infos = self.__driver_manager.getDriverInfos()
+ self.DriverComboBox.clear()
+ installed_idx = 0
+ for i, driver_info in enumerate(self.__driver_infos):
+ if driver_info.driver_status == WebDriverStatus.INSTALLED:
+ installed_idx = i # get the installed driver index
+ display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
+ self.DriverComboBox.addItem(display_text)
+ count = len(self.__driver_infos)
+ self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:")
+ if self.__driver_infos:
+ self.DriverComboBox.setCurrentIndex(installed_idx)
+
+
+ def onDriverComboBoxChanged(
+ self,
+ index: int
+ ):
+
+ if not self.__driver_infos or index < 0 or index >= len(self.__driver_infos):
+ return
+ driver_info = self.__driver_infos[index]
+ self.updateDriverInfoDisplay(driver_info)
+ self.updateProgressBarStates(driver_info)
+ self.updateButtonStates(driver_info)
+
+
+ @Slot()
+ def onRefreshButtonClicked(
+ self
+ ):
+
+ self.refreshDriverList()
+
+
+ @Slot()
+ def onDeleteButtonClicked(
+ self
+ ):
+
+ index = self.DriverComboBox.currentIndex()
+ if index < 0 or index >= len(self.__driver_infos):
+ return
+ driver_info = self.__driver_infos[index]
+ if driver_info.driver_status.name != "INSTALLED":
+ QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除")
+ return
+ reply = QMessageBox.question(
+ self,
+ "确认删除 - AutoLibrary",
+ f"确定要删除 {driver_info.driver_type.value} 驱动吗 ?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
+ )
+ if reply == QMessageBox.StandardButton.No:
+ return
+ try:
+ self.__driver_manager.uninstallDriver(driver_info)
+ self.refreshDriverList()
+ QMessageBox.information(self, "删除成功 - AutoLibrary", "驱动已成功删除")
+ except Exception as e:
+ QMessageBox.critical(self, "删除失败 - AutoLibrary", f"删除驱动时出错:\n{str(e)}")
+
+ @Slot()
+ def onDownloadButtonClicked(
+ self
+ ):
+
+ self.DriverComboBox.setEnabled(False)
+ index = self.DriverComboBox.currentIndex()
+ if index < 0 or index >= len(self.__driver_infos):
+ return
+ driver_info = self.__driver_infos[index]
+ if driver_info.driver_status == WebDriverStatus.INSTALLED:
+ return
+ driver_info.driver_status = WebDriverStatus.DOWNLOADING # we set this only to update
+ # the display, and we will set to not installed in the download thread
+ self.updateDriverInfoDisplay(driver_info)
+ self.updateProgressBarStates(driver_info)
+ self.ProgressText.setText("准备开始下载...")
+ self.updateButtonStates(driver_info)
+ # set to not installed
+ driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
+ self.__download_thread = DownloadWorker(self.__driver_manager, driver_info)
+ self.__download_thread.progress.connect(self.onDownloadProgress)
+ self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
+ self.__download_thread.downloadError.connect(self.onDownloadError)
+ self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
+ self.__download_thread.finished.connect(self.__onThreadFinished)
+ self.__download_thread.start()
+
+ @Slot()
+ def onDownloadProgress(
+ self,
+ downloaded: float,
+ total: int,
+ speed: float,
+ message: str
+ ):
+
+ progress = downloaded
+ self.ProgressBar.setValue(progress)
+ if speed >= 1024:
+ speed_text = f"{speed/1024:.1f} MB/s"
+ else:
+ speed_text = f"{speed:.1f} KB/s"
+ progress_text = f"{message}... {downloaded:.1f}% - {speed_text}"
+ self.ProgressText.setText(progress_text)
+
+ @Slot()
+ def onDownloadFinished(
+ self
+ ):
+
+ self.DriverComboBox.setEnabled(True)
+ index = self.DriverComboBox.currentIndex()
+ if 0 <= index < len(self.__driver_infos):
+ driver_info = self.__driver_infos[index]
+ driver_info.driver_status = WebDriverStatus.INSTALLED
+ self.updateDriverInfoDisplay(driver_info)
+ self.updateProgressBarStates(driver_info)
+ self.updateButtonStates(driver_info)
+
+ @Slot()
+ def onDownloadError(
+ self,
+ error_message: str
+ ):
+
+ self.DriverComboBox.setEnabled(True)
+ index = self.DriverComboBox.currentIndex()
+ if 0 <= index < len(self.__driver_infos):
+ driver_info = self.__driver_infos[index]
+ driver_info.driver_status = WebDriverStatus.ERROR
+ self.updateDriverInfoDisplay(driver_info)
+ self.updateProgressBarStates(driver_info)
+ self.updateButtonStates(driver_info)
+ QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
+
+
+ @Slot()
+ def onDownloadCancelled(
+ self
+ ):
+
+ self.DriverComboBox.setEnabled(True)
+ index = self.DriverComboBox.currentIndex()
+ if 0 <= index < len(self.__driver_infos):
+ driver_info = self.__driver_infos[index]
+ self.__driver_manager.cancelDriverDownload(driver_info)
+ driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
+ self.updateDriverInfoDisplay(driver_info)
+ self.updateProgressBarStates(driver_info)
+ self.updateButtonStates(driver_info)
+ self.ProgressText.setText("下载已取消")
+
+
+ @Slot()
+ def onConfirmButtonClicked(
+ self
+ ):
+
+ index = self.DriverComboBox.currentIndex()
+ if index < 0 or index >= len(self.__driver_infos):
+ return
+ driver_info = self.__driver_infos[index]
+ if driver_info.driver_status != WebDriverStatus.INSTALLED:
+ return
+ self.__selected_driver_info = driver_info
+ self.__confirmed = True
+ self.accept()
+
+
+ @Slot()
+ def onCancelButtonClicked(
+ self
+ ):
+
+ if self.__download_thread:
+ reply = QMessageBox.question(
+ self,
+ "确认取消 - AutoLibrary",
+ "正在下载中, 确定要取消下载吗 ?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
+ )
+ if reply == QMessageBox.StandardButton.Yes:
+ self.__download_thread.cancel()
+ else:
+ self.__confirmed = False
+ self.__selected_driver_info = None
+ self.reject()
+
+
+ def closeEvent(
+ self,
+ event: QCloseEvent
+ ):
+
+ if self.__download_thread and self.__download_thread.isRunning():
+ reply = QMessageBox.question(
+ self,
+ "确认关闭 - AutoLibrary",
+ "驱动正在下载中, 确定要取消并关闭对话框吗 ?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
+ )
+ if reply == QMessageBox.StandardButton.No:
+ event.ignore()
+ return
+ self.__download_thread.stop()
+ if not self.__confirmed:
+ self.__selected_driver_info = None
+ event.accept()
+ super().closeEvent(event)
+
+ def __onThreadFinished(
+ self
+ ):
+
+ if self.__download_thread:
+ self.__download_thread.deleteLater()
+ self.__download_thread = None
+
+
+ def getSelectedDriverInfo(
+ self
+ ) -> Optional[WebDriverInfo]:
+
+ return self.__selected_driver_info
+
+
+ def updateDriverInfoDisplay(
+ self,
+ driver_info: WebDriverInfo
+ ):
+
+ if driver_info.driver_type == WebDriverType.CHROME:
+ driver_type = "Google Chrome"
+ elif driver_info.driver_type == WebDriverType.FIREFOX:
+ driver_type = "Mozilla Firefox"
+ elif driver_info.driver_type == WebDriverType.EDGE:
+ driver_type = "Microsoft Edge"
+ else:
+ driver_type = "未知"
+ self.BrowserTypeLabel.setText(f"类型:{driver_type}")
+ self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
+ if driver_info.driver_path:
+ self.PathLabel.setText(str(driver_info.driver_path))
+ else:
+ self.PathLabel.setText("未安装")
+ match driver_info.driver_status:
+ case WebDriverStatus.NOT_INSTALLED:
+ self.StatusLabel.status = ALStatusLabel.Status.WAITING
+ case WebDriverStatus.INSTALLED:
+ self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
+ case WebDriverStatus.DOWNLOADING:
+ self.StatusLabel.status = ALStatusLabel.Status.RUNNING
+ case WebDriverStatus.ERROR:
+ self.StatusLabel.status = ALStatusLabel.Status.FAILURE
+
+
+ def updateProgressBarStates(
+ self,
+ driver_info: WebDriverInfo
+ ):
+
+ if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
+ self.ProgressBar.setValue(0)
+ self.ProgressText.setText("未安装")
+ elif driver_info.driver_status == WebDriverStatus.INSTALLED:
+ self.ProgressBar.setValue(100)
+ self.ProgressText.setText("已安装")
+ elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
+ pass # update by worker thread
+ elif driver_info.driver_status == WebDriverStatus.ERROR:
+ self.ProgressBar.setValue(0)
+ self.ProgressText.setText("下载失败")
+
+
+ def updateButtonStates(
+ self,
+ driver_info: WebDriverInfo
+ ):
+
+ if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
+ self.RefreshButton.setEnabled(True)
+ self.DeleteButton.setEnabled(False)
+ self.DownloadButton.setEnabled(True)
+ self.CancelButton.setEnabled(True)
+ self.ConfirmButton.setEnabled(False)
+ elif driver_info.driver_status == WebDriverStatus.INSTALLED:
+ self.RefreshButton.setEnabled(True)
+ self.DownloadButton.setEnabled(False)
+ self.DeleteButton.setEnabled(True)
+ self.CancelButton.setEnabled(True)
+ self.ConfirmButton.setEnabled(True)
+ elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
+ self.RefreshButton.setEnabled(False)
+ self.DownloadButton.setEnabled(False)
+ self.DeleteButton.setEnabled(False)
+ self.CancelButton.setEnabled(True)
+ self.ConfirmButton.setEnabled(False)
+ elif driver_info.driver_status == WebDriverStatus.ERROR:
+ self.RefreshButton.setEnabled(True)
+ self.DownloadButton.setEnabled(True)
+ self.DeleteButton.setEnabled(False)
+ self.CancelButton.setEnabled(True)
+ self.ConfirmButton.setEnabled(False)
diff --git a/src/gui/__init__.py b/src/gui/__init__.py
new file mode 100644
index 0000000..c0cf4b0
--- /dev/null
+++ b/src/gui/__init__.py
@@ -0,0 +1,18 @@
+"""
+ GUI module for the AutoLibrary project.
+
+ Here are the classes and modules in this package:
+ - ALMainWindow: Main window class.
+ - ALAboutDialog: About dialog class.
+ - ALConfigWidget: Configuration widget class.
+ - ALSeatFrame: Seat frame class.
+ - ALSeatMapView: Seat map view class.
+ - ALSeatMapTable: Seat map table class.
+ - ALSeatMapSelectDialog: Seat map select dialog class.
+ - ALTimerTaskAddDialog: Timer task add dialog class.
+ - ALTimerTaskHistoryDialog: Timer task history dialog class.
+ - ALTimerTaskManageWidget: Timer task manage widget class.
+ - ALUserTreeWidget: User tree widget class.
+ - ALMainWorkers: Main workers class.
+ - ALVersionInfo: Version info class.
+"""
\ No newline at end of file
diff --git a/src/gui/resources/__init__.py b/src/gui/resources/__init__.py
new file mode 100644
index 0000000..482953b
--- /dev/null
+++ b/src/gui/resources/__init__.py
@@ -0,0 +1,3 @@
+"""
+ GUI resources module for the AutoLibrary project.
+"""
\ No newline at end of file
diff --git a/src/gui/resources/ui/ALConfigWidget.ui b/src/gui/resources/ui/ALConfigWidget.ui
index ea41727..a272576 100644
--- a/src/gui/resources/ui/ALConfigWidget.ui
+++ b/src/gui/resources/ui/ALConfigWidget.ui
@@ -1233,12 +1233,31 @@
- -
-
-
- 浏览器设置
+
-
+
+
+
+ 0
+ 270
+
-
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+
+ -
+
+
+ 运行模式
+
+
5
@@ -1255,162 +1274,59 @@
3
-
-
+
- 80
+ 100
25
- 80
+ 100
25
- 浏览器类型:
+ 自动预约
-
-
+
- 80
+ 100
25
- 80
- 25
-
-
-
- <html><head/><body><p>脚本运行使用的浏览器类型</p></body></html>
-
-
- <html><head/><body><p><br/></p></body></html>
-
-
- edge
-
-
- 0
-
-
- 3
-
-
- 3
-
-
-
-
- edge
-
-
- -
-
- chrome
-
-
- -
-
- firefox
-
-
-
-
- -
-
-
-
- 80
- 25
-
-
-
-
- 80
+ 100
25
- 驱动路径:
+ 自动签到
-
-
-
- 5
-
-
-
-
-
-
- 250
- 25
-
-
-
-
- 300
- 25
-
-
-
- <html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html>
-
-
- <html><head/><body><p><br/></p></body></html>
-
-
-
- -
-
-
-
- 35
- 25
-
-
-
-
- 35
- 25
-
-
-
- ...
-
-
-
-
-
- -
-
+
- 0
+ 100
25
- 16777215
+ 100
25
-
- <html><head/><body><p>运行时不显示浏览器</p></body></html>
-
-
- <html><head/><body><p><br/></p></body></html>
-
- 无头模式
+ 自动续约
@@ -1529,15 +1445,12 @@
- -
-
+
-
+
- 运行模式
+ 浏览器设置
-
-
- 5
-
+
3
@@ -1550,85 +1463,203 @@
3
-
-
-
+
+ 5
+
+
-
+
- 100
+ 80
25
- 100
+ 80
25
- 自动预约
+ 浏览器类型:
- -
-
+
-
+
- 100
+ 80
25
- 100
+ 80
+ 25
+
+
+
+ <html><head/><body><p>脚本运行使用的浏览器类型</p></body></html>
+
+
+ <html><head/><body><p><br/></p></body></html>
+
+
+ edge
+
+
+ 0
+
+
+ 3
+
+
+ 3
+
+
-
+
+ edge
+
+
+ -
+
+ chrome
+
+
+ -
+
+ firefox
+
+
+
+
+ -
+
+
+
+ 175
+ 25
+
+
+
+
+ 175
25
- 自动签到
+ 驱动路径:
- -
-
+
-
+
+
+ 5
+
+
-
+
+
+
+ 250
+ 25
+
+
+
+
+ 300
+ 25
+
+
+
+ <html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html>
+
+
+ <html><head/><body><p><br/></p></body></html>
+
+
+
+ -
+
+
+
+ 35
+ 25
+
+
+
+
+ 35
+ 25
+
+
+
+ ...
+
+
+
+
+
+ -
+
- 100
+ 0
25
- 100
+ 16777215
25
+
+ <html><head/><body><p>运行时不显示浏览器</p></body></html>
+
+
+ <html><head/><body><p><br/></p></body></html>
+
- 自动续约
+ 无头模式
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 120
+ 25
+
+
+
+
+ 120
+ 25
+
+
+
+ Qt::LayoutDirection::LeftToRight
+
+
+ 自动下载驱动
+
+
+
- -
-
-
-
- 0
- 270
-
-
-
- QFrame::Shape::NoFrame
-
-
- QFrame::Shadow::Plain
-
-
- 0
-
-
-
diff --git a/src/managers/__init__.py b/src/managers/__init__.py
new file mode 100644
index 0000000..6665757
--- /dev/null
+++ b/src/managers/__init__.py
@@ -0,0 +1,8 @@
+"""
+ Managers module for the AutoLibrary project.
+
+ Here are the classes and modules in this package:
+ - ConfigManager: Config manager for managing configuration files.
+ - LogManager: Log manager for logging.
+ - WebDriverManager: Web driver manager for managing web drivers.
+"""
\ No newline at end of file
diff --git a/src/utils/ConfigManager.py b/src/managers/config/ConfigManager.py
similarity index 96%
rename from src/utils/ConfigManager.py
rename to src/managers/config/ConfigManager.py
index 59fb1a3..247ec42 100644
--- a/src/utils/ConfigManager.py
+++ b/src/managers/config/ConfigManager.py
@@ -234,11 +234,12 @@ def instance(
global _config_manager_instance
with _instance_lock:
if _config_manager_instance is None:
+ if not config_dir:
+ raise ValueError("ConfigManager 需要配置目录参数")
_config_manager_instance = ConfigManager(config_dir)
else:
if config_dir == "":
return _config_manager_instance
if getBaseConfigDir() != config_dir:
- raise ValueError(
- "ConfigManager 的实例已初始化,不能使用不同的配置目录。")
+ raise ValueError("ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance
diff --git a/src/managers/config/__init__.py b/src/managers/config/__init__.py
new file mode 100644
index 0000000..1c1c60e
--- /dev/null
+++ b/src/managers/config/__init__.py
@@ -0,0 +1,6 @@
+"""
+ Config managers module for the AutoLibrary project.
+
+ Here are the classes and modules in this package:
+ - ConfigManager: Config manager for managing configuration files.
+"""
\ No newline at end of file
diff --git a/src/managers/driver/WebBrowserDetector.py b/src/managers/driver/WebBrowserDetector.py
new file mode 100644
index 0000000..1da85d8
--- /dev/null
+++ b/src/managers/driver/WebBrowserDetector.py
@@ -0,0 +1,166 @@
+import platform
+import installed_browsers
+
+from pathlib import Path
+from enum import Enum
+from dataclasses import dataclass
+
+
+class WebBrowserType(Enum):
+ """
+ Web browser type
+ """
+
+ CHROME = "chrome"
+ FIREFOX = "firefox"
+ EDGE = "edge"
+
+
+class WebBrowserArch(Enum):
+ """
+ Web browser architecture
+ """
+
+ WINX86_32 = 0
+ WINX86_64 = 1
+ WINARM = 2
+
+ LINUXX86_32 = 3
+ LINUXX86_64 = 4
+ LINUXARM = 5
+
+ MACX86_64 = 6
+ MACARM = 7
+
+@dataclass
+class WebBrowserInfo:
+ """
+ Web browser information
+
+ Attributes:
+ browser_arch (WebBrowserArch): Web browser architecture
+ browser_type (WebBrowserType): Web browser type
+ browser_version (str): Web browser version
+ browser_path (Path): Web browser executable file path
+ """
+
+ browser_arch: WebBrowserArch
+ browser_type: WebBrowserType
+ browser_version: str
+ browser_path: Path
+
+
+class WebBrowserArchDetector:
+ """
+ Web browser architecture detector
+ """
+
+ def __init__(
+ self
+ ):
+
+ pass
+
+
+ def detect(
+ self
+ ) -> WebBrowserArch:
+ """
+ Detect system architecture
+
+ Returns:
+ WebBrowserArch: System architecture
+ """
+
+ system = platform.system()
+ machine = platform.machine().lower()
+ if system == "Windows":
+ if machine in ["amd64", "x86_64"]:
+ return WebBrowserArch.WINX86_64
+ elif machine in ["i386", "i686", "x86"]:
+ return WebBrowserArch.WINX86_32
+ elif machine in ["arm64", "aarch64"]:
+ return WebBrowserArch.WINARM
+ else:
+ return WebBrowserArch.WINX86_64
+ elif system == "Darwin":
+ if machine in ["arm64", "aarch64"]:
+ return WebBrowserArch.MACARM
+ else:
+ return WebBrowserArch.MACX86_64
+ elif system == "Linux":
+ if machine in ["amd64", "x86_64"]:
+ return WebBrowserArch.LINUXX86_64
+ elif machine in ["i386", "i686", "x86"]:
+ return WebBrowserArch.LINUXX86_32
+ elif machine in ["arm64", "aarch64"]:
+ return WebBrowserArch.LINUXARM
+ elif machine.startswith("arm"):
+ return WebBrowserArch.LINUXARM
+ else:
+ return WebBrowserArch.LINUXX86_64
+ raise ValueError(f"不支持的系统架构 : {system} {machine}")
+
+
+class WebBrowserDetector:
+ """
+ Web browser detector
+ """
+
+ def __init__(
+ self
+ ):
+
+ self.browser_arch = WebBrowserArchDetector().detect()
+ self.browser_infos : list[WebBrowserInfo] = []
+
+
+ def detect(
+ self
+ ) -> list[WebBrowserInfo]:
+
+ """
+ Detect installed web browsers on the system.
+
+ Returns:
+ list[WebBrowserInfo]: List of detected browser information objects.
+ """
+
+ self.browser_infos = []
+ try:
+ all_browsers = installed_browsers.browsers()
+ except Exception as e:
+ self.browser_infos = []
+ return self.browser_infos
+
+ # Mapping from internal library name to our enum
+ type_map = {
+ 'chrome': WebBrowserType.CHROME,
+ 'firefox': WebBrowserType.FIREFOX,
+ 'msedge': WebBrowserType.EDGE,
+ }
+ for browser in all_browsers:
+ internal_name = browser.get('name', '').lower()
+ if internal_name not in type_map:
+ continue # Not one of the browsers we care about
+ version = browser.get('version')
+ if not version:
+ # Skip browsers with no version info (unlikely, but defensive)
+ continue
+ exe_path = browser.get('location')
+ if not exe_path:
+ continue
+ try:
+ path = Path(exe_path)
+ if not path.is_file():
+ continue
+ except Exception:
+ continue # Invalid path
+ info = WebBrowserInfo(
+ browser_arch=self.browser_arch, # Use system architecture as fallback
+ browser_type=type_map[internal_name],
+ browser_version=version,
+ browser_path=path,
+ )
+ self.browser_infos.append(info)
+ return self.browser_infos
\ No newline at end of file
diff --git a/src/managers/driver/WebDriverDownloader.py b/src/managers/driver/WebDriverDownloader.py
new file mode 100644
index 0000000..c4e3279
--- /dev/null
+++ b/src/managers/driver/WebDriverDownloader.py
@@ -0,0 +1,452 @@
+import os
+import time
+import shutil
+import threading
+import requests
+import zipfile
+import tarfile
+
+from enum import Enum
+from pathlib import Path
+from typing import Optional, Callable
+
+
+class WebDriverType(Enum):
+ """
+ Web driver type
+ """
+
+ CHROME = "chrome"
+ FIREFOX = "firefox"
+ EDGE = "edge"
+
+
+class WebDriverArch(Enum):
+ """
+ Web driver architecture
+ """
+
+ class Chrome(Enum):
+ """
+ Chrome web driver architecture
+ """
+
+ WINX86_32 = "win32"
+ WINX86_64 = "win64"
+
+ # LINUX86_32 : no support for linux 32bit
+ LINUX86_64 = "linux64"
+ # LINUXARM : no support for linux arm64
+
+ MACX86_64 = "mac-x64"
+ MACARM = "mac-arm64"
+
+ class Firefox(Enum):
+ """
+ Firefox web driver architecture
+ """
+
+ WINX86_32 = "win32"
+ WINX86_64 = "win64"
+ WINARM = "win-aarch64"
+
+ LINUXX86_32 = "linux32"
+ LINUXX86_64 = "linux64"
+ LINUXARM = "linux-aarch64"
+
+ MACX86_64 = "macos"
+ MACARM = "macos-aarch64"
+
+ class Edge(Enum):
+ """
+ Edge web driver architecture
+ """
+
+ WINX86_32 = "win32"
+ WINX86_64 = "win64"
+ WINARM = "arm64"
+
+ # LINUX86_32 : no support for linux 32bit
+ LINUXX86_64 = "linux64"
+ # LINUXARM : no support for linux arm64
+
+ MACX86_64 = "mac64"
+ MACARM = "mac64_m1"
+
+
+class WebDriverName:
+ """
+ Web driver name
+ """
+
+ def __init__(
+ self,
+ driver_type: WebDriverType
+ ):
+
+ self.driver_type = driver_type
+
+
+ def __str__(
+ self
+ ) -> str:
+
+ match self.driver_type:
+ case WebDriverType.CHROME:
+ return "chromedriver"
+ case WebDriverType.FIREFOX:
+ return "geckodriver"
+ case WebDriverType.EDGE:
+ return "msedgedriver"
+ case _:
+ raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
+
+
+class WebDriverExecName:
+ """
+ Web driver executable file name
+ """
+
+ def __init__(
+ self,
+ driver_type: WebDriverType,
+ arch: WebDriverArch
+ ):
+
+ self.driver_type = driver_type
+ self.arch = arch
+
+
+ def __str__(
+ self
+ ) -> str:
+
+ is_win = True if self.arch is WebDriverArch.Chrome.WINX86_32 or\
+ self.arch is WebDriverArch.Chrome.WINX86_64 or\
+ self.arch is WebDriverArch.Firefox.WINX86_32 or\
+ self.arch is WebDriverArch.Firefox.WINX86_64 or\
+ self.arch is WebDriverArch.Edge.WINX86_32 or\
+ self.arch is WebDriverArch.Edge.WINX86_64 else False
+ match self.driver_type:
+ case WebDriverType.CHROME:
+ return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
+ case WebDriverType.FIREFOX:
+ return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
+ case WebDriverType.EDGE:
+ return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
+ case _:
+ raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
+
+
+class WebDriverFileName:
+ """\
+ Web driver compressed file name
+ """
+
+ def __init__(
+ self,
+ version: str,
+ driver_type: WebDriverType,
+ arch: WebDriverArch
+ ):
+
+ self.version = version
+ self.driver_type = driver_type
+ self.arch = arch
+
+ def __str__(
+ self
+ ) -> str:
+
+ match self.driver_type:
+ case WebDriverType.CHROME:
+ return f"{WebDriverName(self.driver_type)}-{self.arch.value}.zip"
+ case WebDriverType.FIREFOX:
+ if self.arch is WebDriverArch.Firefox.WINX86_32 or\
+ self.arch is WebDriverArch.Firefox.WINX86_64:
+ suffix = "zip"
+ else:
+ suffix = "tar.gz"
+ return f"{WebDriverName(self.driver_type)}-v{self.version}-{self.arch.value}.{suffix}"
+ case WebDriverType.EDGE:
+ return f"edgedriver_{self.arch.value}.zip" # Edge web driver file name is different
+ case _:
+ raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
+
+
+class WebDriverURL:
+ """
+ Web driver download URL
+ """
+
+ def __init__(
+ self,
+ version: str,
+ driver_type: WebDriverType,
+ arch: WebDriverArch
+ ):
+
+ self.version = version
+ self.driver_type = driver_type
+ self.arch = arch
+ self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
+
+
+ def __str__(
+ self
+ ) -> str:
+
+ match self.driver_type:
+ case WebDriverType.CHROME:
+ return f"https://storage.googleapis.com/chrome-for-testing-public/"\
+ f"{self.version}/"\
+ f"{self.arch.value}/"\
+ f"{self.file_name}"
+ case WebDriverType.FIREFOX:
+ return f"https://github.com/mozilla/geckodriver/releases/download/"\
+ f"v{self.version}/"\
+ f"{self.file_name}"
+ case WebDriverType.EDGE:
+ return f"https://msedgedriver.microsoft.com/"\
+ f"{self.version}/"\
+ f"{self.file_name}"
+ case _:
+ raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
+
+
+class WebDriverDownloader:
+ """
+ Base class for WebDriver downloaders
+
+ Args:
+ driver_type (WebDriverType): Web driver type
+ version (str): WebDriver version
+ arch (WebDriverArch): WebDriver architecture
+ download_dir (str): Download directory
+ """
+
+ def __init__(
+ self,
+ driver_type: WebDriverType,
+ driver_version: str,
+ driver_arch: WebDriverArch,
+ download_dir: str
+ ):
+
+ self.driver_type = driver_type
+ self.arch = driver_arch
+ self.version = driver_version
+ self.download_url = str(WebDriverURL(self.version, self.driver_type, self.arch))
+ self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value
+ self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
+ self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
+
+
+ def download(
+ self,
+ progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
+ cancel_event: Optional[threading.Event] = None
+ ) -> Optional[Path]:
+
+ try:
+ # downlaod file : 0% - 98%
+ if not self._download(progress_callback, cancel_event=cancel_event):
+ return None
+ # verify file : 98% - 99%
+ if not self._verify(progress_callback):
+ progress_callback(0, 100, 0.0, "验证失败")
+ return None
+ # extract file : 99% - 100%
+ driver_path = self._extract(progress_callback)
+ if not driver_path:
+ progress_callback(0, 100, 0.0, "解压失败")
+ return None
+ return driver_path
+ except Exception as e:
+ raise e
+
+
+ def _download(
+ self,
+ progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
+ max_retries: int = 3,
+ cancel_event: Optional[threading.Event] = None
+ ) -> bool:
+
+ CHUNK_SIZE = 8192*8 # 64KB chunk
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ 'Accept-Encoding': 'gzip, deflate'
+ }
+
+ for attempt in range(max_retries):
+ try:
+ if cancel_event and cancel_event.is_set():
+ return False
+ # resume download if file exists
+ if self.download_path.exists():
+ downloaded_size = self.download_path.stat().st_size
+ headers_ = headers.copy()
+ headers_['Range'] = f"bytes={downloaded_size}-"
+ mode = 'ab'
+ else:
+ downloaded_size = 0
+ headers_ = headers
+ mode = 'wb'
+ # get response
+ response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=10)
+ if response.status_code not in [200, 206]:
+ if self.download_path.exists():
+ self.download_path.unlink()
+ downloaded_size = 0
+ mode = 'wb'
+ response = requests.get(str(self.download_url), headers=headers, stream=True)
+ response.raise_for_status()
+ # get total size
+ total_size = int(response.headers.get('Content-Length', 0))
+ if response.status_code == 206: # Partial Content - server supports Range
+ total_size += downloaded_size
+ last_callback_time = time.time()
+ last_callback_size = downloaded_size
+ callback_interval = 0.1
+ with open(self.download_path, mode) as f:
+ for chunk in response.iter_content(CHUNK_SIZE):
+ current_time = time.time()
+ if cancel_event and cancel_event.is_set():
+ response.close()
+ return False
+ if not chunk:
+ continue
+ f.write(chunk)
+ downloaded_size += len(chunk)
+ if not progress_callback or total_size <= 0:
+ continue
+ current_progress = (downloaded_size/total_size)*98.0
+ if current_time - last_callback_time >= callback_interval or current_progress >= 98.0:
+ elapsed = current_time - last_callback_time
+ if elapsed > 0:
+ speed = (downloaded_size - last_callback_size)/(elapsed*1024.0)
+ else:
+ speed = 0.0
+ progress_callback(current_progress, 100, speed, "下载中...")
+ last_callback_time = current_time
+ last_callback_size = downloaded_size
+ if total_size > 0 and self.download_path.stat().st_size < total_size:
+ raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节")
+ return True
+ except Exception as e:
+ if cancel_event and cancel_event.is_set():
+ return False
+ if attempt < max_retries - 1:
+ progress_callback(0, 100, 0.0, f"第 {attempt+1} 次重试...")
+ time.sleep(1)
+ continue
+ raise e
+
+
+ def _verify(
+ self,
+ progress_callback: Optional[Callable[[float, int, float, str], None]] = None
+ ) -> bool:
+
+ progress_callback(98, 100, 0.0, "验证完成")
+ return True
+
+
+ def _extract(
+ self,
+ progress_callback: Optional[Callable[[float, int, float, str], None]] = None
+ ) -> Optional[Path]:
+
+ try:
+ progress_callback(98, 100, 0.0, "解压中...")
+ file_path_str = str(self.download_path)
+ if file_path_str.endswith('.tar.gz'):
+ with tarfile.open(self.download_path, 'r:gz') as tar_ref:
+ tar_ref.extractall(self.download_dir)
+ else:
+ with zipfile.ZipFile(self.download_path, 'r') as zip_ref:
+ zip_ref.extractall(self.download_dir)
+ driver_file = None
+ for root, _, files in os.walk(self.download_dir):
+ for file in files:
+ expected_name = str(WebDriverExecName(self.driver_type, self.arch))
+ if file == str(expected_name):
+ src_path = Path(root, file)
+ dst_path = self.download_dir/file
+ src_path.rename(dst_path)
+ driver_file = dst_path
+ break
+ if driver_file:
+ break
+ if not driver_file:
+ raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
+ progress_callback(100, 100, 0.0, "解压完成")
+ self.download_path.unlink()
+ self._cleanup(driver_file)
+ return driver_file
+ except Exception:
+ return None
+
+
+ def _cleanup(
+ self,
+ driver_file: Path
+ ) -> None:
+
+ for item in self.download_dir.iterdir():
+ if item != driver_file:
+ if item.is_dir():
+ shutil.rmtree(item)
+ else:
+ item.unlink()
+
+
+class ChromeDriverDownloader(WebDriverDownloader):
+ """
+ Chrome web driver downloader
+
+ Only support version higher than 114
+ """
+
+ def __init__(
+ self,
+ version: str,
+ arch: WebDriverArch,
+ download_dir: str
+ ):
+
+ super().__init__(WebDriverType.CHROME, version, arch, download_dir)
+
+
+class FirefoxDriverDownloader(WebDriverDownloader):
+ """
+ Firefox web driver downloader
+
+ This class do not resolve version mapping,
+ only support driver version higher than 0.17.0
+ """
+
+ def __init__(
+ self,
+ version: str,
+ arch: WebDriverArch,
+ download_dir: str
+ ):
+
+ super().__init__(WebDriverType.FIREFOX, version, arch, download_dir)
+
+
+class EdgeDriverDownloader(WebDriverDownloader):
+ """
+ Edge web driver downloader
+ """
+
+ def __init__(
+ self,
+ version: str,
+ arch: WebDriverArch,
+ download_dir: str
+ ):
+
+ super().__init__(WebDriverType.EDGE, version, arch, download_dir)
diff --git a/src/managers/driver/WebDriverManager.py b/src/managers/driver/WebDriverManager.py
new file mode 100644
index 0000000..7846560
--- /dev/null
+++ b/src/managers/driver/WebDriverManager.py
@@ -0,0 +1,471 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 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 threading
+import packaging.version as ver
+
+from enum import Enum
+from pathlib import Path
+from typing import Optional, Callable
+
+from managers.driver.WebBrowserDetector import (
+ WebBrowserType, WebBrowserArch, WebBrowserInfo, WebBrowserDetector
+)
+from managers.driver.WebDriverDownloader import (
+ WebDriverArch, WebDriverType,
+ ChromeDriverDownloader, FirefoxDriverDownloader, EdgeDriverDownloader
+)
+
+
+class WebDriverStatus(Enum):
+ """
+ Web driver status.
+ """
+
+ NOT_INSTALLED = 0
+ INSTALLED = 1
+ DOWNLOADING = 2
+ ERROR = 3
+
+
+class WebDriverInfo:
+ """
+ Web driver information.
+
+ Attributes:
+ driver_type (WebDriverType): Web driver type
+ driver_arch (WebDriverArch): Web driver architecture
+ driver_version (str): Web driver version
+ browser_version (str): Web browser version
+ driver_path (Optional[Path]): Web driver executable file path
+ driver_status (DriverStatus): Web driver status
+ """
+
+ def __init__(
+ self
+ ):
+
+ self.driver_type = None
+ self.driver_arch = None
+ self.driver_version = ""
+ self.browser_version = ""
+ self.driver_path: Optional[Path] = None
+ self.driver_status = WebDriverStatus.NOT_INSTALLED
+
+
+class WebDriverManager:
+ """
+ Web Driver Manager Singleton Class
+
+ Args:
+ driver_dir (str): The directory to store web drivers.
+ """
+
+ def __init__(
+ self,
+ driver_dir: str
+ ):
+
+ self.__driver_dir = os.path.abspath(driver_dir)
+ self.__browser_detector = WebBrowserDetector()
+ self.__driver_infos: list[WebDriverInfo] = []
+ self.__initialized = False
+ self.__lock = threading.Lock()
+
+ self.initialize()
+
+
+ def initialize(
+ self
+ ):
+
+ if self.__initialized:
+ return
+ os.makedirs(self.__driver_dir, exist_ok=True)
+ self._detectBrowsers()
+ self._checkDriverStatus()
+ self.__initialized = True
+
+
+ def _detectBrowsers(
+ self
+ ):
+
+ with self.__lock:
+ browser_infos = self.__browser_detector.detect()
+ self.__driver_infos = [
+ self._getDriverInfo(info)
+ for info in browser_infos
+ ]
+
+
+ def _checkDriverStatus(
+ self
+ ):
+
+ with self.__lock:
+ for driver_info in self.__driver_infos:
+ driver_path = self._getDriverPath(driver_info)
+ if driver_path and driver_path.exists() and driver_path.is_file():
+ driver_info.driver_path = driver_path
+ driver_info.driver_status = WebDriverStatus.INSTALLED
+
+
+ def _mapWebBrowserTypeToDriver(
+ self,
+ browser_type: WebBrowserType
+ ) -> WebDriverType:
+
+ if browser_type == WebBrowserType.CHROME:
+ return WebDriverType.CHROME
+ elif browser_type == WebBrowserType.FIREFOX:
+ return WebDriverType.FIREFOX
+ elif browser_type == WebBrowserType.EDGE:
+ return WebDriverType.EDGE
+ else:
+ raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
+
+
+ def _mapWebBrowserArchToDriver(
+ self,
+ browser_type: WebBrowserType,
+ browser_arch: WebBrowserArch
+ ) -> WebDriverArch:
+
+ if browser_type == WebBrowserType.CHROME:
+ if browser_arch == WebBrowserArch.WINX86_32:
+ return WebDriverArch.Chrome.WINX86_32
+ elif browser_arch == WebBrowserArch.WINX86_64:
+ return WebDriverArch.Chrome.WINX86_64
+ elif browser_arch == WebBrowserArch.WINARM:
+ raise ValueError("Chrome 不支持 Windows ARM 架构")
+ elif browser_arch == WebBrowserArch.LINUXX86_32:
+ raise ValueError("Chrome 不支持 Linux x86_32 架构")
+ elif browser_arch == WebBrowserArch.LINUXX86_64:
+ return WebDriverArch.Chrome.LINUXX86_64
+ elif browser_arch == WebBrowserArch.LINUXARM:
+ raise ValueError("Chrome 不支持 Linux ARM 架构")
+ elif browser_arch == WebBrowserArch.MACX86_64:
+ return WebDriverArch.Chrome.MACX86_64
+ elif browser_arch == WebBrowserArch.MACARM:
+ return WebDriverArch.Chrome.MACARM
+ else:
+ raise ValueError(f"不支持的 Chrome 浏览器架构 : {browser_arch}")
+ elif browser_type == WebBrowserType.FIREFOX:
+ if browser_arch == WebBrowserArch.WINX86_32:
+ return WebDriverArch.Firefox.WINX86_32
+ elif browser_arch == WebBrowserArch.WINX86_64:
+ return WebDriverArch.Firefox.WINX86_64
+ elif browser_arch == WebBrowserArch.WINARM:
+ return WebDriverArch.Firefox.WINARM
+ elif browser_arch == WebBrowserArch.LINUXX86_32:
+ return WebDriverArch.Firefox.LINUXX86_32
+ elif browser_arch == WebBrowserArch.LINUXX86_64:
+ return WebDriverArch.Firefox.LINUXX86_64
+ elif browser_arch == WebBrowserArch.LINUXARM:
+ return WebDriverArch.Firefox.LINUXARM
+ elif browser_arch == WebBrowserArch.MACX86_64:
+ return WebDriverArch.Firefox.MACX86_64
+ elif browser_arch == WebBrowserArch.MACARM:
+ return WebDriverArch.Firefox.MACARM
+ else:
+ raise ValueError(f"不支持的 Firefox 浏览器架构 : {browser_arch}")
+ elif browser_type == WebBrowserType.EDGE:
+ if browser_arch == WebBrowserArch.WINX86_32:
+ return WebDriverArch.Edge.WINX86_32
+ elif browser_arch == WebBrowserArch.WINX86_64:
+ return WebDriverArch.Edge.WINX86_64
+ elif browser_arch == WebBrowserArch.WINARM:
+ return WebDriverArch.Edge.WINARM
+ elif browser_arch == WebBrowserArch.LINUXX86_32:
+ raise ValueError("Edge 不支持 Linux x86_32 架构")
+ elif browser_arch == WebBrowserArch.LINUXX86_64:
+ return WebDriverArch.Edge.LINUXX86_64
+ elif browser_arch == WebBrowserArch.LINUXARM:
+ raise ValueError("Edge 不支持 Linux ARM 架构")
+ elif browser_arch == WebBrowserArch.MACX86_64:
+ return WebDriverArch.Edge.MACX86_64
+ elif browser_arch == WebBrowserArch.MACARM:
+ return WebDriverArch.Edge.MACARM
+ else:
+ raise ValueError(f"不支持的 Edge 浏览器架构 : {browser_arch}")
+ else:
+ raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
+
+
+ def _mapFirefoxDriverVersion(
+ self,
+ version: str
+ ) -> str:
+
+ version_mapping = [
+ (ver.Version("128.0"), ver.Version("999.0"), "0.36.0"),
+ (ver.Version("115.0"), ver.Version("127.0"), "0.35.0"),
+ (ver.Version("91.0"), ver.Version("114.0"), "0.34.0"),
+ (ver.Version("91.0"), ver.Version("120.0"), "0.33.0"),
+ (ver.Version("91.0"), ver.Version("120.0"), "0.32.0"),
+ (ver.Version("91.0"), ver.Version("120.0"), "0.31.0"),
+ (ver.Version("78.0"), ver.Version("90.0"), "0.30.0"),
+ (ver.Version("60.0"), ver.Version("90.0"), "0.29.0"),
+ (ver.Version("60.0"), ver.Version("90.0"), "0.28.0"),
+ (ver.Version("60.0"), ver.Version("90.0"), "0.27.0"),
+ (ver.Version("57.0"), ver.Version("90.0"), "0.26.0"),
+ (ver.Version("55.0"), ver.Version("62.0"), "0.25.0"),
+ (ver.Version("55.0"), ver.Version("62.0"), "0.24.0"),
+ (ver.Version("57.0"), ver.Version("79.0"), "0.23.0"),
+ (ver.Version("57.0"), ver.Version("79.0"), "0.22.0"),
+ (ver.Version("57.0"), ver.Version("79.0"), "0.21.0"),
+ (ver.Version("55.0"), ver.Version("62.0"), "0.20.0"),
+ (ver.Version("55.0"), ver.Version("62.0"), "0.19.0"),
+ (ver.Version("53.0"), ver.Version("62.0"), "0.18.0"),
+ (ver.Version("52.0"), ver.Version("62.0"), "0.17.0"),
+ ]
+
+ try:
+ firefox_version = ver.Version(version)
+ for min_ver, max_ver, gecko_ver in version_mapping:
+ if min_ver <= firefox_version <= max_ver:
+ return gecko_ver
+ raise ValueError(
+ f"不支持的 Firefox 版本 : {version}"
+ f"Firefox 版本 52 及以上受支持"
+ )
+ except Exception as e:
+ raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
+
+
+ def _getDriverInfo(
+ self,
+ browser_info: WebBrowserInfo
+ ) -> WebDriverInfo:
+
+ driver_info = WebDriverInfo()
+ driver_info.driver_type = self._mapWebBrowserTypeToDriver(browser_info.browser_type)
+ driver_info.driver_arch = self._mapWebBrowserArchToDriver(browser_info.browser_type, browser_info.browser_arch)
+ if browser_info.browser_type == WebBrowserType.FIREFOX:
+ driver_info.driver_version = self._mapFirefoxDriverVersion(browser_info.browser_version)
+ else:
+ driver_info.driver_version = browser_info.browser_version
+ driver_info.browser_version = browser_info.browser_version
+ return driver_info
+
+
+ def _getDriverPath(
+ self,
+ driver_info: WebDriverInfo
+ ) -> Optional[Path]:
+
+ driver_type = driver_info.driver_type
+ driver_arch = driver_info.driver_arch
+ driver_version = driver_info.driver_version
+ if driver_type == WebDriverType.CHROME:
+ driver_name = "chromedriver"
+ elif driver_type == WebDriverType.FIREFOX:
+ driver_name = "geckodriver"
+ elif driver_type == WebDriverType.EDGE:
+ driver_name = "msedgedriver"
+ else:
+ return None
+ is_win = driver_arch in [
+ WebDriverArch.Chrome.WINX86_32,
+ WebDriverArch.Chrome.WINX86_64,
+ WebDriverArch.Firefox.WINX86_32,
+ WebDriverArch.Firefox.WINX86_64,
+ WebDriverArch.Edge.WINX86_32,
+ WebDriverArch.Edge.WINX86_64,
+ ]
+ exe_name = f"{driver_name}.exe" if is_win else driver_name
+ driver_dir = Path(self.__driver_dir)/driver_type.value/driver_version/driver_arch.value
+ driver_path = driver_dir/exe_name
+ return driver_path
+
+
+ def refresh(
+ self
+ ):
+
+ self._detectBrowsers()
+ self._checkDriverStatus()
+
+
+ def getDriverInfos(
+ self
+ ) -> list[WebDriverInfo]:
+
+ with self.__lock:
+ return self.__driver_infos.copy()
+
+
+ def getDriverInfo(
+ self,
+ driver_type: WebDriverType
+ ) -> list[WebDriverInfo]:
+
+ with self.__lock:
+ return [
+ info
+ for info in self.__driver_infos
+ if info.driver_type == driver_type
+ ]
+
+
+ def getDriverPath(
+ self,
+ driver_info: WebDriverInfo
+ ) -> Optional[Path]:
+
+ if driver_info and driver_info.driver_status == WebDriverStatus.INSTALLED:
+ return driver_info.driver_path
+ return None
+
+
+ def installDriver(
+ self,
+ driver_info: WebDriverInfo,
+ progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
+ cancel_event: Optional[threading.Event] = None
+ ) -> Optional[Path]:
+
+ with self.__lock:
+ if not driver_info:
+ if progress_callback:
+ progress_callback(0, 0, 0, "未找到浏览器信息")
+ else:
+ raise ValueError("未找到浏览器信息")
+ if driver_info and driver_info.driver_status == WebDriverStatus.DOWNLOADING:
+ if progress_callback:
+ progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动正在下载中")
+ else:
+ raise ValueError(f"{driver_info.driver_type} 驱动正在下载中")
+ try:
+ if not driver_info:
+ raise ValueError("未找到浏览器信息")
+ driver_arch = driver_info.driver_arch
+ driver_type = driver_info.driver_type
+ driver_version = driver_info.driver_version
+ downloader = None
+ if driver_type == WebDriverType.CHROME:
+ downloader = ChromeDriverDownloader(
+ version=driver_version,
+ arch=driver_arch,
+ download_dir=self.__driver_dir
+ )
+ elif driver_type == WebDriverType.FIREFOX:
+ downloader = FirefoxDriverDownloader(
+ version=driver_version,
+ arch=driver_arch,
+ download_dir=self.__driver_dir
+ )
+ elif driver_type == WebDriverType.EDGE:
+ downloader = EdgeDriverDownloader(
+ version=driver_version,
+ arch=driver_arch,
+ download_dir=self.__driver_dir
+ )
+ if downloader is None:
+ if progress_callback:
+ progress_callback(0, 0, 0, f"不支持的 Web Driver 类型")
+ else:
+ raise ValueError(f"不支持的 Web Driver 类型")
+ with self.__lock:
+ driver_info.driver_status = WebDriverStatus.DOWNLOADING
+ driver_path = downloader.download(progress_callback=progress_callback, cancel_event=cancel_event)
+ with self.__lock:
+ if driver_path:
+ driver_info.driver_path = driver_path
+ driver_info.driver_version = driver_version
+ driver_info.driver_status = WebDriverStatus.INSTALLED
+ else:
+ driver_info.driver_status = WebDriverStatus.ERROR
+ return driver_path
+ except Exception as e:
+ with self.__lock:
+ driver_info.driver_status = WebDriverStatus.ERROR
+ raise e
+
+
+ def cancelDriverDownload(
+ self,
+ driver_info: WebDriverInfo
+ ) -> bool:
+
+ import shutil
+
+ try:
+ driver_path = self._getDriverPath(driver_info)
+ if driver_path:
+ download_dir = driver_path.parent
+ if download_dir.exists():
+ shutil.rmtree(download_dir, ignore_errors=True)
+ with self.__lock:
+ driver_info.driver_path = None
+ driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
+ return True
+ except Exception:
+ return False
+
+
+ def uninstallDriver(
+ self,
+ driver_info: WebDriverInfo,
+ progress_callback: Optional[Callable[[int, int, float, str], None]] = None
+ ) -> bool:
+
+ with self.__lock:
+ if not driver_info:
+ if progress_callback:
+ progress_callback(0, 0, 0, "未找到浏览器信息")
+ else:
+ raise ValueError("未找到浏览器信息")
+ if driver_info.driver_status != WebDriverStatus.INSTALLED:
+ if progress_callback:
+ progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动未安装")
+ else:
+ raise ValueError(f"{driver_info.driver_type} 驱动未安装")
+ try:
+ driver_path = driver_info.driver_path
+ driver_path.unlink()
+ with self.__lock:
+ driver_info.driver_path = None
+ driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
+ return True
+ except Exception:
+ with self.__lock:
+ driver_info.driver_status = WebDriverStatus.ERROR
+ raise
+
+
+ def driverDir(
+ self
+ ) -> str:
+
+ return self.__driver_dir
+
+
+# WebDriverManager singleton instance.
+_webdriver_manager_instance = None
+
+# Singleton instance lock.
+_instance_lock = threading.Lock()
+
+def instance(
+ driver_dir: str = ""
+) -> WebDriverManager:
+
+ global _webdriver_manager_instance
+ with _instance_lock:
+ if _webdriver_manager_instance is None:
+ if not driver_dir:
+ raise ValueError("WebDriverManager 需要驱动目录参数")
+ _webdriver_manager_instance = WebDriverManager(driver_dir)
+ else:
+ if driver_dir and _webdriver_manager_instance.driverDir() != os.path.abspath(driver_dir):
+ raise ValueError("WebDriverManager 的实例已初始化, 不能使用不同的驱动目录")
+ return _webdriver_manager_instance
diff --git a/src/managers/driver/__init__.py b/src/managers/driver/__init__.py
new file mode 100644
index 0000000..7e0b908
--- /dev/null
+++ b/src/managers/driver/__init__.py
@@ -0,0 +1,8 @@
+"""
+ Driver managers module for the AutoLibrary project.
+
+ Here are the classes and modules in this package:
+ - WebBrowserDetector: Web browser detector class.
+ - WebDriverDownloader: Web driver downloader class.
+ - WebDriverManager: Web driver manager class.
+"""
\ No newline at end of file
diff --git a/src/managers/log/LogManager.py b/src/managers/log/LogManager.py
new file mode 100644
index 0000000..28a860c
--- /dev/null
+++ b/src/managers/log/LogManager.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 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 logging
+import os
+import threading
+
+from logging.handlers import TimedRotatingFileHandler
+from typing import Optional
+
+
+class CallerInfoFormatter(logging.Formatter):
+ """
+ Custom formatter to extract real caller information.
+ Skips MsgBase._showTrace to show the actual calling location.
+
+ Format:
+ - Logger name: left-aligned, max 15 chars
+ - Level name: left-aligned, max 8 chars
+ - Filename: left-aligned, max 20 chars
+ - Line number: left-aligned, max 4 digits
+ """
+
+ def __init__(
+ self,
+ fmt=None,
+ datefmt=None,
+ style='%'
+ ):
+
+ super().__init__(fmt, datefmt, style)
+ self.basefmt = fmt
+
+ def format(
+ self,
+ record
+ ):
+
+ depth = 0
+ while depth < 10:
+ record.filename = os.path.basename(record.pathname)
+ if 'MsgBase.py' not in record.filename and record.funcName != '_showTrace':
+ break
+ if not hasattr(record, 'stack'):
+ record.stack = True
+ import traceback
+ record.stack_list = traceback.extract_stack()
+ depth += 1
+ if depth < len(record.stack_list):
+ frame = record.stack_list[-depth-1]
+ record.filename = os.path.basename(frame.filename)
+ record.lineno = int(frame.lineno)
+ record.funcName = frame.name
+ record.name = record.name[-15:].ljust(15)
+ record.levelname = record.levelname.ljust(8)
+ record.filename = record.filename[-20:].ljust(20)
+ # Ensure lineno is always integer before formatting
+ try:
+ lineno_int = int(record.lineno)
+ except (ValueError, TypeError):
+ lineno_int = 0
+ record.lineno = f"{lineno_int:04d}"
+
+ return super().format(record)
+
+
+class LogManager:
+ """
+ Log Manager Singleton Class
+
+ Args:
+ log_dir (str): The directory to store log files.
+ """
+
+ def __init__(
+ self,
+ log_dir: str
+ ):
+
+ self.__log_dir = os.path.abspath(log_dir)
+ self.__logger = None
+ self.__initialized = False
+
+ self.initialize()
+
+
+ def initialize(
+ self
+ ):
+
+ if self.__initialized:
+ return
+ os.makedirs(self.__log_dir, exist_ok=True)
+ self.__logger = logging.getLogger("AutoLibrary")
+ self.__logger.setLevel(logging.DEBUG)
+ self.__logger.handlers.clear()
+
+ formatter = CallerInfoFormatter(
+ '[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+ )
+
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.INFO)
+ console_handler.setFormatter(formatter)
+ self.__logger.addHandler(console_handler)
+
+ all_log_file = os.path.join(self.__log_dir, "all.log")
+ file_handler_all = TimedRotatingFileHandler(
+ all_log_file,
+ when='midnight',
+ interval=1,
+ backupCount=7,
+ encoding='utf-8'
+ )
+ file_handler_all.suffix = "%Y-%m-%d.log"
+ file_handler_all.setLevel(logging.DEBUG)
+ file_handler_all.setFormatter(formatter)
+ self.__logger.addHandler(file_handler_all)
+
+ error_log_file = os.path.join(self.__log_dir, "error.log")
+ file_handler_error = TimedRotatingFileHandler(
+ error_log_file,
+ when='midnight',
+ interval=1,
+ backupCount=14,
+ encoding='utf-8'
+ )
+ file_handler_error.suffix = "%Y-%m-%d.log"
+ file_handler_error.setLevel(logging.ERROR)
+ file_handler_error.setFormatter(formatter)
+ self.__logger.addHandler(file_handler_error)
+
+ self.__initialized = True
+
+
+ def getLogger(
+ self,
+ name: Optional[str] = None
+ ) -> logging.Logger:
+
+ if name:
+ return self.__logger.getChild(name)
+ return self.__logger
+
+
+ def setLevel(
+ self,
+ level: int
+ ):
+
+ if self.__logger:
+ self.__logger.setLevel(level)
+
+
+ def logDir(
+ self
+ ) -> str:
+
+ return self.__log_dir
+
+
+# LogManager singleton instance.
+_log_manager_instance = None
+
+# Singleton instance lock.
+_instance_lock = threading.Lock()
+def instance(
+ log_dir: str = ""
+) -> LogManager:
+
+ global _log_manager_instance
+ with _instance_lock:
+ if _log_manager_instance is None:
+ if not log_dir:
+ raise ValueError("LogManager 需要日志目录参数")
+ _log_manager_instance = LogManager(log_dir)
+ else:
+ if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir):
+ raise ValueError("LogManager 的实例已初始化, 不能使用不同的日志目录")
+ return _log_manager_instance
+
+
+def getLogger(
+ name: Optional[str] = None
+) -> logging.Logger:
+
+ if _log_manager_instance is None:
+ raise RuntimeError("LogManager 未初始化, 请先调用 LogManager.instance(log_dir) 初始化")
+ return _log_manager_instance.getLogger(name)
diff --git a/src/managers/log/__init__.py b/src/managers/log/__init__.py
new file mode 100644
index 0000000..bdf450a
--- /dev/null
+++ b/src/managers/log/__init__.py
@@ -0,0 +1,6 @@
+"""
+ Log managers module for the AutoLibrary project.
+
+ Here are the classes and modules in this package:
+ - LogManager: Log manager for logging.
+"""
\ No newline at end of file
diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py
index 651aa09..8613d2a 100644
--- a/src/operators/AutoLib.py
+++ b/src/operators/AutoLib.py
@@ -54,7 +54,7 @@ class AutoLib(MsgBase):
self
) -> bool:
- self._showTrace("正在初始化浏览器驱动......")
+ self._showTrace("正在初始化浏览器驱动......", no_log=True)
web_driver_config = self.__run_config.get("web_driver", None)
self.__driver_type = web_driver_config.get("driver_type")
@@ -66,11 +66,14 @@ class AutoLib(MsgBase):
case "firefox":
driver_options = webdriver.FirefoxOptions()
case _:
- self._showTrace(f"不支持的浏览器驱动类型: {self.__driver_type} !")
+ self._showTrace(
+ f"不支持的浏览器驱动类型: {self.__driver_type} !",
+ self.TraceLevel.WARNING
+ )
return False
if not web_driver_config:
- self._showTrace("未配置浏览器驱动参数 !")
+ self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
return False
if web_driver_config.get("headless"):
driver_options.add_argument("--headless")
@@ -110,7 +113,7 @@ class AutoLib(MsgBase):
# init browser driver
self.__driver_path = web_driver_config.get("driver_path")
if not self.__driver_path:
- self._showTrace("未配置浏览器驱动路径 !")
+ self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING)
return False
self.__driver_path = os.path.abspath(self.__driver_path)
try:
@@ -123,18 +126,18 @@ class AutoLib(MsgBase):
service = ChromeService(executable_path=self.__driver_path)
self.__driver = webdriver.Chrome(service=service, options=driver_options)
case "firefox":
- self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...")
+ self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...", no_log=True)
service = FirefoxService(executable_path=self.__driver_path)
self.__driver = webdriver.Firefox(service=service, options=driver_options)
case _: # actually will not happen, beacuse we have checked it at the initlization
# of 'driver_options'
- raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
+ raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type} !")
self.__driver.implicitly_wait(1)
self.__driver.execute_script(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
)
except Exception as e:
- self._showTrace(f"浏览器驱动初始化失败: {e}")
+ self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
return True
@@ -145,7 +148,7 @@ class AutoLib(MsgBase):
):
if not self.__driver:
- self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !")
+ self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !", self.TraceLevel.WARNING)
return
self.__lib_checker = LibChecker(self._input_queue, self._output_queue, self.__driver)
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
@@ -178,7 +181,7 @@ class AutoLib(MsgBase):
)
return True
except:
- self._showTrace(f"登录页面加载失败 !")
+ self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR)
return False
@@ -188,7 +191,7 @@ class AutoLib(MsgBase):
lib_config = self.__run_config.get("library", None)
if not lib_config:
- self._showTrace("未配置图书馆参数 !")
+ self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR)
return False
url = lib_config.get("host_url") + lib_config.get("login_url")
self.__driver.set_page_load_timeout(5)
@@ -196,7 +199,9 @@ class AutoLib(MsgBase):
self.__driver.get(url)
except TimeoutException:
self.__driver.execute_script("window.stop();")
- self._showTrace(f"图书馆登录页面加载超时 ! 请检查网络环境是否正常")
+ self._showTrace(
+ f"图书馆登录页面加载超时 ! 请检查网络环境是否正常", self.TraceLevel.ERROR
+ )
return False
if not self.__waitResponseLoad():
return False
@@ -238,32 +243,45 @@ class AutoLib(MsgBase):
else:
result = 1
else:
- self._showTrace(f"用户 {username} 无法预约,已跳过")
+ self._showTrace(f"用户 {username} 无法预约, 已跳过")
result = 2
+
# checkin
- if run_mode["auto_checkin"] and result != 1:
+ last_result = result
+ if run_mode["auto_checkin"] and last_result != 1:
if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username):
result = 0
else:
result = 1
else:
- self._showTrace(f"用户 {username} 无法签到,已跳过")
+ self._showTrace(f"用户 {username} 无法签到, 已跳过")
result = 2
+ if last_result == 0: # partly success
+ result = 0
+
# renewal
- if run_mode["auto_renewal"] and result != 1:
+ last_result = result
+ if run_mode["auto_renewal"] and last_result != 1:
can_renew, record = self.__lib_checker.canRenew()
if can_renew:
if self.__lib_renew.renew(username, record, reserve_info):
if self.__lib_checker.postRenewCheck(record):
+ self._showTrace(f"用户 {username} 续约成功 !")
result = 0
else:
- result = 1
+ if result != 1: # partly success
+ result = 0
+ else:
+ result = 1
else:
result = 1
else:
- self._showTrace(f"用户 {username} 无法续约,已跳过")
+ self._showTrace(f"用户 {username} 无法续约, 已跳过")
result = 2
+ if last_result == 0: # partly success
+ result = 0
+
# logout
if not self.__lib_logout.logout(
username
@@ -288,7 +306,8 @@ class AutoLib(MsgBase):
for user in users:
user_counter["current"] += 1
self._showTrace(
- f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......"
+ f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......",
+ no_log=True
)
if not user["enabled"]:
self._showTrace(f"用户 {user["username"]} 已跳过")
@@ -303,7 +322,8 @@ class AutoLib(MsgBase):
)
if r == -1:
self._showTrace(
- f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
+ f"用户 {user["username"]} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !",
+ self.TraceLevel.WARNING
)
break
elif r == 0:
@@ -326,11 +346,14 @@ class AutoLib(MsgBase):
if self.__driver:
if self.__driver_type.lower() == "firefox":
- self._showTrace(f"Firefox 浏览器驱动关闭略慢, 请耐心等待...")
+ self._showTrace(
+ f"Firefox 浏览器驱动关闭略慢, 请耐心等待...",
+ no_log=True
+ )
self.__driver.quit()
self.__driver = None
self._showTrace(f"浏览器驱动已关闭")
return True
else:
- self._showTrace(f"浏览器驱动未初始化, 无需关闭")
+ self._showTrace(f"浏览器驱动未初始化, 无需关闭", no_log=True)
return False
\ No newline at end of file
diff --git a/src/operators/LibChecker.py b/src/operators/LibChecker.py
index 6560efd..e1d4554 100644
--- a/src/operators/LibChecker.py
+++ b/src/operators/LibChecker.py
@@ -63,7 +63,7 @@ class LibChecker(LibOperator):
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
)
except:
- self._showTrace("加载预约记录页面失败 !")
+ self._showTrace("加载预约记录页面失败 !", self.TraceLevel.ERROR)
return False
return True
@@ -174,7 +174,7 @@ class LibChecker(LibOperator):
)
return reservations
except:
- self._showTrace("加载预约记录失败 !")
+ self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR)
return None
@@ -197,10 +197,10 @@ class LibChecker(LibOperator):
self.__driver.execute_script("arguments[0].click();", more_btn)
return True
else:
- self._showTrace("用户无法加载更多预约记录")
+ self._showTrace("用户无法加载更多预约记录", self.TraceLevel.WARNING)
return False
except:
- self._showTrace("加载更多预约记录失败 !")
+ self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR)
return False
@@ -211,9 +211,9 @@ class LibChecker(LibOperator):
) -> dict:
if wanted_date is None:
- self._showTrace("日期未指定, 无法检查当前预约状态")
+ self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING)
return None
- self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
+ self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......", no_log=True)
checked_count = 0
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
@@ -245,7 +245,8 @@ class LibChecker(LibOperator):
self._showTrace(
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
f"详细信息: {record["date"]} "
- f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
+ f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}",
+ no_log=True
)
return record
if not self.__showMoreReserveRecords():
@@ -369,7 +370,7 @@ class LibChecker(LibOperator):
else:
self._showTrace(f"\n"\
f" 续约失败 !\n"\
- f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
+ f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
)
return False
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
diff --git a/src/operators/LibCheckin.py b/src/operators/LibCheckin.py
index 37cd23c..6705ef8 100644
--- a/src/operators/LibCheckin.py
+++ b/src/operators/LibCheckin.py
@@ -51,7 +51,7 @@ class LibCheckin(LibOperator):
)
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
except:
- self._showTrace("签到时发生未知错误 !")
+ self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
return False
result_message = result_message_element.text
if "签到成功" in result_message:
@@ -107,9 +107,9 @@ class LibCheckin(LibOperator):
result = self.__driver.execute_script(script)
time.sleep(0.1)
if result:
- self._showTrace("签到按钮已启用")
+ self._showTrace("签到按钮已启用", no_log=True)
else:
- self._showTrace("签到按钮启用失败")
+ self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING)
return result
@@ -119,24 +119,24 @@ class LibCheckin(LibOperator):
) -> bool:
if self.__driver is None:
- self._showTrace("未提供有效 WebDriver 实例 !")
+ self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
return False
try:
checkin_btn = WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "btnCheckIn"))
)
except:
- self._showTrace(f"用户 {username} 签到界面加载失败 !")
+ self._showTrace(f"用户 {username} 签到界面加载失败 !", self.TraceLevel.ERROR)
return False
if "disabled" in checkin_btn.get_attribute("class"):
- self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......")
+ self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......", no_log=True)
if not self.__enableCheckinBtn():
- self._showTrace(f"签到按钮启用失败 !")
+ self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
return False
checkin_btn.click()
if self._waitResponseLoad():
- self._showTrace(f"用户 {username} 签到成功 !")
+ self._showTrace(f"用户 {username} 签到成功 !", no_log=True)
return True
else:
- self._showTrace(f"用户 {username} 签到失败 !")
+ self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR)
return False
diff --git a/src/operators/LibLogin.py b/src/operators/LibLogin.py
index 31cf2de..28555d3 100644
--- a/src/operators/LibLogin.py
+++ b/src/operators/LibLogin.py
@@ -52,7 +52,10 @@ class LibLogin(LibOperator):
)
return True
except:
- self._showTrace(f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准")
+ self._showTrace(
+ f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准",
+ self.TraceLevel.ERROR
+ )
return False
@@ -71,7 +74,7 @@ class LibLogin(LibOperator):
password_element.clear()
password_element.send_keys(password)
except Exception as e:
- self._showTrace(f"用户名或密码填写失败 ! : {e}")
+ self._showTrace(f"用户名或密码填写失败 ! : {e}", self.TraceLevel.ERROR)
return False
return True
@@ -88,12 +91,13 @@ class LibLogin(LibOperator):
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}'")
+ self._showTrace(f"识别到验证码为 : '{captcha_text}'", no_log=True)
if len(captcha_text) != 4:
+ self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("识别到的验证码长度不等于 4 个字符 !")
return captcha_text
except Exception as e:
- self._showTrace(f"验证码识别失败 ! : {e}")
+ self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
@@ -105,12 +109,13 @@ class LibLogin(LibOperator):
try:
self._showMsg("请输入验证码:")
captcha_text = self._waitMsg(timeout=15)
- self._showTrace(f"输入的验证码为 : '{captcha_text}'")
+ self._showTrace(f"输入的验证码为 : '{captcha_text}'", no_log=True)
if len(captcha_text) != 4:
+ self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("输入的验证码长度不等于 4 个字符 !")
return captcha_text
except Exception as e:
- self._showTrace(f"输入验证码失败 ! : {e}")
+ self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
return ""
@@ -120,13 +125,13 @@ class LibLogin(LibOperator):
# refresh captcha
try:
- self._showTrace("刷新验证码......")
+ self._showTrace("刷新验证码......", no_log=True)
self.__driver.find_element(
By.ID, "loadImgId"
).click()
return True
except Exception as e:
- self._showTrace(f"刷新验证码失败 ! : {e}")
+ self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR)
return False
@@ -140,14 +145,17 @@ class LibLogin(LibOperator):
if auto_captcha:
captcha_text = self.__autoRecognizeCaptcha()
else:
- self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
+ self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !", no_log=True)
captcha_text = self.__manualRecognizeCaptcha()
if captcha_text:
return captcha_text
else:
if not self.__refreshCaptcha():
return ""
- self._showTrace(f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !")
+ self._showTrace(
+ f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !",
+ self.TraceLevel.WARNING
+ )
return ""
@@ -162,7 +170,7 @@ class LibLogin(LibOperator):
captcha_element.send_keys(captcha_text)
return True
except Exception as e:
- self._showTrace(f"验证码填写失败 ! : {e}")
+ self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR)
return False
@@ -175,11 +183,11 @@ class LibLogin(LibOperator):
) -> bool:
if self.__driver is None:
- self._showTrace("未提供有效 WebDriver 实例 !")
+ self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
return False
# begin login process
for attempt in range(max_attempts):
- self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......")
+ self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......", no_log=True)
if not self.__fillLogInElements(
username,
password,
@@ -190,7 +198,7 @@ class LibLogin(LibOperator):
continue
if not self.__fillCaptchaElement(captcha_text):
continue
- self._showTrace("尝试登录...")
+ self._showTrace("尝试登录...", no_log=True)
try:
self.__driver.find_element(
By.XPATH,
@@ -203,5 +211,5 @@ class LibLogin(LibOperator):
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
return True
else:
- self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !")
+ self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !",self.TraceLevel.WARNING)
return False
diff --git a/src/operators/LibLogout.py b/src/operators/LibLogout.py
index c907835..72edf5d 100644
--- a/src/operators/LibLogout.py
+++ b/src/operators/LibLogout.py
@@ -42,7 +42,7 @@ class LibLogout(LibOperator):
) -> bool:
if self.__driver is None:
- self._showTrace("未提供有效 WebDriver 实例 !")
+ self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
return False
try:
self.__driver.find_element(
@@ -51,5 +51,5 @@ class LibLogout(LibOperator):
self._showTrace(f"用户 {username} 注销成功 !")
return True
except Exception as e:
- self._showTrace(f"用户 {username} 注销失败 ! : {e}")
+ self._showTrace(f"用户 {username} 注销失败 ! : {e}", self.TraceLevel.ERROR)
return False
diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py
index cea784c..62114a6 100644
--- a/src/operators/LibRenew.py
+++ b/src/operators/LibRenew.py
@@ -54,14 +54,14 @@ class LibRenew(LibTimeSelector):
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage"))
)
except:
- self._showTrace("续约时间选择界面加载失败 !")
+ self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR)
return False
head_message = head_message.text.strip()
if "警告" in head_message:
result_message = result_message.text.strip()
self._showTrace(f"\n"\
f" 续约失败 !\n"\
- f" {result_message}")
+ f" {result_message}", no_log=True)
return False
try:
WebDriverWait(self.__driver, 2).until(
@@ -73,7 +73,7 @@ class LibRenew(LibTimeSelector):
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
)
except:
- self._showTrace("续约时间选择界面加载失败 !")
+ self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR)
return False
return True
@@ -91,7 +91,7 @@ class LibRenew(LibTimeSelector):
renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"]
- target_renew_mins = self._timeToMins(end_time) + renew_info["expect_duration"]*60
+ target_renew_mins = self._timeStrToMins(end_time) + renew_info["expect_duration"]*60
# Validate and adjust target renew time to library closing time
if not self.__validateAndAdjustRenewTime(end_time, target_renew_mins):
@@ -99,7 +99,7 @@ class LibRenew(LibTimeSelector):
renew_ok_btn = self.__driver.find_element(By.CSS_SELECTOR, "#extendDiv .btnOK")
renew_time_opts = self.__driver.find_elements(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
if not renew_time_opts:
- self._showTrace("当前未查询到可用续约时间 !")
+ self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING)
return False
# Find best renewal time option
@@ -110,7 +110,8 @@ class LibRenew(LibTimeSelector):
return self.__confirmRenewal(best_opt, best_text, actual_diff, record, renew_ok_btn)
self._showTrace(
"无法选择最近的可用续约时间 ! "
- f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
+ f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !",
+ self.TraceLevel.WARNING
)
self._showTrace(f"当前可供续约的时间有: {free_times}")
return False
@@ -127,12 +128,12 @@ class LibRenew(LibTimeSelector):
"""
LIBRARY_CLOSE_TIME = 1410 # 23:30 in minutes
if target_renew_mins > LIBRARY_CLOSE_TIME:
- actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeToMins(end_time)
+ actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time)
if actual_renew_duration <= 0:
- self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !")
+ self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR)
return False
self._showTrace(
- f"续约时间已调整至闭馆时间 {self._minsToTime(LIBRARY_CLOSE_TIME)},"
+ f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)},"
f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟"
)
return True
@@ -163,7 +164,7 @@ class LibRenew(LibTimeSelector):
ok_btn.click()
return True
except:
- self._showTrace("确认续约时发生错误 !")
+ self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR)
return False
@@ -175,28 +176,29 @@ class LibRenew(LibTimeSelector):
) -> bool:
if self.__driver is None:
- self._showTrace("未提供有效 WebDriver 实例 !")
+ self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
return False
try:
renew_btn = WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "btnExtend"))
)
except:
- self._showTrace(f"用户 {username} 续约界面加载失败 !")
+ self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR)
return False
if "disabled" in renew_btn.get_attribute("class"):
- self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
+ self._showLog(f"用户 {username} 续约按钮不可用, 可能不在场馆内")
+ self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试", no_log=True)
return False
renew_btn.click()
if not self.__waitRenewDialog():
- self._showTrace(f"用户 {username} 续约失败 !")
+ self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR)
# After the renewal, the webpage will display a mask overlay,
# so we need to refresh the page for subsequent operations.
self.__driver.refresh()
return False
if not self.__selectNearestTime(record, reserve_info):
- self._showTrace(f"用户 {username} 续约失败 !")
+ self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR)
self.__driver.refresh()
return False
if self._waitResponseLoad():
diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py
index 224eeee..55572bd 100644
--- a/src/operators/LibReserve.py
+++ b/src/operators/LibReserve.py
@@ -72,13 +72,13 @@ class LibReserve(LibTimeSelector):
By.CSS_SELECTOR, ".layoutSeat dd"
)
if not content_elements:
- self._showTrace("未找到预约结果")
+ self._showTrace("未找到预约结果", self.TraceLevel.WARNING)
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)}")
+ self._showTrace(f"预约失败 - {"".join(contents)}", self.TraceLevel.ERROR)
raise
if "预定好了" in title or "预约成功" in title or "操作成功" in title:
if len(contents) >= 6:
@@ -96,7 +96,7 @@ class LibReserve(LibTimeSelector):
)
return True
except:
- self._showTrace(f"预约结果加载失败 !")
+ self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR)
return False
@@ -107,6 +107,8 @@ class LibReserve(LibTimeSelector):
try:
# must contain the required infomation
+ # key 'place' is no need to check
+ # because 'place' is only has one possible value '1' or '图书馆'
if reserve_info.get("floor") is None: # if existence ?
raise ValueError("未指定楼层")
if reserve_info["floor"] not in self.__floor_map: # if in the mao ?
@@ -123,7 +125,13 @@ class LibReserve(LibTimeSelector):
except ValueError as e:
self._showTrace(
f"预约信息错误 ! : {e}, "\
- f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
+ f"由于缺少必要的预约信息, 无法开始预约流程",
+ self.TraceLevel.ERROR
+ )
+ self._showTrace(
+ f"预约信息错误 ! : {e}, "\
+ f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整",
+ no_log=True
)
return False
@@ -133,17 +141,20 @@ class LibReserve(LibTimeSelector):
reserve_info: dict
) -> bool:
- cur_date = time.strftime("%Y-%m-%d", time.localtime())
+ cur_date_str = time.strftime("%Y-%m-%d", time.localtime())
+ cur_timestamp = time.mktime(time.strptime(cur_date_str, "%Y-%m-%d"))
if reserve_info.get("date") is None:
- reserve_info["date"] = cur_date
- self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}")
+ reserve_info["date"] = cur_date_str
+ self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date_str}")
else:
- if reserve_info["date"] < cur_date:
+ res_timestamp = time.mktime(time.strptime(reserve_info["date"], "%Y-%m-%d"))
+ if res_timestamp < cur_timestamp:
self._showTrace(
f"预约日期错误 ! :"\
- f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期"
+ f"{reserve_info['date']} 早于当前日期 {cur_date_str}, 自动设置为当前日期",
+ self.TraceLevel.WARNING
)
- reserve_info["date"] = cur_date
+ reserve_info["date"] = cur_date_str
return True
@@ -190,10 +201,13 @@ class LibReserve(LibTimeSelector):
if reserve_info.get("end_time") is None:
reserve_info["end_time"] = {}
if "time" not in reserve_info["end_time"]:
- end_mins = self._timeToMins(reserve_info["begin_time"]["time"])
+ # here we add the expect duration to the begin time first,
+ # the edge case that the end time is later than 23:30 will
+ # be handled in __finalCheck. so no need to concern about it.
+ end_mins = self._timeStrToMins(reserve_info["begin_time"]["time"])
end_mins = end_mins + int(reserve_info["expect_duration"]*60)
reserve_info["end_time"] = {
- "time": self._minsToTime(end_mins),
+ "time": self._minsToTimeStr(end_mins),
"max_diff": 30,
"prefer_early": False
}
@@ -215,32 +229,39 @@ class LibReserve(LibTimeSelector):
):
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
- begin_mins = self._timeToMins(begin_time["time"])
- end_mins = self._timeToMins(end_time["time"])
+ begin_mins = self._timeStrToMins(begin_time["time"])
+ end_mins = self._timeStrToMins(end_time["time"])
+
# if end time is earlier than begin_time, exchange them
- if end_mins < begin_mins:
+ # except that the user has set the satisfy_duration to True
+ if end_mins < begin_mins and reserve_info["satisfy_duration"] is False:
self._showTrace(
- f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
+ f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间",
+ self.TraceLevel.WARNING
)
- reserve_info["end_time"] = begin_time
- reserve_info["begin_time"] = end_time
- begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
- begin_mins = self._timeToMins(begin_time["time"])
- end_mins = self._timeToMins(end_time["time"])
+ reserve_info["end_time"], reserve_info["begin_time"] = begin_time, end_time
+ begin_time, end_time = end_time, begin_time
+ begin_mins = self._timeStrToMins(begin_time["time"])
+ end_mins = self._timeStrToMins(end_time["time"])
+
# ensure the end time is not later than 23:30
- if end_mins > self._timeToMins("23:30"):
+ max_end_mins = self._timeStrToMins("23:30")
+ if end_mins > max_end_mins:
self._showTrace(
- f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
+ f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30",
+ self.TraceLevel.WARNING
)
reserve_info["end_time"]["time"] = "23:30"
- end_mins = self._timeToMins("23:30")
+ end_mins = max_end_mins
+
# ensure the duration is not longer than 8 hours
if reserve_info["satisfy_duration"]:
if reserve_info["expect_duration"] > 8:
self._showTrace(
f"该用户设置了优先满足时长要求, 但是预约期望持续时间 "
f"{reserve_info['expect_duration']} 小时 "
- f"超出最大时长 8 小时, 自动设置为 8 小时"
+ f"超出最大时长 8 小时, 自动设置为 8 小时",
+ self.TraceLevel.WARNING
)
reserve_info["expect_duration"] = 8
else:
@@ -248,9 +269,10 @@ class LibReserve(LibTimeSelector):
self._showTrace(
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
f"{float((end_mins - begin_mins)/60)} 小时 "
- f"超出最大时长 8 小时, 自动设置为 8 小时"
+ f"超出最大时长 8 小时, 自动设置为 8 小时",
+ self.TraceLevel.WARNING
)
- reserve_info["end_time"]["time"] = self._minsToTime(begin_mins + 8*60)
+ reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60)
return True
@@ -274,8 +296,8 @@ class LibReserve(LibTimeSelector):
self._showTrace(
f"预约信息检查完成, 准备预约 "
f"{reserve_info['date']} "
- f"{reserve_info['begin_time']["time"]} - "
- f"{reserve_info['end_time']["time"]} "
+ 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']]} "
@@ -418,7 +440,7 @@ class LibReserve(LibTimeSelector):
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
- self._showTrace("加载房间/区域失败 !")
+ self._showTrace("加载房间/区域失败 !", self.TraceLevel.ERROR)
return False
# select room
try:
@@ -428,7 +450,7 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"房间 {display_room} 选择成功 !")
return True
except:
- self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
+ self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
return False
@@ -446,7 +468,7 @@ class LibReserve(LibTimeSelector):
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']"))
)
except:
- self._showTrace(f"座位加载失败 !")
+ self._showTrace(f"座位加载失败 !", self.TraceLevel.ERROR)
return False
try:
all_seats = self.__driver.find_elements(
@@ -464,9 +486,10 @@ class LibReserve(LibTimeSelector):
seat_status = seat_link.get_attribute("title")
self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'")
return True
- self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确")
+ self._showLog(f"座位 {seat_id} 在该楼层区域中不存在", self.TraceLevel.WARNING)
+ self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", no_log=True)
except:
- self._showTrace(f"座位选择失败 !")
+ self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR)
return False
@@ -481,6 +504,9 @@ class LibReserve(LibTimeSelector):
"""
Select the nearest available time option.
+
+ Returns:
+ int: The actual selected time value in minutes.
"""
# Wait for time options to load
try:
@@ -490,7 +516,7 @@ class LibReserve(LibTimeSelector):
)
)
except:
- self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
+ self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR)
return -1
# Find best time option
@@ -499,7 +525,7 @@ class LibReserve(LibTimeSelector):
f"#{time_id} ul li a"
)
if not all_time_opts:
- self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
+ self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR)
return -1
best_opt, best_text, actual_diff, free_times = self._findBestTimeOption(
all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True
@@ -515,8 +541,8 @@ class LibReserve(LibTimeSelector):
)
return target_time
self._showTrace(
- f"无法选择最近的 {time_type} {self._minsToTime(target_time)}, "
- f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟"
+ f"无法选择最近的 {time_type} {self._minsToTimeStr(target_time)}, "
+ f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", self.TraceLevel.WARNING
)
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
return -1
@@ -526,51 +552,58 @@ class LibReserve(LibTimeSelector):
self,
begin_time: dict,
end_time: dict,
- expct_duration: int = 4,
+ expect_duration: int = 4,
satisfy_duration: bool = True
) -> bool:
- """Select seat begin and end time."""
- expect_begin_time = actual_begin_time = begin_time["time"]
- expect_end_time = actual_end_time = end_time["time"]
- expect_begin_mins = self._timeToMins(expect_begin_time)
- actual_begin_mins = expect_begin_mins
- expect_end_mins = self._timeToMins(expect_end_time)
+ """
+ Select seat begin and end time.
+ """
+ exp_beg_tm_str = begin_time["time"]
+ exp_end_tm_str = end_time["time"]
+ # Initialize actual time strings for logging
+ act_beg_tm_str = exp_beg_tm_str
+ act_end_tm_str = exp_end_tm_str
+ exp_beg_mins = self._timeStrToMins(exp_beg_tm_str)
+ act_beg_mins = exp_beg_mins
+ exp_end_mins = self._timeStrToMins(exp_end_tm_str)
+ act_end_mins = exp_end_mins
# Select begin time
- if self.__selectNearestTime(
+ act_beg_mins = self.__selectNearestTime(
time_id="startTime",
time_type="开始时间",
- target_time=expect_begin_mins,
+ target_time=exp_beg_mins,
max_time_diff=begin_time["max_diff"],
prefer_earlier=begin_time["prefer_early"]
- ) == -1:
+ )
+ if act_beg_mins == -1:
return False
- actual_begin_time = self._minsToTime(expect_begin_mins)
- actual_begin_mins = self._timeToMins(actual_begin_time)
+ act_beg_tm_str = self._minsToTimeStr(act_beg_mins)
# If 'satisfy_duration' is True, select end time based on actual begin time
if satisfy_duration:
- expect_end_mins = self.validateAndAdjustEndTime(actual_begin_mins, expct_duration)
- expect_end_time = self._minsToTime(expect_end_mins)
+ exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expect_duration))
+ exp_end_tm_str = self._minsToTimeStr(exp_end_mins)
self._showTrace(
- f"需要满足期望预约持续时间: {expct_duration} 小时, "
- f"根据开始时间 {actual_begin_time} 计算结束时间: {expect_end_time}"
+ f"需要满足期望预约持续时间: {expect_duration} 小时, "
+ f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}"
)
# Select end time
- if self.__selectNearestTime(
+ act_end_mins = self.__selectNearestTime(
time_id="endTime",
time_type="结束时间",
- target_time=expect_end_mins,
+ target_time=exp_end_mins,
max_time_diff=end_time["max_diff"],
prefer_earlier=end_time["prefer_early"]
- ) == -1:
+ )
+ if act_end_mins == -1:
return False
- actual_end_time = self._minsToTime(expect_end_mins)
+ act_end_tm_str = self._minsToTimeStr(act_end_mins)
self._showTrace(
- f"期望预约时间段: {expect_begin_time} - {expect_end_time}, "
- f"实际预约时间段: {actual_begin_time} - {actual_end_time}"
+ f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, "
+ f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}"
)
return True
@@ -584,12 +617,13 @@ class LibReserve(LibTimeSelector):
"""
Validate and adjust reserve end time to library closing time if needed.
"""
- LIBRARY_CLOSE_TIME = self._timeToMins("23:30")
- expect_end_mins = begin_mins + duration * 60
+ LIBRARY_CLOSE_TIME = self._timeStrToMins("23:30")
+ expect_end_mins = int(begin_mins + duration*60)
if expect_end_mins > LIBRARY_CLOSE_TIME:
expect_end_mins = LIBRARY_CLOSE_TIME
self._showTrace(
- f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
+ f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30",
+ self.TraceLevel.WARNING
)
return expect_end_mins
@@ -616,7 +650,7 @@ class LibReserve(LibTimeSelector):
EC.presence_of_element_located((By.ID, "seatLayout"))
)
except:
- self._showTrace(f"加载预约选座页面失败 !")
+ self._showTrace(f"加载预约选座页面失败 !", self.TraceLevel.ERROR)
return False
# date, place, floor, room
if not self.__selectDate(reserve_info["date"]):
@@ -635,7 +669,7 @@ class LibReserve(LibTimeSelector):
elif not self.__selectSeatTime(
begin_time=reserve_info["begin_time"],
end_time=reserve_info["end_time"],
- expct_duration=reserve_info["expect_duration"],
+ expect_duration=reserve_info["expect_duration"],
satisfy_duration=reserve_info["satisfy_duration"]
):
pass
@@ -649,11 +683,11 @@ class LibReserve(LibTimeSelector):
raise
reserve_success = True
except:
- self._showTrace(f"预约提交失败 !")
+ self._showTrace(f"预约提交失败 !", self.TraceLevel.ERROR)
if not submit_reserve and have_hover_on_page:
self.__driver.refresh()
if reserve_success:
self._showTrace(f"用户 {username} 预约成功 !")
else:
- self._showTrace(f"用户 {username} 预约失败 !")
+ self._showTrace(f"用户 {username} 预约失败 !", self.TraceLevel.ERROR)
return reserve_success
\ No newline at end of file
diff --git a/src/utils/__init__.py b/src/utils/__init__.py
index a05bee0..4d0a056 100644
--- a/src/utils/__init__.py
+++ b/src/utils/__init__.py
@@ -2,7 +2,7 @@
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- - ConfigManager: Configuration manager class for the AutoLibrary project.
+ - TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
"""
\ No newline at end of file