From f2029253337c25516c400b0b2a3c803372d204af Mon Sep 17 00:00:00 2001 From: hyb-oyqq <1512383570@qq.com> Date: Fri, 18 Jul 2025 18:59:19 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E9=A1=B9=E7=9B=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除多个不再使用的源文件,包括动画、下载、配置、UI 相关文件及图标,清理代码库以提高可维护性。 --- result.csv | 979 ++++++++++++++++++ source/{ => bin}/aria2c.exe | Bin source/{ => bin}/cfst.exe | Bin source/bin/result.csv | 11 + source/core/__init__.py | 11 + source/{ => core}/animations.py | 0 source/core/debug_manager.py | 57 + source/core/download_manager.py | 493 +++++++++ source/core/ui_manager.py | 94 ++ source/{ => data}/config.py | 0 source/{ => data}/pic_data.py | 0 source/main_window.py | 864 ++++------------ source/main_window.py.bak | 656 ++++++++++++ source/{ => resources/data}/ip.txt | 0 source/{ => resources/data}/ipv6.txt | 0 source/{ => resources/icons}/icon.ico | Bin source/resources/images/After/voaf_ga01.jpg | Bin 0 -> 165408 bytes source/resources/images/After/voaf_ga02.jpg | Bin 0 -> 183090 bytes source/resources/images/BG/bg1.jpg | Bin 0 -> 584706 bytes source/resources/images/BG/bg2.jpg | Bin 0 -> 256189 bytes source/resources/images/BG/bg3.jpg | Bin 0 -> 1332885 bytes source/resources/images/BG/bg4.jpg | Bin 0 -> 49738 bytes source/resources/images/BG/menubg.jpg | Bin 0 -> 234117 bytes source/resources/images/BTN/exit.bmp | Bin 0 -> 14463 bytes source/resources/images/BTN/start_install.bmp | Bin 0 -> 14558 bytes source/resources/images/ICO/icon.ico | Bin 0 -> 270622 bytes source/resources/images/ICO/icon.png | Bin 0 -> 40924 bytes .../resources/images/LOGO/gl_head_logo_jp.png | Bin 0 -> 8824 bytes source/resources/images/LOGO/vo01_logo.png | Bin 0 -> 51805 bytes source/resources/images/LOGO/vo02_logo.png | Bin 0 -> 50772 bytes source/resources/images/LOGO/vo03_logo.png | Bin 0 -> 181574 bytes source/resources/images/LOGO/vo04_logo.png | Bin 0 -> 53370 bytes source/resources/images/LOGO/voaf_logo.png | Bin 0 -> 334745 bytes source/resources/images/vol4/vo04_ga01.jpg | Bin 0 -> 169949 bytes source/resources/images/vol4/vo04_ga05.jpg | Bin 0 -> 188593 bytes source/resources/images/vol4/vo04_ga06.jpg | Bin 0 -> 201959 bytes source/resources/images/vol4/vo04_ga07.jpg | Bin 0 -> 170372 bytes source/{ => ui}/Ui_install.py | 2 +- source/{ => ui}/install.ui | 0 source/{ => ui}/popup.ui | 0 source/utils/__init__.py | 18 + source/{utils.py => utils/helpers.py} | 135 ++- source/utils/logger.py | 19 + source/workers/__init__.py | 14 + source/workers/config_fetch_thread.py | 52 + source/{ => workers}/download.py | 2 +- source/workers/extraction_thread.py | 31 + source/workers/hash_thread.py | 28 + source/{ => workers}/ip_optimizer.py | 94 +- 49 files changed, 2876 insertions(+), 684 deletions(-) create mode 100644 result.csv rename source/{ => bin}/aria2c.exe (100%) rename source/{ => bin}/cfst.exe (100%) create mode 100644 source/bin/result.csv create mode 100644 source/core/__init__.py rename source/{ => core}/animations.py (100%) create mode 100644 source/core/debug_manager.py create mode 100644 source/core/download_manager.py create mode 100644 source/core/ui_manager.py rename source/{ => data}/config.py (100%) rename source/{ => data}/pic_data.py (100%) create mode 100644 source/main_window.py.bak rename source/{ => resources/data}/ip.txt (100%) rename source/{ => resources/data}/ipv6.txt (100%) rename source/{ => resources/icons}/icon.ico (100%) create mode 100644 source/resources/images/After/voaf_ga01.jpg create mode 100644 source/resources/images/After/voaf_ga02.jpg create mode 100644 source/resources/images/BG/bg1.jpg create mode 100644 source/resources/images/BG/bg2.jpg create mode 100644 source/resources/images/BG/bg3.jpg create mode 100644 source/resources/images/BG/bg4.jpg create mode 100644 source/resources/images/BG/menubg.jpg create mode 100644 source/resources/images/BTN/exit.bmp create mode 100644 source/resources/images/BTN/start_install.bmp create mode 100644 source/resources/images/ICO/icon.ico create mode 100644 source/resources/images/ICO/icon.png create mode 100644 source/resources/images/LOGO/gl_head_logo_jp.png create mode 100644 source/resources/images/LOGO/vo01_logo.png create mode 100644 source/resources/images/LOGO/vo02_logo.png create mode 100644 source/resources/images/LOGO/vo03_logo.png create mode 100644 source/resources/images/LOGO/vo04_logo.png create mode 100644 source/resources/images/LOGO/voaf_logo.png create mode 100644 source/resources/images/vol4/vo04_ga01.jpg create mode 100644 source/resources/images/vol4/vo04_ga05.jpg create mode 100644 source/resources/images/vol4/vo04_ga06.jpg create mode 100644 source/resources/images/vol4/vo04_ga07.jpg rename source/{ => ui}/Ui_install.py (99%) rename source/{ => ui}/install.ui (100%) rename source/{ => ui}/popup.ui (100%) create mode 100644 source/utils/__init__.py rename source/{utils.py => utils/helpers.py} (71%) create mode 100644 source/utils/logger.py create mode 100644 source/workers/__init__.py create mode 100644 source/workers/config_fetch_thread.py rename source/{ => workers}/download.py (99%) create mode 100644 source/workers/extraction_thread.py create mode 100644 source/workers/hash_thread.py rename source/{ => workers}/ip_optimizer.py (55%) diff --git a/result.csv b/result.csv new file mode 100644 index 0000000..fd888bb --- /dev/null +++ b/result.csv @@ -0,0 +1,979 @@ +IP 地址,已发送,已接收,丢包率,平均延迟,下载速度(MB/s),地区码 +141.101.121.206,4,4,0.00,170.61,0.00,N/A +141.101.120.141,4,4,0.00,172.19,0.00,N/A +172.64.34.217,4,4,0.00,172.73,0.00,N/A +104.18.232.108,4,4,0.00,175.13,0.00,N/A +104.19.49.47,4,4,0.00,175.76,0.00,N/A +104.18.210.110,4,4,0.00,176.37,0.00,N/A +104.18.133.38,4,4,0.00,176.87,0.00,N/A +172.65.200.235,4,4,0.00,180.18,0.00,N/A +172.65.206.215,4,4,0.00,181.09,0.00,N/A +172.65.88.110,4,4,0.00,181.92,0.00,N/A +104.18.87.58,4,4,0.00,182.15,0.00,N/A +104.27.195.221,4,4,0.00,182.32,0.00,N/A +172.64.35.209,4,4,0.00,182.80,0.00,N/A +172.65.83.97,4,4,0.00,183.80,0.00,N/A +172.67.115.45,4,4,0.00,184.35,0.00,N/A +172.67.109.61,4,4,0.00,184.42,0.00,N/A +104.27.199.104,4,4,0.00,184.61,0.00,N/A +104.27.55.140,4,4,0.00,184.95,0.00,N/A +104.23.126.50,4,4,0.00,186.35,0.00,N/A +104.23.97.94,4,4,0.00,186.58,0.00,N/A +172.65.80.97,4,4,0.00,187.11,0.00,N/A +172.65.141.175,4,4,0.00,187.34,0.00,N/A +104.27.49.243,4,4,0.00,188.22,0.00,N/A +108.162.195.23,4,4,0.00,189.54,0.00,N/A +172.65.146.228,4,4,0.00,189.77,0.00,N/A +172.65.89.157,4,4,0.00,189.96,0.00,N/A +104.25.4.81,4,4,0.00,190.07,0.00,N/A +104.25.11.89,4,4,0.00,190.35,0.00,N/A +172.65.90.242,4,4,0.00,190.36,0.00,N/A +104.25.138.144,4,4,0.00,190.39,0.00,N/A +104.27.200.172,4,4,0.00,190.83,0.00,N/A +172.67.88.239,4,4,0.00,191.16,0.00,N/A +104.18.250.196,4,4,0.00,192.35,0.00,N/A +172.65.240.126,4,4,0.00,192.80,0.00,N/A +104.27.118.3,4,4,0.00,192.95,0.00,N/A +104.25.63.97,4,4,0.00,192.96,0.00,N/A +104.27.204.236,4,4,0.00,193.05,0.00,N/A +104.25.47.196,4,4,0.00,193.90,0.00,N/A +172.66.135.31,4,4,0.00,194.02,0.00,N/A +104.16.201.188,4,4,0.00,194.24,0.00,N/A +104.27.201.28,4,4,0.00,195.17,0.00,N/A +104.27.66.223,4,4,0.00,195.48,0.00,N/A +172.65.81.92,4,4,0.00,196.15,0.00,N/A +172.65.138.75,4,4,0.00,196.45,0.00,N/A +162.159.9.55,4,4,0.00,196.60,0.00,N/A +104.19.77.26,4,4,0.00,197.43,0.00,N/A +172.65.125.141,4,4,0.00,197.70,0.00,N/A +104.25.101.238,4,4,0.00,199.28,0.00,N/A +141.101.113.135,4,4,0.00,199.94,0.00,N/A +104.24.253.165,4,4,0.00,201.58,0.00,N/A +104.25.43.12,4,4,0.00,201.72,0.00,N/A +104.27.64.178,4,4,0.00,202.07,0.00,N/A +104.25.40.112,4,4,0.00,202.49,0.00,N/A +172.65.85.42,4,4,0.00,203.00,0.00,N/A +104.27.206.177,4,4,0.00,203.81,0.00,N/A +104.27.194.185,4,4,0.00,204.52,0.00,N/A +104.27.57.176,4,4,0.00,204.72,0.00,N/A +162.159.6.99,4,4,0.00,205.14,0.00,N/A +104.16.208.207,4,4,0.00,205.96,0.00,N/A +172.65.106.7,4,4,0.00,206.00,0.00,N/A +104.25.100.152,4,4,0.00,206.14,0.00,N/A +104.25.9.10,4,4,0.00,206.22,0.00,N/A +104.25.2.103,4,4,0.00,206.67,0.00,N/A +104.25.6.214,4,4,0.00,207.38,0.00,N/A +104.27.125.248,4,4,0.00,207.56,0.00,N/A +104.23.99.171,4,4,0.00,208.38,0.00,N/A +162.159.20.67,4,4,0.00,208.69,0.00,N/A +172.65.137.158,4,4,0.00,208.89,0.00,N/A +141.101.90.252,4,4,0.00,208.91,0.00,N/A +104.17.27.5,4,4,0.00,209.43,0.00,N/A +104.23.96.103,4,4,0.00,209.71,0.00,N/A +198.41.200.113,4,4,0.00,210.96,0.00,N/A +104.25.3.135,4,4,0.00,211.53,0.00,N/A +172.67.192.153,4,4,0.00,213.75,0.00,N/A +172.67.128.121,4,4,0.00,216.03,0.00,N/A +104.16.192.233,4,4,0.00,216.10,0.00,N/A +104.29.158.150,4,4,0.00,216.16,0.00,N/A +104.19.27.221,4,4,0.00,217.16,0.00,N/A +104.16.238.110,4,4,0.00,219.89,0.00,N/A +104.21.110.121,4,4,0.00,220.02,0.00,N/A +172.67.142.189,4,4,0.00,221.18,0.00,N/A +162.159.10.98,4,4,0.00,221.72,0.00,N/A +172.67.141.146,4,4,0.00,222.05,0.00,N/A +104.21.21.9,4,4,0.00,222.20,0.00,N/A +104.29.157.254,4,4,0.00,223.36,0.00,N/A +104.21.44.171,4,4,0.00,223.49,0.00,N/A +104.21.33.67,4,4,0.00,224.51,0.00,N/A +162.159.14.250,4,4,0.00,225.19,0.00,N/A +172.67.149.124,4,4,0.00,225.46,0.00,N/A +104.21.60.76,4,4,0.00,226.39,0.00,N/A +104.21.34.167,4,4,0.00,228.03,0.00,N/A +104.21.66.44,4,4,0.00,228.14,0.00,N/A +198.41.193.202,4,4,0.00,228.58,0.00,N/A +172.67.231.90,4,4,0.00,229.40,0.00,N/A +172.65.60.144,4,4,0.00,229.46,0.00,N/A +162.159.11.102,4,4,0.00,229.99,0.00,N/A +188.114.97.221,4,4,0.00,230.32,0.00,N/A +104.21.31.187,4,4,0.00,230.39,0.00,N/A +198.41.206.251,4,4,0.00,230.57,0.00,N/A +172.67.169.129,4,4,0.00,231.16,0.00,N/A +104.18.221.160,4,4,0.00,231.31,0.00,N/A +172.67.251.106,4,4,0.00,231.38,0.00,N/A +104.21.19.88,4,4,0.00,232.40,0.00,N/A +104.21.76.1,4,4,0.00,233.29,0.00,N/A +104.21.85.131,4,4,0.00,233.30,0.00,N/A +104.21.25.159,4,4,0.00,233.59,0.00,N/A +172.67.208.10,4,4,0.00,234.32,0.00,N/A +172.64.92.242,4,4,0.00,234.75,0.00,N/A +172.65.5.190,4,4,0.00,235.78,0.00,N/A +173.245.59.112,4,4,0.00,237.44,0.00,N/A +104.21.108.113,4,4,0.00,237.57,0.00,N/A +104.18.245.3,4,4,0.00,238.08,0.00,N/A +162.159.40.247,4,4,0.00,239.48,0.00,N/A +104.19.0.13,4,4,0.00,240.12,0.00,N/A +104.21.42.59,4,4,0.00,240.75,0.00,N/A +104.19.15.109,4,4,0.00,248.82,0.00,N/A +104.21.49.195,4,4,0.00,249.41,0.00,N/A +104.19.14.123,4,4,0.00,250.93,0.00,N/A +198.41.199.9,4,4,0.00,253.58,0.00,N/A +104.29.159.206,4,4,0.00,255.12,0.00,N/A +108.162.198.59,4,4,0.00,260.18,0.00,N/A +104.29.144.155,4,4,0.00,261.55,0.00,N/A +198.41.216.166,4,4,0.00,263.88,0.00,N/A +162.159.25.79,4,4,0.00,265.49,0.00,N/A +104.16.173.251,4,4,0.00,265.54,0.00,N/A +104.16.219.16,4,4,0.00,267.19,0.00,N/A +103.21.244.66,4,4,0.00,267.32,0.00,N/A +198.41.192.140,4,4,0.00,270.43,0.00,N/A +162.159.16.8,4,4,0.00,281.10,0.00,N/A +104.29.145.249,4,4,0.00,284.12,0.00,N/A +104.17.14.107,4,4,0.00,284.74,0.00,N/A +162.159.33.9,4,3,0.25,163.50,0.00,N/A +104.19.1.203,4,3,0.25,165.66,0.00,N/A +104.17.10.246,4,3,0.25,168.12,0.00,N/A +172.65.204.110,4,3,0.25,168.44,0.00,N/A +104.16.251.149,4,3,0.25,169.01,0.00,N/A +104.19.24.233,4,3,0.25,169.23,0.00,N/A +141.101.122.200,4,3,0.25,169.24,0.00,N/A +104.16.222.196,4,3,0.25,169.37,0.00,N/A +104.17.18.41,4,3,0.25,169.59,0.00,N/A +104.19.59.153,4,3,0.25,170.84,0.00,N/A +104.17.3.191,4,3,0.25,171.07,0.00,N/A +172.65.212.42,4,3,0.25,171.19,0.00,N/A +172.65.194.74,4,3,0.25,171.33,0.00,N/A +104.16.194.52,4,3,0.25,171.42,0.00,N/A +198.41.203.196,4,3,0.25,173.66,0.00,N/A +104.19.40.143,4,3,0.25,173.79,0.00,N/A +104.16.224.56,4,3,0.25,174.55,0.00,N/A +104.16.171.114,4,3,0.25,174.63,0.00,N/A +104.17.16.183,4,3,0.25,174.76,0.00,N/A +141.101.115.123,4,3,0.25,175.35,0.00,N/A +198.41.217.76,4,3,0.25,175.82,0.00,N/A +141.101.123.213,4,3,0.25,176.23,0.00,N/A +198.41.201.13,4,3,0.25,176.44,0.00,N/A +104.19.136.126,4,3,0.25,176.96,0.00,N/A +104.16.193.195,4,3,0.25,177.00,0.00,N/A +104.21.224.85,4,3,0.25,177.85,0.00,N/A +172.65.238.127,4,3,0.25,177.96,0.00,N/A +104.18.206.194,4,3,0.25,178.00,0.00,N/A +172.65.233.45,4,3,0.25,178.90,0.00,N/A +104.19.50.237,4,3,0.25,179.04,0.00,N/A +104.19.61.11,4,3,0.25,179.38,0.00,N/A +172.65.216.254,4,3,0.25,179.63,0.00,N/A +173.245.58.254,4,3,0.25,179.74,0.00,N/A +198.41.222.188,4,3,0.25,179.95,0.00,N/A +173.245.49.150,4,3,0.25,180.27,0.00,N/A +104.16.195.175,4,3,0.25,180.62,0.00,N/A +104.19.57.218,4,3,0.25,182.05,0.00,N/A +190.93.247.8,4,3,0.25,182.22,0.00,N/A +104.16.191.64,4,3,0.25,183.41,0.00,N/A +172.64.42.188,4,3,0.25,183.54,0.00,N/A +104.25.158.236,4,3,0.25,183.65,0.00,N/A +162.159.23.138,4,3,0.25,183.81,0.00,N/A +172.64.159.95,4,3,0.25,184.51,0.00,N/A +104.27.115.83,4,3,0.25,185.01,0.00,N/A +104.27.93.122,4,3,0.25,185.08,0.00,N/A +162.159.27.175,4,3,0.25,185.26,0.00,N/A +104.27.90.5,4,3,0.25,185.65,0.00,N/A +104.25.134.158,4,3,0.25,185.71,0.00,N/A +172.65.197.229,4,3,0.25,185.75,0.00,N/A +104.25.104.84,4,3,0.25,185.96,0.00,N/A +104.27.192.21,4,3,0.25,186.04,0.00,N/A +104.16.216.68,4,3,0.25,186.26,0.00,N/A +198.41.207.148,4,3,0.25,186.59,0.00,N/A +104.27.198.29,4,3,0.25,186.64,0.00,N/A +104.27.117.34,4,3,0.25,186.68,0.00,N/A +172.65.148.94,4,3,0.25,186.96,0.00,N/A +104.25.121.4,4,3,0.25,187.30,0.00,N/A +104.25.49.216,4,3,0.25,187.42,0.00,N/A +104.25.84.187,4,3,0.25,187.48,0.00,N/A +104.27.116.178,4,3,0.25,188.87,0.00,N/A +104.16.215.44,4,3,0.25,188.89,0.00,N/A +104.18.234.235,4,3,0.25,188.90,0.00,N/A +104.25.108.235,4,3,0.25,188.91,0.00,N/A +104.27.197.249,4,3,0.25,189.45,0.00,N/A +104.27.122.29,4,3,0.25,189.92,0.00,N/A +104.16.189.44,4,3,0.25,190.13,0.00,N/A +104.19.58.126,4,3,0.25,190.13,0.00,N/A +104.18.238.3,4,3,0.25,190.26,0.00,N/A +172.65.128.134,4,3,0.25,190.66,0.00,N/A +104.27.51.248,4,3,0.25,190.79,0.00,N/A +104.16.178.48,4,3,0.25,191.00,0.00,N/A +172.65.203.83,4,3,0.25,191.08,0.00,N/A +104.25.112.128,4,3,0.25,191.28,0.00,N/A +104.25.83.3,4,3,0.25,191.51,0.00,N/A +172.64.82.38,4,3,0.25,191.55,0.00,N/A +104.27.193.89,4,3,0.25,191.69,0.00,N/A +104.25.5.141,4,3,0.25,192.19,0.00,N/A +162.159.4.237,4,3,0.25,192.21,0.00,N/A +104.25.45.112,4,3,0.25,192.49,0.00,N/A +104.16.184.4,4,3,0.25,192.59,0.00,N/A +172.65.103.133,4,3,0.25,193.07,0.00,N/A +104.25.136.115,4,3,0.25,193.45,0.00,N/A +104.25.135.203,4,3,0.25,193.77,0.00,N/A +104.27.62.142,4,3,0.25,194.12,0.00,N/A +104.27.120.219,4,3,0.25,194.18,0.00,N/A +104.19.55.100,4,3,0.25,194.19,0.00,N/A +104.20.62.129,4,3,0.25,194.26,0.00,N/A +104.24.249.90,4,3,0.25,194.30,0.00,N/A +104.25.24.7,4,3,0.25,194.43,0.00,N/A +104.25.198.124,4,3,0.25,194.78,0.00,N/A +104.16.170.74,4,3,0.25,195.30,0.00,N/A +104.25.139.149,4,3,0.25,195.68,0.00,N/A +104.27.47.127,4,3,0.25,196.12,0.00,N/A +104.25.80.93,4,3,0.25,196.20,0.00,N/A +104.25.25.78,4,3,0.25,196.23,0.00,N/A +104.20.24.111,4,3,0.25,196.53,0.00,N/A +104.27.96.63,4,3,0.25,196.87,0.00,N/A +104.25.64.161,4,3,0.25,197.45,0.00,N/A +172.65.108.167,4,3,0.25,198.03,0.00,N/A +172.65.198.92,4,3,0.25,198.90,0.00,N/A +104.27.203.151,4,3,0.25,199.26,0.00,N/A +172.65.207.20,4,3,0.25,199.30,0.00,N/A +172.65.220.142,4,3,0.25,199.39,0.00,N/A +104.25.131.218,4,3,0.25,199.47,0.00,N/A +172.65.182.19,4,3,0.25,200.05,0.00,N/A +104.19.42.3,4,3,0.25,200.06,0.00,N/A +104.25.22.250,4,3,0.25,200.09,0.00,N/A +104.25.155.139,4,3,0.25,200.19,0.00,N/A +104.25.7.77,4,3,0.25,200.55,0.00,N/A +104.18.240.134,4,3,0.25,200.85,0.00,N/A +104.23.138.193,4,3,0.25,201.23,0.00,N/A +104.16.255.166,4,3,0.25,201.57,0.00,N/A +172.65.171.208,4,3,0.25,201.79,0.00,N/A +104.19.51.187,4,3,0.25,201.87,0.00,N/A +104.25.67.102,4,3,0.25,201.96,0.00,N/A +172.64.37.142,4,3,0.25,202.65,0.00,N/A +104.25.15.172,4,3,0.25,202.72,0.00,N/A +104.16.183.157,4,3,0.25,202.80,0.00,N/A +104.25.16.44,4,3,0.25,202.92,0.00,N/A +104.27.119.20,4,3,0.25,203.03,0.00,N/A +104.27.202.72,4,3,0.25,203.32,0.00,N/A +104.27.205.247,4,3,0.25,204.24,0.00,N/A +104.27.44.147,4,3,0.25,204.95,0.00,N/A +172.65.162.180,4,3,0.25,205.18,0.00,N/A +104.25.27.102,4,3,0.25,205.50,0.00,N/A +104.25.20.71,4,3,0.25,205.55,0.00,N/A +104.25.118.22,4,3,0.25,205.63,0.00,N/A +104.27.196.211,4,3,0.25,205.69,0.00,N/A +162.159.2.235,4,3,0.25,205.82,0.00,N/A +104.24.242.122,4,3,0.25,205.83,0.00,N/A +172.65.169.247,4,3,0.25,205.93,0.00,N/A +104.25.137.129,4,3,0.25,206.03,0.00,N/A +108.162.192.52,4,3,0.25,206.74,0.00,N/A +172.65.87.122,4,3,0.25,206.76,0.00,N/A +172.65.102.200,4,3,0.25,206.82,0.00,N/A +104.25.77.56,4,3,0.25,206.91,0.00,N/A +104.25.160.115,4,3,0.25,207.14,0.00,N/A +104.16.226.129,4,3,0.25,207.18,0.00,N/A +104.25.115.147,4,3,0.25,207.34,0.00,N/A +172.65.84.239,4,3,0.25,207.39,0.00,N/A +104.25.154.250,4,3,0.25,207.40,0.00,N/A +104.16.190.135,4,3,0.25,207.60,0.00,N/A +172.65.101.45,4,3,0.25,207.72,0.00,N/A +104.27.48.85,4,3,0.25,207.91,0.00,N/A +104.25.152.164,4,3,0.25,208.62,0.00,N/A +172.65.166.48,4,3,0.25,208.71,0.00,N/A +104.25.146.130,4,3,0.25,208.94,0.00,N/A +104.25.28.221,4,3,0.25,209.34,0.00,N/A +172.65.237.6,4,3,0.25,209.59,0.00,N/A +104.24.252.194,4,3,0.25,209.71,0.00,N/A +104.25.46.121,4,3,0.25,210.02,0.00,N/A +104.25.169.246,4,3,0.25,210.61,0.00,N/A +104.25.161.66,4,3,0.25,210.67,0.00,N/A +104.27.207.21,4,3,0.25,210.90,0.00,N/A +104.16.198.181,4,3,0.25,210.97,0.00,N/A +104.23.100.117,4,3,0.25,211.25,0.00,N/A +162.159.1.252,4,3,0.25,211.26,0.00,N/A +172.67.23.7,4,3,0.25,211.28,0.00,N/A +162.159.8.88,4,3,0.25,211.35,0.00,N/A +172.65.135.249,4,3,0.25,211.63,0.00,N/A +172.65.165.6,4,3,0.25,211.69,0.00,N/A +104.19.64.35,4,3,0.25,212.13,0.00,N/A +104.19.46.41,4,3,0.25,212.23,0.00,N/A +104.25.120.222,4,3,0.25,212.38,0.00,N/A +104.21.69.157,4,3,0.25,212.45,0.00,N/A +190.93.244.83,4,3,0.25,212.70,0.00,N/A +104.19.178.229,4,3,0.25,212.79,0.00,N/A +104.25.122.149,4,3,0.25,213.01,0.00,N/A +104.25.17.46,4,3,0.25,213.05,0.00,N/A +104.17.8.199,4,3,0.25,213.24,0.00,N/A +104.25.109.0,4,3,0.25,213.25,0.00,N/A +104.16.207.56,4,3,0.25,213.36,0.00,N/A +172.67.130.27,4,3,0.25,214.47,0.00,N/A +172.64.38.0,4,3,0.25,215.75,0.00,N/A +198.41.198.1,4,3,0.25,216.23,0.00,N/A +104.18.205.192,4,3,0.25,216.30,0.00,N/A +104.18.239.237,4,3,0.25,216.80,0.00,N/A +172.65.163.87,4,3,0.25,216.98,0.00,N/A +104.19.39.46,4,3,0.25,217.28,0.00,N/A +104.21.46.108,4,3,0.25,217.74,0.00,N/A +104.19.8.219,4,3,0.25,217.91,0.00,N/A +162.159.13.95,4,3,0.25,218.47,0.00,N/A +172.65.232.188,4,3,0.25,218.78,0.00,N/A +104.17.22.192,4,3,0.25,219.23,0.00,N/A +172.67.252.113,4,3,0.25,219.49,0.00,N/A +104.19.56.193,4,3,0.25,220.02,0.00,N/A +104.21.26.80,4,3,0.25,220.23,0.00,N/A +104.21.24.120,4,3,0.25,221.44,0.00,N/A +104.19.33.84,4,3,0.25,221.66,0.00,N/A +172.67.143.82,4,3,0.25,222.08,0.00,N/A +172.67.254.234,4,3,0.25,222.35,0.00,N/A +104.21.112.16,4,3,0.25,223.11,0.00,N/A +104.19.21.218,4,3,0.25,223.37,0.00,N/A +104.19.73.167,4,3,0.25,223.90,0.00,N/A +104.21.23.135,4,3,0.25,224.35,0.00,N/A +104.16.234.1,4,3,0.25,224.35,0.00,N/A +104.19.48.44,4,3,0.25,224.48,0.00,N/A +172.67.207.103,4,3,0.25,224.66,0.00,N/A +172.67.221.88,4,3,0.25,224.72,0.00,N/A +104.21.88.25,4,3,0.25,225.10,0.00,N/A +104.16.218.194,4,3,0.25,225.40,0.00,N/A +104.17.134.211,4,3,0.25,225.62,0.00,N/A +172.67.159.242,4,3,0.25,225.83,0.00,N/A +104.17.21.1,4,3,0.25,227.12,0.00,N/A +104.18.209.181,4,3,0.25,227.46,0.00,N/A +172.67.244.246,4,3,0.25,227.66,0.00,N/A +172.67.253.56,4,3,0.25,228.01,0.00,N/A +172.67.247.4,4,3,0.25,228.89,0.00,N/A +104.16.169.161,4,3,0.25,228.97,0.00,N/A +172.67.218.10,4,3,0.25,229.20,0.00,N/A +104.21.70.108,4,3,0.25,229.34,0.00,N/A +172.67.223.187,4,3,0.25,229.65,0.00,N/A +172.65.213.174,4,3,0.25,230.05,0.00,N/A +104.21.43.192,4,3,0.25,230.10,0.00,N/A +104.21.14.60,4,3,0.25,230.30,0.00,N/A +172.67.241.43,4,3,0.25,230.34,0.00,N/A +104.17.33.86,4,3,0.25,231.27,0.00,N/A +172.67.140.236,4,3,0.25,231.73,0.00,N/A +104.19.36.71,4,3,0.25,232.24,0.00,N/A +104.18.204.121,4,3,0.25,232.55,0.00,N/A +172.67.199.61,4,3,0.25,232.65,0.00,N/A +104.21.9.13,4,3,0.25,233.36,0.00,N/A +104.19.160.32,4,3,0.25,234.72,0.00,N/A +104.21.58.175,4,3,0.25,234.77,0.00,N/A +172.67.220.236,4,3,0.25,234.79,0.00,N/A +162.159.7.161,4,3,0.25,235.58,0.00,N/A +104.19.84.157,4,3,0.25,235.61,0.00,N/A +104.16.211.208,4,3,0.25,235.86,0.00,N/A +104.18.243.77,4,3,0.25,237.12,0.00,N/A +172.67.217.240,4,3,0.25,238.03,0.00,N/A +172.67.219.51,4,3,0.25,238.64,0.00,N/A +104.19.75.223,4,3,0.25,239.77,0.00,N/A +104.21.39.161,4,3,0.25,239.78,0.00,N/A +172.67.250.234,4,3,0.25,239.85,0.00,N/A +104.21.125.50,4,3,0.25,240.77,0.00,N/A +104.17.40.37,4,3,0.25,241.11,0.00,N/A +104.21.61.36,4,3,0.25,241.76,0.00,N/A +172.67.246.106,4,3,0.25,242.09,0.00,N/A +172.67.255.212,4,3,0.25,243.02,0.00,N/A +104.16.228.223,4,3,0.25,243.10,0.00,N/A +104.21.63.40,4,3,0.25,243.37,0.00,N/A +104.19.79.26,4,3,0.25,243.66,0.00,N/A +104.21.90.89,4,3,0.25,245.12,0.00,N/A +104.21.91.252,4,3,0.25,248.01,0.00,N/A +104.19.99.114,4,3,0.25,248.71,0.00,N/A +104.18.220.135,4,3,0.25,248.95,0.00,N/A +104.17.156.56,4,3,0.25,249.01,0.00,N/A +104.16.156.57,4,3,0.25,249.55,0.00,N/A +104.19.13.228,4,3,0.25,250.32,0.00,N/A +104.16.223.248,4,3,0.25,250.33,0.00,N/A +104.21.84.7,4,3,0.25,253.21,0.00,N/A +104.21.32.10,4,3,0.25,254.79,0.00,N/A +104.19.52.148,4,3,0.25,255.58,0.00,N/A +188.114.99.114,4,3,0.25,256.25,0.00,N/A +162.159.26.85,4,3,0.25,257.47,0.00,N/A +104.17.24.239,4,3,0.25,259.24,0.00,N/A +104.17.46.40,4,3,0.25,259.94,0.00,N/A +104.16.237.232,4,3,0.25,263.49,0.00,N/A +104.16.172.243,4,3,0.25,266.25,0.00,N/A +104.16.244.83,4,3,0.25,269.50,0.00,N/A +104.16.248.22,4,3,0.25,270.58,0.00,N/A +104.19.9.231,4,3,0.25,274.11,0.00,N/A +104.17.26.25,4,3,0.25,277.95,0.00,N/A +104.18.143.96,4,3,0.25,280.35,0.00,N/A +104.16.254.154,4,3,0.25,290.76,0.00,N/A +104.16.128.24,4,3,0.25,295.17,0.00,N/A +104.16.217.215,4,3,0.25,296.90,0.00,N/A +104.16.243.152,4,3,0.25,298.05,0.00,N/A +104.19.74.232,4,3,0.25,301.24,0.00,N/A +104.16.186.132,4,3,0.25,301.86,0.00,N/A +104.19.22.68,4,3,0.25,305.49,0.00,N/A +104.17.25.230,4,3,0.25,305.60,0.00,N/A +162.159.12.250,4,3,0.25,333.08,0.00,N/A +104.16.232.127,4,2,0.50,165.77,0.00,N/A +172.65.211.195,4,2,0.50,166.03,0.00,N/A +104.16.210.63,4,2,0.50,166.28,0.00,N/A +104.18.202.245,4,2,0.50,167.10,0.00,N/A +104.19.32.244,4,2,0.50,168.25,0.00,N/A +104.19.47.130,4,2,0.50,168.65,0.00,N/A +104.19.28.39,4,2,0.50,168.73,0.00,N/A +104.19.62.128,4,2,0.50,169.06,0.00,N/A +104.18.236.37,4,2,0.50,169.51,0.00,N/A +104.16.213.127,4,2,0.50,169.63,0.00,N/A +104.19.5.114,4,2,0.50,169.63,0.00,N/A +172.65.214.236,4,2,0.50,169.80,0.00,N/A +104.19.34.192,4,2,0.50,170.53,0.00,N/A +172.65.217.13,4,2,0.50,170.61,0.00,N/A +104.18.255.70,4,2,0.50,171.30,0.00,N/A +104.18.228.127,4,2,0.50,171.34,0.00,N/A +104.16.196.113,4,2,0.50,171.73,0.00,N/A +104.18.200.77,4,2,0.50,171.82,0.00,N/A +104.19.29.149,4,2,0.50,172.26,0.00,N/A +104.18.246.66,4,2,0.50,172.34,0.00,N/A +104.19.53.109,4,2,0.50,172.43,0.00,N/A +104.17.9.181,4,2,0.50,172.74,0.00,N/A +172.65.208.209,4,2,0.50,173.03,0.00,N/A +104.18.219.64,4,2,0.50,173.10,0.00,N/A +104.19.6.132,4,2,0.50,173.37,0.00,N/A +104.19.66.0,4,2,0.50,173.63,0.00,N/A +172.65.193.184,4,2,0.50,173.81,0.00,N/A +104.19.25.178,4,2,0.50,174.11,0.00,N/A +104.16.230.87,4,2,0.50,174.42,0.00,N/A +104.16.225.15,4,2,0.50,174.97,0.00,N/A +104.17.6.82,4,2,0.50,175.80,0.00,N/A +104.16.168.243,4,2,0.50,176.35,0.00,N/A +162.159.46.227,4,2,0.50,176.41,0.00,N/A +104.18.247.48,4,2,0.50,176.50,0.00,N/A +104.19.19.210,4,2,0.50,177.17,0.00,N/A +172.65.215.191,4,2,0.50,177.38,0.00,N/A +172.65.228.103,4,2,0.50,177.92,0.00,N/A +104.19.54.104,4,2,0.50,178.14,0.00,N/A +172.65.209.80,4,2,0.50,178.23,0.00,N/A +172.64.149.174,4,2,0.50,178.96,0.00,N/A +108.162.194.75,4,2,0.50,179.04,0.00,N/A +104.27.113.13,4,2,0.50,179.52,0.00,N/A +104.18.198.55,4,2,0.50,180.07,0.00,N/A +104.19.60.74,4,2,0.50,180.28,0.00,N/A +104.17.67.158,4,2,0.50,180.45,0.00,N/A +104.19.241.60,4,2,0.50,180.59,0.00,N/A +172.65.131.127,4,2,0.50,180.84,0.00,N/A +104.16.181.177,4,2,0.50,181.33,0.00,N/A +172.67.111.160,4,2,0.50,181.90,0.00,N/A +104.27.89.177,4,2,0.50,182.25,0.00,N/A +104.25.1.205,4,2,0.50,182.37,0.00,N/A +104.27.121.46,4,2,0.50,182.55,0.00,N/A +104.25.156.2,4,2,0.50,182.89,0.00,N/A +172.65.242.204,4,2,0.50,183.62,0.00,N/A +172.65.161.140,4,2,0.50,183.64,0.00,N/A +104.27.94.162,4,2,0.50,184.37,0.00,N/A +104.25.145.241,4,2,0.50,184.61,0.00,N/A +104.25.167.30,4,2,0.50,185.29,0.00,N/A +104.25.61.52,4,2,0.50,185.51,0.00,N/A +104.16.236.16,4,2,0.50,186.96,0.00,N/A +172.65.210.45,4,2,0.50,186.99,0.00,N/A +104.16.204.13,4,2,0.50,187.11,0.00,N/A +172.64.146.66,4,2,0.50,187.42,0.00,N/A +172.65.134.158,4,2,0.50,187.58,0.00,N/A +172.65.226.202,4,2,0.50,187.86,0.00,N/A +104.25.103.134,4,2,0.50,189.11,0.00,N/A +172.65.190.155,4,2,0.50,189.11,0.00,N/A +104.27.73.8,4,2,0.50,189.20,0.00,N/A +172.65.92.58,4,2,0.50,189.23,0.00,N/A +104.25.125.82,4,2,0.50,189.24,0.00,N/A +104.27.124.170,4,2,0.50,189.25,0.00,N/A +104.27.114.18,4,2,0.50,189.58,0.00,N/A +104.25.204.1,4,2,0.50,189.60,0.00,N/A +104.25.147.35,4,2,0.50,189.62,0.00,N/A +104.25.30.42,4,2,0.50,189.70,0.00,N/A +104.27.53.231,4,2,0.50,189.94,0.00,N/A +104.23.112.114,4,2,0.50,190.05,0.00,N/A +104.25.150.43,4,2,0.50,190.41,0.00,N/A +104.17.28.28,4,2,0.50,190.48,0.00,N/A +104.25.163.122,4,2,0.50,190.63,0.00,N/A +104.25.92.129,4,2,0.50,190.84,0.00,N/A +104.16.174.195,4,2,0.50,191.23,0.00,N/A +172.65.180.91,4,2,0.50,191.24,0.00,N/A +172.65.191.61,4,2,0.50,191.38,0.00,N/A +104.25.129.229,4,2,0.50,191.48,0.00,N/A +172.65.140.27,4,2,0.50,191.66,0.00,N/A +104.27.59.198,4,2,0.50,191.79,0.00,N/A +104.27.86.147,4,2,0.50,191.95,0.00,N/A +104.27.123.120,4,2,0.50,192.21,0.00,N/A +162.159.51.244,4,2,0.50,192.76,0.00,N/A +104.16.197.186,4,2,0.50,193.16,0.00,N/A +104.25.79.107,4,2,0.50,193.32,0.00,N/A +172.65.130.170,4,2,0.50,194.25,0.00,N/A +104.27.72.254,4,2,0.50,194.69,0.00,N/A +172.65.189.137,4,2,0.50,195.25,0.00,N/A +104.25.114.125,4,2,0.50,195.28,0.00,N/A +104.25.153.60,4,2,0.50,195.49,0.00,N/A +104.27.95.5,4,2,0.50,196.06,0.00,N/A +172.65.82.218,4,2,0.50,196.10,0.00,N/A +104.16.202.120,4,2,0.50,196.33,0.00,N/A +104.27.54.21,4,2,0.50,196.33,0.00,N/A +172.65.127.66,4,2,0.50,196.40,0.00,N/A +172.66.145.171,4,2,0.50,196.63,0.00,N/A +104.27.112.128,4,2,0.50,196.68,0.00,N/A +104.25.107.63,4,2,0.50,197.10,0.00,N/A +104.23.115.58,4,2,0.50,197.14,0.00,N/A +172.67.147.131,4,2,0.50,197.27,0.00,N/A +104.27.65.58,4,2,0.50,197.46,0.00,N/A +104.25.38.160,4,2,0.50,197.66,0.00,N/A +104.16.235.17,4,2,0.50,197.72,0.00,N/A +104.25.166.221,4,2,0.50,197.78,0.00,N/A +104.25.39.109,4,2,0.50,197.83,0.00,N/A +104.25.21.254,4,2,0.50,198.45,0.00,N/A +172.65.172.134,4,2,0.50,198.48,0.00,N/A +104.25.102.240,4,2,0.50,198.58,0.00,N/A +104.25.202.146,4,2,0.50,198.61,0.00,N/A +104.25.128.235,4,2,0.50,198.76,0.00,N/A +104.25.19.188,4,2,0.50,199.01,0.00,N/A +104.25.91.90,4,2,0.50,199.07,0.00,N/A +104.16.206.246,4,2,0.50,199.23,0.00,N/A +172.65.168.169,4,2,0.50,199.43,0.00,N/A +172.65.139.219,4,2,0.50,200.38,0.00,N/A +104.27.127.17,4,2,0.50,200.40,0.00,N/A +172.67.114.185,4,2,0.50,201.63,0.00,N/A +104.24.245.37,4,2,0.50,201.73,0.00,N/A +104.18.233.82,4,2,0.50,201.99,0.00,N/A +104.23.134.70,4,2,0.50,202.04,0.00,N/A +104.27.45.21,4,2,0.50,202.21,0.00,N/A +104.25.76.224,4,2,0.50,202.43,0.00,N/A +104.25.69.222,4,2,0.50,202.52,0.00,N/A +172.65.244.105,4,2,0.50,202.69,0.00,N/A +172.66.196.154,4,2,0.50,202.74,0.00,N/A +172.65.167.167,4,2,0.50,202.94,0.00,N/A +104.25.117.55,4,2,0.50,203.07,0.00,N/A +104.25.132.250,4,2,0.50,203.24,0.00,N/A +104.19.23.120,4,2,0.50,203.25,0.00,N/A +172.67.193.52,4,2,0.50,203.42,0.00,N/A +104.16.231.173,4,2,0.50,203.75,0.00,N/A +104.25.111.179,4,2,0.50,203.81,0.00,N/A +172.65.145.26,4,2,0.50,203.95,0.00,N/A +172.67.122.116,4,2,0.50,204.07,0.00,N/A +104.27.87.198,4,2,0.50,204.25,0.00,N/A +104.16.250.187,4,2,0.50,204.32,0.00,N/A +172.65.192.175,4,2,0.50,204.45,0.00,N/A +172.65.178.57,4,2,0.50,204.62,0.00,N/A +104.27.43.169,4,2,0.50,204.75,0.00,N/A +172.67.112.161,4,2,0.50,204.87,0.00,N/A +162.159.42.97,4,2,0.50,205.12,0.00,N/A +172.67.238.4,4,2,0.50,205.90,0.00,N/A +172.67.243.192,4,2,0.50,205.92,0.00,N/A +104.23.98.14,4,2,0.50,205.99,0.00,N/A +104.27.71.131,4,2,0.50,206.35,0.00,N/A +104.25.182.99,4,2,0.50,206.55,0.00,N/A +104.17.15.149,4,2,0.50,206.97,0.00,N/A +104.25.89.4,4,2,0.50,207.06,0.00,N/A +104.27.88.10,4,2,0.50,207.06,0.00,N/A +104.25.78.37,4,2,0.50,208.03,0.00,N/A +104.23.101.18,4,2,0.50,208.19,0.00,N/A +104.25.162.191,4,2,0.50,208.30,0.00,N/A +104.16.209.136,4,2,0.50,208.41,0.00,N/A +104.27.46.19,4,2,0.50,209.08,0.00,N/A +104.25.148.28,4,2,0.50,209.50,0.00,N/A +104.25.72.41,4,2,0.50,209.60,0.00,N/A +104.27.69.216,4,2,0.50,209.81,0.00,N/A +172.65.124.65,4,2,0.50,209.92,0.00,N/A +104.25.123.30,4,2,0.50,210.09,0.00,N/A +104.25.143.37,4,2,0.50,210.44,0.00,N/A +104.27.74.218,4,2,0.50,210.49,0.00,N/A +104.25.71.180,4,2,0.50,211.95,0.00,N/A +104.25.110.235,4,2,0.50,212.26,0.00,N/A +172.67.230.47,4,2,0.50,212.26,0.00,N/A +104.25.106.176,4,2,0.50,212.27,0.00,N/A +104.25.119.239,4,2,0.50,212.62,0.00,N/A +104.25.18.74,4,2,0.50,213.15,0.00,N/A +104.24.254.26,4,2,0.50,213.23,0.00,N/A +172.67.226.119,4,2,0.50,213.24,0.00,N/A +172.67.179.228,4,2,0.50,213.75,0.00,N/A +104.25.159.108,4,2,0.50,214.22,0.00,N/A +172.67.117.58,4,2,0.50,214.58,0.00,N/A +104.27.56.251,4,2,0.50,214.91,0.00,N/A +172.67.224.75,4,2,0.50,215.10,0.00,N/A +172.65.129.88,4,2,0.50,215.35,0.00,N/A +172.67.249.143,4,2,0.50,215.54,0.00,N/A +172.67.43.168,4,2,0.50,216.97,0.00,N/A +172.67.227.148,4,2,0.50,217.04,0.00,N/A +172.67.36.28,4,2,0.50,218.39,0.00,N/A +104.18.254.179,4,2,0.50,218.56,0.00,N/A +172.67.144.231,4,2,0.50,219.65,0.00,N/A +104.21.100.208,4,2,0.50,219.97,0.00,N/A +104.25.70.253,4,2,0.50,220.19,0.00,N/A +104.21.94.224,4,2,0.50,220.34,0.00,N/A +104.21.89.3,4,2,0.50,220.83,0.00,N/A +172.67.229.25,4,2,0.50,222.55,0.00,N/A +104.21.68.236,4,2,0.50,222.66,0.00,N/A +172.67.233.144,4,2,0.50,222.73,0.00,N/A +104.21.62.90,4,2,0.50,223.24,0.00,N/A +172.67.245.123,4,2,0.50,223.47,0.00,N/A +104.21.36.9,4,2,0.50,223.48,0.00,N/A +198.41.214.181,4,2,0.50,223.61,0.00,N/A +172.67.196.9,4,2,0.50,223.67,0.00,N/A +172.67.171.87,4,2,0.50,224.33,0.00,N/A +104.21.73.62,4,2,0.50,224.38,0.00,N/A +104.18.214.143,4,2,0.50,225.16,0.00,N/A +104.19.16.151,4,2,0.50,225.38,0.00,N/A +172.67.133.180,4,2,0.50,225.94,0.00,N/A +104.21.45.36,4,2,0.50,226.22,0.00,N/A +104.17.11.9,4,2,0.50,227.71,0.00,N/A +172.67.150.56,4,2,0.50,227.74,0.00,N/A +104.21.22.214,4,2,0.50,227.81,0.00,N/A +104.21.79.9,4,2,0.50,228.13,0.00,N/A +104.17.12.216,4,2,0.50,228.19,0.00,N/A +104.21.51.135,4,2,0.50,228.24,0.00,N/A +104.21.35.76,4,2,0.50,228.26,0.00,N/A +104.21.47.241,4,2,0.50,228.45,0.00,N/A +172.67.236.31,4,2,0.50,228.80,0.00,N/A +172.67.148.83,4,2,0.50,229.02,0.00,N/A +104.16.227.253,4,2,0.50,229.10,0.00,N/A +172.67.239.243,4,2,0.50,229.11,0.00,N/A +172.67.184.224,4,2,0.50,229.43,0.00,N/A +172.67.200.95,4,2,0.50,230.74,0.00,N/A +104.21.117.130,4,2,0.50,231.99,0.00,N/A +104.21.38.25,4,2,0.50,232.07,0.00,N/A +172.67.235.165,4,2,0.50,232.51,0.00,N/A +172.67.191.89,4,2,0.50,232.92,0.00,N/A +104.21.78.31,4,2,0.50,233.48,0.00,N/A +172.64.235.13,4,2,0.50,233.52,0.00,N/A +104.17.5.111,4,2,0.50,234.38,0.00,N/A +172.67.163.174,4,2,0.50,234.60,0.00,N/A +172.65.23.28,4,2,0.50,235.39,0.00,N/A +104.21.77.247,4,2,0.50,235.64,0.00,N/A +104.21.95.112,4,2,0.50,235.92,0.00,N/A +172.67.234.190,4,2,0.50,236.33,0.00,N/A +104.21.80.214,4,2,0.50,236.43,0.00,N/A +172.67.187.174,4,2,0.50,236.78,0.00,N/A +104.21.72.115,4,2,0.50,236.98,0.00,N/A +104.16.249.247,4,2,0.50,237.12,0.00,N/A +172.67.222.130,4,2,0.50,237.44,0.00,N/A +172.67.232.134,4,2,0.50,237.45,0.00,N/A +172.67.186.23,4,2,0.50,237.52,0.00,N/A +172.67.146.102,4,2,0.50,237.54,0.00,N/A +104.21.71.65,4,2,0.50,238.21,0.00,N/A +172.67.240.219,4,2,0.50,238.31,0.00,N/A +104.19.31.159,4,2,0.50,238.99,0.00,N/A +172.67.209.179,4,2,0.50,239.14,0.00,N/A +104.18.203.126,4,2,0.50,240.80,0.00,N/A +172.67.225.16,4,2,0.50,241.65,0.00,N/A +104.19.20.215,4,2,0.50,241.67,0.00,N/A +104.18.199.178,4,2,0.50,241.70,0.00,N/A +104.21.93.155,4,2,0.50,242.14,0.00,N/A +104.21.105.247,4,2,0.50,242.39,0.00,N/A +172.67.237.203,4,2,0.50,243.20,0.00,N/A +104.21.67.19,4,2,0.50,244.70,0.00,N/A +172.67.129.156,4,2,0.50,245.99,0.00,N/A +172.67.242.188,4,2,0.50,246.07,0.00,N/A +172.67.201.160,4,2,0.50,246.09,0.00,N/A +172.67.228.250,4,2,0.50,246.47,0.00,N/A +172.67.195.214,4,2,0.50,247.36,0.00,N/A +104.17.23.35,4,2,0.50,250.13,0.00,N/A +104.16.252.241,4,2,0.50,251.28,0.00,N/A +104.18.249.186,4,2,0.50,252.02,0.00,N/A +172.67.248.187,4,2,0.50,252.07,0.00,N/A +172.67.131.46,4,2,0.50,252.28,0.00,N/A +104.17.0.9,4,2,0.50,252.95,0.00,N/A +172.67.204.103,4,2,0.50,254.53,0.00,N/A +104.16.179.117,4,2,0.50,254.91,0.00,N/A +172.65.12.220,4,2,0.50,256.55,0.00,N/A +172.67.176.156,4,2,0.50,256.81,0.00,N/A +172.65.231.8,4,2,0.50,258.67,0.00,N/A +104.16.203.254,4,2,0.50,262.04,0.00,N/A +104.19.41.46,4,2,0.50,265.39,0.00,N/A +104.16.221.213,4,2,0.50,268.02,0.00,N/A +104.19.97.228,4,2,0.50,269.77,0.00,N/A +104.19.38.43,4,2,0.50,275.62,0.00,N/A +104.16.205.95,4,2,0.50,282.59,0.00,N/A +162.159.0.137,4,2,0.50,283.38,0.00,N/A +162.159.32.3,4,2,0.50,291.78,0.00,N/A +162.159.36.48,4,2,0.50,296.85,0.00,N/A +104.16.182.109,4,2,0.50,308.08,0.00,N/A +104.17.1.92,4,2,0.50,315.93,0.00,N/A +104.19.72.245,4,2,0.50,319.16,0.00,N/A +104.16.229.45,4,2,0.50,321.22,0.00,N/A +190.93.245.152,4,2,0.50,336.90,0.00,N/A +104.17.7.145,4,2,0.50,349.57,0.00,N/A +104.18.213.14,4,1,0.75,163.67,0.00,N/A +104.19.30.199,4,1,0.75,164.04,0.00,N/A +104.18.252.219,4,1,0.75,164.51,0.00,N/A +104.18.237.110,4,1,0.75,165.68,0.00,N/A +104.16.188.172,4,1,0.75,166.77,0.00,N/A +104.18.217.241,4,1,0.75,167.06,0.00,N/A +172.65.246.132,4,1,0.75,168.13,0.00,N/A +104.19.65.194,4,1,0.75,168.13,0.00,N/A +104.16.214.194,4,1,0.75,168.14,0.00,N/A +104.16.199.146,4,1,0.75,169.13,0.00,N/A +104.16.187.85,4,1,0.75,169.21,0.00,N/A +104.18.201.59,4,1,0.75,169.94,0.00,N/A +104.16.200.21,4,1,0.75,170.40,0.00,N/A +104.19.7.172,4,1,0.75,171.16,0.00,N/A +104.16.233.87,4,1,0.75,171.19,0.00,N/A +104.18.241.152,4,1,0.75,171.42,0.00,N/A +104.18.212.123,4,1,0.75,171.42,0.00,N/A +104.16.239.52,4,1,0.75,171.47,0.00,N/A +104.18.244.113,4,1,0.75,171.79,0.00,N/A +172.65.227.240,4,1,0.75,171.86,0.00,N/A +172.65.251.201,4,1,0.75,172.89,0.00,N/A +104.17.75.226,4,1,0.75,173.25,0.00,N/A +104.18.223.101,4,1,0.75,173.25,0.00,N/A +104.18.229.11,4,1,0.75,173.40,0.00,N/A +104.19.37.75,4,1,0.75,173.70,0.00,N/A +104.16.177.246,4,1,0.75,174.08,0.00,N/A +104.18.218.96,4,1,0.75,174.15,0.00,N/A +104.16.240.184,4,1,0.75,174.20,0.00,N/A +104.16.241.21,4,1,0.75,174.25,0.00,N/A +104.21.237.45,4,1,0.75,176.08,0.00,N/A +104.17.30.19,4,1,0.75,176.25,0.00,N/A +104.18.225.76,4,1,0.75,176.46,0.00,N/A +172.65.229.96,4,1,0.75,176.57,0.00,N/A +104.19.26.118,4,1,0.75,176.57,0.00,N/A +104.17.45.234,4,1,0.75,176.78,0.00,N/A +172.65.219.91,4,1,0.75,176.96,0.00,N/A +198.41.223.198,4,1,0.75,177.06,0.00,N/A +172.65.222.206,4,1,0.75,178.64,0.00,N/A +190.93.246.193,4,1,0.75,179.66,0.00,N/A +104.25.75.19,4,1,0.75,179.71,0.00,N/A +104.18.226.224,4,1,0.75,180.63,0.00,N/A +108.162.193.248,4,1,0.75,181.13,0.00,N/A +104.25.31.119,4,1,0.75,181.94,0.00,N/A +172.67.110.213,4,1,0.75,182.15,0.00,N/A +162.159.41.63,4,1,0.75,182.80,0.00,N/A +104.25.8.235,4,1,0.75,182.84,0.00,N/A +104.25.105.177,4,1,0.75,182.97,0.00,N/A +104.25.29.162,4,1,0.75,183.02,0.00,N/A +172.67.118.15,4,1,0.75,183.07,0.00,N/A +172.65.174.192,4,1,0.75,183.14,0.00,N/A +104.25.50.20,4,1,0.75,183.59,0.00,N/A +104.21.239.56,4,1,0.75,183.60,0.00,N/A +172.65.159.113,4,1,0.75,183.75,0.00,N/A +104.25.41.41,4,1,0.75,183.86,0.00,N/A +104.25.168.83,4,1,0.75,184.24,0.00,N/A +104.23.123.173,4,1,0.75,184.85,0.00,N/A +104.23.116.70,4,1,0.75,184.85,0.00,N/A +104.25.170.252,4,1,0.75,184.94,0.00,N/A +104.27.42.251,4,1,0.75,185.19,0.00,N/A +172.65.234.41,4,1,0.75,185.35,0.00,N/A +104.16.7.148,4,1,0.75,185.90,0.00,N/A +162.159.128.78,4,1,0.75,185.91,0.00,N/A +104.25.85.217,4,1,0.75,186.13,0.00,N/A +104.25.23.29,4,1,0.75,186.50,0.00,N/A +104.23.141.211,4,1,0.75,186.78,0.00,N/A +104.27.76.127,4,1,0.75,187.87,0.00,N/A +104.16.157.22,4,1,0.75,188.21,0.00,N/A +104.25.178.192,4,1,0.75,189.04,0.00,N/A +104.17.241.221,4,1,0.75,190.77,0.00,N/A +104.19.96.75,4,1,0.75,190.80,0.00,N/A +162.159.245.6,4,1,0.75,191.48,0.00,N/A +104.23.129.59,4,1,0.75,192.33,0.00,N/A +162.159.243.54,4,1,0.75,192.36,0.00,N/A +104.27.92.55,4,1,0.75,192.62,0.00,N/A +104.25.65.123,4,1,0.75,192.95,0.00,N/A +104.25.157.69,4,1,0.75,193.41,0.00,N/A +172.65.158.237,4,1,0.75,194.02,0.00,N/A +104.19.2.132,4,1,0.75,194.04,0.00,N/A +104.23.114.211,4,1,0.75,194.68,0.00,N/A +104.27.52.65,4,1,0.75,194.82,0.00,N/A +104.25.116.110,4,1,0.75,194.86,0.00,N/A +172.65.179.128,4,1,0.75,194.91,0.00,N/A +104.25.124.114,4,1,0.75,195.15,0.00,N/A +104.25.0.192,4,1,0.75,195.22,0.00,N/A +104.25.86.81,4,1,0.75,195.43,0.00,N/A +104.25.144.144,4,1,0.75,195.68,0.00,N/A +104.25.10.30,4,1,0.75,195.76,0.00,N/A +172.65.173.33,4,1,0.75,195.82,0.00,N/A +104.25.173.172,4,1,0.75,196.16,0.00,N/A +104.25.73.32,4,1,0.75,196.44,0.00,N/A +104.27.75.93,4,1,0.75,196.77,0.00,N/A +104.27.91.117,4,1,0.75,197.12,0.00,N/A +104.25.140.229,4,1,0.75,197.32,0.00,N/A +172.65.185.64,4,1,0.75,197.33,0.00,N/A +104.25.66.201,4,1,0.75,197.39,0.00,N/A +172.65.142.109,4,1,0.75,197.51,0.00,N/A +172.65.175.0,4,1,0.75,197.54,0.00,N/A +172.65.164.223,4,1,0.75,197.54,0.00,N/A +104.16.102.58,4,1,0.75,197.81,0.00,N/A +172.65.133.234,4,1,0.75,197.83,0.00,N/A +104.25.133.24,4,1,0.75,197.83,0.00,N/A +104.25.81.166,4,1,0.75,197.91,0.00,N/A +172.67.134.70,4,1,0.75,198.12,0.00,N/A +104.25.127.129,4,1,0.75,198.41,0.00,N/A +104.27.68.180,4,1,0.75,198.52,0.00,N/A +104.25.113.193,4,1,0.75,198.67,0.00,N/A +104.27.61.7,4,1,0.75,198.77,0.00,N/A +172.65.156.55,4,1,0.75,198.83,0.00,N/A +172.65.132.228,4,1,0.75,199.10,0.00,N/A +104.23.106.123,4,1,0.75,199.24,0.00,N/A +172.67.172.253,4,1,0.75,199.41,0.00,N/A +172.67.113.106,4,1,0.75,199.41,0.00,N/A +104.18.151.71,4,1,0.75,199.45,0.00,N/A +104.25.44.230,4,1,0.75,199.53,0.00,N/A +172.65.183.239,4,1,0.75,199.54,0.00,N/A +104.21.16.108,4,1,0.75,200.13,0.00,N/A +172.65.188.8,4,1,0.75,200.15,0.00,N/A +172.65.160.83,4,1,0.75,200.30,0.00,N/A +104.27.78.3,4,1,0.75,200.46,0.00,N/A +104.27.80.42,4,1,0.75,200.69,0.00,N/A +104.27.82.19,4,1,0.75,200.93,0.00,N/A +104.18.150.215,4,1,0.75,201.21,0.00,N/A +104.25.82.130,4,1,0.75,201.26,0.00,N/A +172.67.132.213,4,1,0.75,201.42,0.00,N/A +104.25.165.110,4,1,0.75,203.09,0.00,N/A +172.67.116.7,4,1,0.75,204.09,0.00,N/A +172.67.79.153,4,1,0.75,205.89,0.00,N/A +104.25.177.29,4,1,0.75,206.47,0.00,N/A +104.23.127.84,4,1,0.75,206.79,0.00,N/A +172.65.157.26,4,1,0.75,206.81,0.00,N/A +172.65.143.100,4,1,0.75,207.04,0.00,N/A +172.67.213.100,4,1,0.75,207.71,0.00,N/A +172.67.120.9,4,1,0.75,207.72,0.00,N/A +172.67.153.129,4,1,0.75,208.63,0.00,N/A +172.67.125.200,4,1,0.75,208.65,0.00,N/A +104.25.149.106,4,1,0.75,208.83,0.00,N/A +162.159.34.226,4,1,0.75,209.01,0.00,N/A +104.21.104.184,4,1,0.75,209.12,0.00,N/A +172.67.212.137,4,1,0.75,209.67,0.00,N/A +172.67.123.136,4,1,0.75,209.89,0.00,N/A +104.21.0.77,4,1,0.75,210.27,0.00,N/A +172.65.170.217,4,1,0.75,210.29,0.00,N/A +104.27.35.43,4,1,0.75,210.34,0.00,N/A +104.27.79.195,4,1,0.75,210.51,0.00,N/A +104.27.63.69,4,1,0.75,210.53,0.00,N/A +104.25.26.185,4,1,0.75,210.61,0.00,N/A +104.25.42.149,4,1,0.75,210.70,0.00,N/A +198.41.196.53,4,1,0.75,210.92,0.00,N/A +172.65.202.128,4,1,0.75,211.10,0.00,N/A +104.17.17.118,4,1,0.75,211.16,0.00,N/A +104.25.126.214,4,1,0.75,211.18,0.00,N/A +172.65.123.189,4,1,0.75,211.24,0.00,N/A +104.27.60.48,4,1,0.75,211.46,0.00,N/A +172.65.186.1,4,1,0.75,211.52,0.00,N/A +172.65.86.79,4,1,0.75,211.57,0.00,N/A +104.21.10.53,4,1,0.75,211.76,0.00,N/A +172.65.184.152,4,1,0.75,211.78,0.00,N/A +104.27.70.234,4,1,0.75,211.98,0.00,N/A +172.67.168.42,4,1,0.75,212.00,0.00,N/A +104.21.99.173,4,1,0.75,212.05,0.00,N/A +172.65.181.172,4,1,0.75,212.07,0.00,N/A +172.67.203.54,4,1,0.75,212.08,0.00,N/A +104.27.50.143,4,1,0.75,212.10,0.00,N/A +104.21.53.192,4,1,0.75,212.10,0.00,N/A +172.67.206.8,4,1,0.75,212.26,0.00,N/A +104.27.77.89,4,1,0.75,212.28,0.00,N/A +104.25.151.104,4,1,0.75,212.32,0.00,N/A +104.21.12.16,4,1,0.75,212.68,0.00,N/A +104.21.17.104,4,1,0.75,212.70,0.00,N/A +172.65.187.74,4,1,0.75,212.74,0.00,N/A +104.25.33.231,4,1,0.75,212.80,0.00,N/A +104.25.141.117,4,1,0.75,213.00,0.00,N/A +104.27.67.150,4,1,0.75,213.13,0.00,N/A +172.65.144.178,4,1,0.75,213.20,0.00,N/A +104.21.52.226,4,1,0.75,213.68,0.00,N/A +172.65.177.1,4,1,0.75,213.78,0.00,N/A +104.27.58.213,4,1,0.75,214.07,0.00,N/A +172.67.178.33,4,1,0.75,214.39,0.00,N/A +172.67.127.189,4,1,0.75,214.40,0.00,N/A +172.67.167.87,4,1,0.75,214.48,0.00,N/A +104.21.96.216,4,1,0.75,214.66,0.00,N/A +104.21.103.195,4,1,0.75,214.83,0.00,N/A +104.23.122.11,4,1,0.75,214.85,0.00,N/A +104.21.87.122,4,1,0.75,214.92,0.00,N/A +172.67.185.245,4,1,0.75,214.92,0.00,N/A +172.65.91.153,4,1,0.75,215.02,0.00,N/A +104.27.97.215,4,1,0.75,215.05,0.00,N/A +104.25.142.8,4,1,0.75,215.25,0.00,N/A +104.23.132.217,4,1,0.75,215.33,0.00,N/A +172.67.145.128,4,1,0.75,215.37,0.00,N/A +172.67.210.240,4,1,0.75,215.43,0.00,N/A +172.67.182.141,4,1,0.75,215.57,0.00,N/A +172.67.170.100,4,1,0.75,215.85,0.00,N/A +172.67.215.56,4,1,0.75,216.24,0.00,N/A +172.65.176.224,4,1,0.75,216.76,0.00,N/A +172.67.151.13,4,1,0.75,217.05,0.00,N/A +104.21.198.22,4,1,0.75,217.27,0.00,N/A +104.21.50.44,4,1,0.75,217.36,0.00,N/A +104.25.32.177,4,1,0.75,217.38,0.00,N/A +104.19.35.200,4,1,0.75,217.44,0.00,N/A +104.21.115.123,4,1,0.75,217.72,0.00,N/A +104.21.20.237,4,1,0.75,218.20,0.00,N/A +172.67.173.238,4,1,0.75,218.40,0.00,N/A +104.25.231.57,4,1,0.75,218.45,0.00,N/A +104.21.106.152,4,1,0.75,218.58,0.00,N/A +104.27.126.205,4,1,0.75,218.88,0.00,N/A +172.67.124.177,4,1,0.75,219.90,0.00,N/A +172.66.164.0,4,1,0.75,220.22,0.00,N/A +104.21.121.201,4,1,0.75,221.67,0.00,N/A +104.19.45.187,4,1,0.75,221.99,0.00,N/A +104.21.6.226,4,1,0.75,222.78,0.00,N/A +172.67.188.195,4,1,0.75,224.44,0.00,N/A +104.21.59.151,4,1,0.75,224.92,0.00,N/A +104.21.111.210,4,1,0.75,225.97,0.00,N/A +172.65.221.120,4,1,0.75,226.62,0.00,N/A +172.65.93.123,4,1,0.75,226.94,0.00,N/A +104.21.82.137,4,1,0.75,227.02,0.00,N/A +104.21.75.155,4,1,0.75,227.19,0.00,N/A +104.21.4.103,4,1,0.75,227.65,0.00,N/A +172.65.218.115,4,1,0.75,228.33,0.00,N/A +172.67.183.190,4,1,0.75,228.38,0.00,N/A +172.65.225.135,4,1,0.75,228.57,0.00,N/A +104.21.18.142,4,1,0.75,228.62,0.00,N/A +172.67.216.164,4,1,0.75,229.22,0.00,N/A +104.21.41.229,4,1,0.75,229.28,0.00,N/A +104.21.86.97,4,1,0.75,229.30,0.00,N/A +104.21.54.37,4,1,0.75,229.37,0.00,N/A +172.67.190.171,4,1,0.75,229.48,0.00,N/A +172.67.194.139,4,1,0.75,229.59,0.00,N/A +104.21.40.93,4,1,0.75,229.68,0.00,N/A +172.67.180.110,4,1,0.75,229.94,0.00,N/A +172.67.166.204,4,1,0.75,230.28,0.00,N/A +104.21.74.128,4,1,0.75,230.60,0.00,N/A +172.67.214.107,4,1,0.75,230.77,0.00,N/A +172.67.175.89,4,1,0.75,231.01,0.00,N/A +104.16.175.228,4,1,0.75,231.75,0.00,N/A +104.21.48.8,4,1,0.75,232.07,0.00,N/A +104.21.37.104,4,1,0.75,232.16,0.00,N/A +104.21.8.192,4,1,0.75,233.78,0.00,N/A +172.67.202.238,4,1,0.75,235.09,0.00,N/A +172.67.205.247,4,1,0.75,235.42,0.00,N/A +172.65.223.201,4,1,0.75,236.56,0.00,N/A +104.21.193.204,4,1,0.75,236.93,0.00,N/A +104.21.102.58,4,1,0.75,236.95,0.00,N/A +104.18.231.140,4,1,0.75,238.04,0.00,N/A +172.67.154.197,4,1,0.75,240.34,0.00,N/A +104.16.180.108,4,1,0.75,242.44,0.00,N/A +104.21.116.58,4,1,0.75,243.03,0.00,N/A +172.67.136.221,4,1,0.75,243.68,0.00,N/A +104.21.13.66,4,1,0.75,244.41,0.00,N/A +104.21.92.240,4,1,0.75,244.88,0.00,N/A +172.67.157.171,4,1,0.75,245.27,0.00,N/A +104.21.101.247,4,1,0.75,246.05,0.00,N/A +172.67.181.79,4,1,0.75,246.05,0.00,N/A +104.21.118.139,4,1,0.75,246.36,0.00,N/A +172.67.177.56,4,1,0.75,246.65,0.00,N/A +104.21.127.178,4,1,0.75,246.81,0.00,N/A +104.21.98.191,4,1,0.75,247.32,0.00,N/A +104.21.113.193,4,1,0.75,247.69,0.00,N/A +172.67.189.86,4,1,0.75,248.51,0.00,N/A +104.21.2.249,4,1,0.75,248.68,0.00,N/A +104.21.222.98,4,1,0.75,248.68,0.00,N/A +104.18.242.144,4,1,0.75,250.21,0.00,N/A +104.21.126.207,4,1,0.75,251.52,0.00,N/A +104.16.242.230,4,1,0.75,253.13,0.00,N/A +172.67.211.106,4,1,0.75,253.30,0.00,N/A +104.21.114.153,4,1,0.75,256.73,0.00,N/A +104.21.97.15,4,1,0.75,258.68,0.00,N/A +172.67.174.227,4,1,0.75,259.79,0.00,N/A +172.67.198.73,4,1,0.75,259.83,0.00,N/A +104.19.3.200,4,1,0.75,260.44,0.00,N/A +104.19.63.44,4,1,0.75,262.87,0.00,N/A +172.65.32.207,4,1,0.75,263.57,0.00,N/A +104.18.216.81,4,1,0.75,264.24,0.00,N/A +104.21.1.68,4,1,0.75,265.42,0.00,N/A +104.21.81.31,4,1,0.75,265.49,0.00,N/A +104.18.251.232,4,1,0.75,266.91,0.00,N/A +172.67.197.65,4,1,0.75,267.28,0.00,N/A +104.18.156.241,4,1,0.75,269.81,0.00,N/A +172.65.24.244,4,1,0.75,271.18,0.00,N/A +104.16.253.250,4,1,0.75,271.66,0.00,N/A +104.17.4.252,4,1,0.75,273.42,0.00,N/A +104.18.215.115,4,1,0.75,278.16,0.00,N/A +104.16.212.149,4,1,0.75,287.06,0.00,N/A +104.17.13.0,4,1,0.75,290.13,0.00,N/A +141.101.114.149,4,1,0.75,303.88,0.00,N/A +104.18.230.149,4,1,0.75,333.73,0.00,N/A +104.18.248.2,4,1,0.75,346.67,0.00,N/A +104.18.71.193,4,1,0.75,358.68,0.00,N/A +104.17.2.17,4,1,0.75,386.55,0.00,N/A +104.17.216.244,4,1,0.75,398.34,0.00,N/A diff --git a/source/aria2c.exe b/source/bin/aria2c.exe similarity index 100% rename from source/aria2c.exe rename to source/bin/aria2c.exe diff --git a/source/cfst.exe b/source/bin/cfst.exe similarity index 100% rename from source/cfst.exe rename to source/bin/cfst.exe diff --git a/source/bin/result.csv b/source/bin/result.csv new file mode 100644 index 0000000..2f31890 --- /dev/null +++ b/source/bin/result.csv @@ -0,0 +1,11 @@ +IP 地址,已发送,已接收,丢包率,平均延迟,下载速度(MB/s),地区码 +104.21.237.213,4,4,0.00,173.18,86.14,LAX +141.101.120.105,4,4,0.00,173.02,40.72,SJC +162.159.251.244,4,4,0.00,171.02,8.59,LAX +104.18.19.226,4,4,0.00,172.26,5.27,LAX +104.19.32.102,4,4,0.00,173.01,4.52,SJC +141.101.121.113,4,4,0.00,169.51,3.69,SJC +162.159.207.149,4,4,0.00,169.94,3.24,SJC +104.19.21.114,4,4,0.00,171.00,0.38,SJC +104.19.127.210,4,4,0.00,170.53,0.22,SJC +104.19.60.94,4,4,0.00,168.33,0.00,N/A diff --git a/source/core/__init__.py b/source/core/__init__.py new file mode 100644 index 0000000..559866b --- /dev/null +++ b/source/core/__init__.py @@ -0,0 +1,11 @@ +from .animations import MultiStageAnimations +from .ui_manager import UIManager +from .download_manager import DownloadManager +from .debug_manager import DebugManager + +__all__ = [ + 'MultiStageAnimations', + 'UIManager', + 'DownloadManager', + 'DebugManager' +] \ No newline at end of file diff --git a/source/animations.py b/source/core/animations.py similarity index 100% rename from source/animations.py rename to source/core/animations.py diff --git a/source/core/debug_manager.py b/source/core/debug_manager.py new file mode 100644 index 0000000..5e5f82e --- /dev/null +++ b/source/core/debug_manager.py @@ -0,0 +1,57 @@ +import os +import sys +from PySide6 import QtWidgets +from data.config import LOG_FILE +from utils import Logger + +class DebugManager: + def __init__(self, main_window): + """初始化调试管理器 + + Args: + main_window: 主窗口实例 + """ + self.main_window = main_window + self.logger = None + self.original_stdout = None + self.original_stderr = None + + def toggle_debug_mode(self, checked): + """切换调试模式 + + Args: + checked: 是否启用调试模式 + """ + self.main_window.config["debug_mode"] = checked + self.main_window.save_config(self.main_window.config) + if checked: + self.start_logging() + else: + self.stop_logging() + + def start_logging(self): + """启动日志记录""" + if self.logger is None: + try: + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) + # 保存原始的 stdout 和 stderr + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + # 创建 Logger 实例 + self.logger = Logger(LOG_FILE, self.original_stdout) + sys.stdout = self.logger + sys.stderr = self.logger + print("--- Debug mode enabled ---") + except (IOError, OSError) as e: + QtWidgets.QMessageBox.critical(self.main_window, "错误", f"无法创建日志文件: {e}") + self.logger = None + + def stop_logging(self): + """停止日志记录""" + if self.logger: + print("--- Debug mode disabled ---") + sys.stdout = self.original_stdout + sys.stderr = self.original_stderr + self.logger.close() + self.logger = None \ No newline at end of file diff --git a/source/core/download_manager.py b/source/core/download_manager.py new file mode 100644 index 0000000..aff68f7 --- /dev/null +++ b/source/core/download_manager.py @@ -0,0 +1,493 @@ +import os +import requests +import json +from collections import deque +from urllib.parse import urlparse + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +from utils import msgbox_frame, HostsManager +from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL +from workers import IpOptimizerThread + +class DownloadManager: + def __init__(self, main_window): + """初始化下载管理器 + + Args: + main_window: 主窗口实例,用于访问UI和状态 + """ + self.main_window = main_window + self.selected_folder = "" + self.download_queue = deque() + self.current_download_thread = None + self.hosts_manager = HostsManager() + self.optimized_ip = None + self.optimization_done = False # 标记是否已执行过优选 + self.optimizing_msg_box = None + + def file_dialog(self): + """显示文件夹选择对话框,选择游戏安装目录""" + self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory( + self.main_window, f"选择游戏所在【上级目录】 {APP_NAME}" + ) + if not self.selected_folder: + QtWidgets.QMessageBox.warning( + self.main_window, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" + ) + return + self.download_action() + + def get_install_paths(self): + """获取所有游戏版本的安装路径""" + return { + game: os.path.join(self.selected_folder, info["install_path"]) + for game, info in GAME_INFO.items() + } + + def is_debug_mode(self): + """检查是否处于调试模式""" + if hasattr(self.main_window, 'ui_manager') and self.main_window.ui_manager: + if hasattr(self.main_window.ui_manager, 'debug_action') and self.main_window.ui_manager.debug_action: + return self.main_window.ui_manager.debug_action.isChecked() + return False + + def get_download_url(self) -> dict: + """获取所有游戏版本的下载链接 + + Returns: + dict: 包含游戏版本和下载URL的字典 + """ + try: + if self.main_window.cloud_config: + if self.is_debug_mode(): + print("--- Using pre-fetched cloud config ---") + config_data = self.main_window.cloud_config + else: + # 如果没有预加载的配置,则同步获取 + headers = {"User-Agent": UA} + response = requests.get(CONFIG_URL, headers=headers, timeout=10) + response.raise_for_status() + config_data = response.json() + + if not config_data: + raise ValueError("未能获取或解析配置数据") + + if self.is_debug_mode(): + print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}") + + # 统一处理URL提取,确保返回扁平化的字典 + urls = {} + for i in range(4): + key = f"vol.{i+1}.data" + if key in config_data and "url" in config_data[key]: + urls[f"vol{i+1}"] = config_data[key]["url"] + + if "after.data" in config_data and "url" in config_data["after.data"]: + urls["after"] = config_data["after.data"]["url"] + + # 检查是否成功提取了所有URL + if len(urls) != 5: + missing_keys_map = { + f"vol{i+1}": f"vol.{i+1}.data" for i in range(4) + } + missing_keys_map["after"] = "after.data" + + extracted_keys = set(urls.keys()) + all_keys = set(missing_keys_map.keys()) + missing_simple_keys = all_keys - extracted_keys + + missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys] + raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}") + + if self.is_debug_mode(): + print(f"DEBUG: Extracted URLs: {urls}") + print("--- Finished getting download URL successfully ---") + return urls + + except requests.exceptions.RequestException as e: + status_code = e.response.status_code if e.response is not None else "未知" + try: + error_response = e.response.json() if e.response else {} + json_title = error_response.get("title", "无错误类型") + json_message = error_response.get("message", "无附加错误信息") + except (ValueError, AttributeError): + json_title = "配置文件异常,无法解析错误类型" + json_message = "配置文件异常,无法解析错误信息" + + if self.is_debug_mode(): + print(f"ERROR: Failed to get download config due to RequestException: {e}") + + QtWidgets.QMessageBox.critical( + self.main_window, + f"错误 - {APP_NAME}", + f"\n下载配置获取失败\n\n【HTTP状态】:{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n", + ) + return {} + except ValueError as e: + if self.is_debug_mode(): + print(f"ERROR: Failed to parse download config due to ValueError: {e}") + + QtWidgets.QMessageBox.critical( + self.main_window, + f"错误 - {APP_NAME}", + f"\n配置文件格式异常\n\n【错误信息】:{e}\n", + ) + return {} + + def download_action(self): + """开始下载流程""" + # 禁用开始安装按钮 + self.main_window.ui.start_install_btn.setEnabled(False) + + # 显示哈希检查窗口 + self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window() + + # 执行预检查 + install_paths = self.get_install_paths() + + self.main_window.hash_thread = self.main_window.create_hash_thread("pre", install_paths) + self.main_window.hash_thread.pre_finished.connect(self.on_pre_hash_finished) + self.main_window.hash_thread.start() + + def on_pre_hash_finished(self, updated_status): + """哈希预检查完成后的处理 + + Args: + updated_status: 更新后的安装状态 + """ + self.main_window.installed_status = updated_status + if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): + self.main_window.hash_msg_box.accept() + self.main_window.hash_msg_box = None + + # 获取下载配置 + config = self.get_download_url() + if not config: + QtWidgets.QMessageBox.critical( + self.main_window, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" + ) + # 重新启用开始安装按钮 + self.main_window.ui.start_install_btn.setEnabled(True) + return + + # 填充下载队列 + self._fill_download_queue(config) + + # 如果没有需要下载的内容,直接进行最终校验 + if not self.download_queue: + self.main_window.after_hash_compare() + return + + # 只有当有需要下载内容时才询问是否使用Cloudflare加速 + # 询问用户是否使用Cloudflare加速 + msg_box = QtWidgets.QMessageBox(self.main_window) + msg_box.setWindowTitle(f"下载优化 - {APP_NAME}") + msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。") + msg_box.setIcon(QtWidgets.QMessageBox.Icon.Question) + + yes_button = msg_box.addButton("是,开启加速", QtWidgets.QMessageBox.ButtonRole.YesRole) + no_button = msg_box.addButton("否,直接下载", QtWidgets.QMessageBox.ButtonRole.NoRole) + + msg_box.exec() + + use_optimization = msg_box.clickedButton() == yes_button + + if use_optimization and not self.optimization_done: + first_url = self.download_queue[0][0] + self._start_ip_optimization(first_url) + else: + # 如果用户选择不优化,或已经优化过,直接开始下载 + self.next_download_task() + + def _fill_download_queue(self, config): + """填充下载队列 + + Args: + config: 包含下载URL的配置字典 + """ + # 清空现有队列 + self.download_queue.clear() + + # 添加nekopara 1-4 + for i in range(1, 5): + game_version = f"NEKOPARA Vol.{i}" + if not self.main_window.installed_status.get(game_version, False): + url = config.get(f"vol{i}") + if not url: continue + game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}") + _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) + + # 添加nekopara after + game_version = "NEKOPARA After" + if not self.main_window.installed_status.get(game_version, False): + url = config.get("after") + if url: + game_folder = os.path.join(self.selected_folder, "NEKOPARA After") + _7z_path = os.path.join(PLUGIN, "after.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) + + def _start_ip_optimization(self, url): + """开始IP优化过程 + + Args: + url: 用于优化的URL + """ + # 禁用退出按钮 + self.main_window.ui.exit_btn.setEnabled(False) + + self.optimizing_msg_box = msgbox_frame( + f"通知 - {APP_NAME}", + "\n正在优选Cloudflare IP,请稍候...\n\n这可能需要5-10分钟,请耐心等待喵~" + ) + # 我们不再提供"跳过"按钮 + self.optimizing_msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.NoButton) + self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal) + self.optimizing_msg_box.open() + + # 创建并启动优化线程 + self.ip_optimizer_thread = IpOptimizerThread(url) + self.ip_optimizer_thread.finished.connect(self.on_optimization_finished) + self.ip_optimizer_thread.start() + + def on_optimization_finished(self, ip): + """IP优化完成后的处理 + + Args: + ip: 优选的IP地址,如果失败则为空字符串 + """ + self.optimized_ip = ip + self.optimization_done = True + + # 关闭提示框 + if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box: + if self.optimizing_msg_box.isVisible(): + self.optimizing_msg_box.accept() + self.optimizing_msg_box = None + + # 显示优选结果 + if not ip: + QtWidgets.QMessageBox.warning( + self.main_window, + f"优选失败 - {APP_NAME}", + "\n未能找到合适的Cloudflare IP,将使用默认网络进行下载。\n" + ) + else: + # 应用优选IP到hosts文件 + if self.download_queue: + first_url = self.download_queue[0][0] + hostname = urlparse(first_url).hostname + + # 先清理可能存在的旧记录 + self.hosts_manager.clean_hostname_entries(hostname) + + if self.hosts_manager.apply_ip(hostname, ip): + QtWidgets.QMessageBox.information( + self.main_window, + f"成功 - {APP_NAME}", + f"\n已将优选IP ({ip}) 应用到hosts文件。\n" + ) + else: + QtWidgets.QMessageBox.critical( + self.main_window, + f"错误 - {APP_NAME}", + "\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n" + ) + + # 开始下载 + self.next_download_task() + + def next_download_task(self): + """处理下载队列中的下一个任务""" + if not self.download_queue: + self.main_window.after_hash_compare() + return + + # 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务 + if self.current_download_thread and self.current_download_thread.isRunning(): + return + + # 获取下一个下载任务并开始 + url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft() + self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) + + def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path): + """准备下载特定游戏版本 + + Args: + url: 下载URL + game_folder: 游戏文件夹路径 + game_version: 游戏版本名称 + _7z_path: 7z文件保存路径 + plugin_path: 插件路径 + """ + game_exe = { + game: os.path.join( + self.selected_folder, info["install_path"].split("/")[0], info["exe"] + ) + for game, info in GAME_INFO.items() + } + + # 检查游戏是否已安装 + if ( + game_version not in game_exe + or not os.path.exists(game_exe[game_version]) + or self.main_window.installed_status[game_version] + ): + self.main_window.installed_status[game_version] = False + self.main_window.show_result() + return + + # 创建进度窗口并开始下载 + self.main_window.progress_window = self.main_window.create_progress_window() + self.start_download(url, _7z_path, game_version, game_folder, plugin_path) + + def start_download(self, url, _7z_path, game_version, game_folder, plugin_path): + """启动下载线程 + + Args: + url: 下载URL + _7z_path: 7z文件保存路径 + game_version: 游戏版本名称 + game_folder: 游戏文件夹路径 + plugin_path: 插件路径 + """ + # 禁用退出按钮 + self.main_window.ui.exit_btn.setEnabled(False) + + if self.optimized_ip: + print(f"已为 {game_version} 获取到优选IP: {self.optimized_ip}") + else: + print(f"未能为 {game_version} 获取优选IP,将使用默认线路。") + + # 创建并连接下载线程 + self.current_download_thread = self.main_window.create_download_thread(url, _7z_path, game_version) + self.current_download_thread.progress.connect(self.main_window.progress_window.update_progress) + self.current_download_thread.finished.connect( + lambda success, error: self.on_download_finished( + success, + error, + url, + game_folder, + game_version, + _7z_path, + plugin_path, + ) + ) + + # 连接停止按钮 + self.main_window.progress_window.stop_button.clicked.connect(self.current_download_thread.stop) + + # 启动线程和显示进度窗口 + self.current_download_thread.start() + self.main_window.progress_window.exec() + + def on_download_finished(self, success, error, url, game_folder, game_version, _7z_path, plugin_path): + """下载完成后的处理 + + Args: + success: 是否下载成功 + error: 错误信息 + url: 下载URL + game_folder: 游戏文件夹路径 + game_version: 游戏版本名称 + _7z_path: 7z文件保存路径 + plugin_path: 插件路径 + """ + # 关闭进度窗口 + if self.main_window.progress_window.isVisible(): + self.main_window.progress_window.reject() + + # 处理下载失败 + if not success: + print(f"--- Download Failed: {game_version} ---") + print(error) + print("------------------------------------") + msg_box = QtWidgets.QMessageBox(self.main_window) + msg_box.setWindowTitle(f"下载失败 - {APP_NAME}") + msg_box.setText(f"\n文件获取失败: {game_version}\n错误: {error}\n\n是否重试?") + + retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole) + next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole) + end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole) + + msg_box.exec() + clicked_button = msg_box.clickedButton() + + # 处理用户选择 + if clicked_button == retry_button: + self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) + elif clicked_button == next_button: + self.next_download_task() + else: + self.on_download_stopped() + return + + # 下载成功,开始解压缩 + self.main_window.hash_msg_box = self.main_window.hash_manager.hash_pop_window() + + # 创建并启动解压线程 + self.main_window.extraction_thread = self.main_window.create_extraction_thread( + _7z_path, game_folder, plugin_path, game_version + ) + self.main_window.extraction_thread.finished.connect(self.on_extraction_finished) + self.main_window.extraction_thread.start() + + def on_extraction_finished(self, success, error_message, game_version): + """解压完成后的处理 + + Args: + success: 是否解压成功 + error_message: 错误信息 + game_version: 游戏版本 + """ + # 关闭哈希检查窗口 + if self.main_window.hash_msg_box and self.main_window.hash_msg_box.isVisible(): + self.main_window.hash_msg_box.close() + + # 处理解压结果 + if not success: + QtWidgets.QMessageBox.critical(self.main_window, f"错误 - {APP_NAME}", error_message) + self.main_window.installed_status[game_version] = False + else: + self.main_window.installed_status[game_version] = True + + # 继续下一个下载任务 + self.next_download_task() + + def on_download_stopped(self): + """当用户点击停止按钮或选择结束时调用的函数""" + # 停止IP优化线程 + if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning(): + self.ip_optimizer_thread.stop() + self.ip_optimizer_thread.wait() + if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box: + if self.optimizing_msg_box.isVisible(): + self.optimizing_msg_box.accept() + self.optimizing_msg_box = None + + # 停止当前可能仍在运行的下载线程 + if self.current_download_thread and self.current_download_thread.isRunning(): + self.current_download_thread.stop() + self.current_download_thread.wait() # 等待线程完全终止 + + # 清空下载队列,因为用户决定停止 + self.download_queue.clear() + + # 确保进度窗口已关闭 + if hasattr(self.main_window, 'progress_window') and self.main_window.progress_window.isVisible(): + self.main_window.progress_window.reject() + + # 可以在这里决定是否立即进行哈希比较或显示结果 + print("下载已全部停止。") + self.main_window.setEnabled(True) # 恢复主窗口交互 + + # 重新启用退出按钮和开始安装按钮 + self.main_window.ui.exit_btn.setEnabled(True) + self.main_window.ui.start_install_btn.setEnabled(True) + + self.main_window.show_result() \ No newline at end of file diff --git a/source/core/ui_manager.py b/source/core/ui_manager.py new file mode 100644 index 0000000..3bb3601 --- /dev/null +++ b/source/core/ui_manager.py @@ -0,0 +1,94 @@ +from PySide6.QtGui import QIcon, QAction +from PySide6.QtWidgets import QMessageBox, QMainWindow +from PySide6.QtCore import Qt +import webbrowser + +from utils import load_base64_image, msgbox_frame +from data.config import APP_NAME, APP_VERSION +from data.pic_data import img_data + +class UIManager: + def __init__(self, main_window): + """初始化UI管理器 + + Args: + main_window: 主窗口实例,用于设置UI元素 + """ + self.main_window = main_window + # 使用getattr获取ui属性,如果不存在则为None + self.ui = getattr(main_window, 'ui', None) + self.debug_action = None + + def setup_ui(self): + """设置UI元素,包括窗口图标、标题和菜单""" + # 设置窗口图标 + icon_data = img_data.get("icon") + if icon_data: + pixmap = load_base64_image(icon_data) + self.main_window.setWindowIcon(QIcon(pixmap)) + + # 设置窗口标题 + self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}") + + # 设置菜单 + self._setup_help_menu() + self._setup_settings_menu() + + def _setup_help_menu(self): + """设置"帮助"菜单""" + if not self.ui or not hasattr(self.ui, 'menu_2'): + return + + project_home_action = QAction("项目主页", self.main_window) + project_home_action.triggered.connect(self.open_project_home_page) + + about_action = QAction("关于", self.main_window) + about_action.triggered.connect(self.show_about_dialog) + + self.ui.menu_2.addAction(project_home_action) + self.ui.menu_2.addAction(about_action) + + def _setup_settings_menu(self): + """设置"设置"菜单""" + if not self.ui or not hasattr(self.ui, 'menu'): + return + + self.debug_action = QAction("Debug模式", self.main_window, checkable=True) + + # 安全地获取config属性 + config = getattr(self.main_window, 'config', {}) + debug_mode = False + if isinstance(config, dict): + debug_mode = config.get("debug_mode", False) + + self.debug_action.setChecked(debug_mode) + + # 安全地连接toggle_debug_mode方法 + if hasattr(self.main_window, 'toggle_debug_mode'): + self.debug_action.triggered.connect(self.main_window.toggle_debug_mode) + + self.ui.menu.addAction(self.debug_action) + + # 为未来功能预留的"切换下载源"按钮 + self.switch_source_action = QAction("切换下载源", self.main_window) + self.switch_source_action.setEnabled(False) # 暂时禁用 + self.ui.menu.addAction(self.switch_source_action) + + def open_project_home_page(self): + """打开项目主页""" + webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT") + + def show_about_dialog(self): + """显示关于对话框""" + about_text = f""" +
{APP_NAME} v{APP_VERSION}
+原作: Yanam1Anna
+此应用根据 GPL-3.0 许可证 授权。
+ """ + msg_box = msgbox_frame( + f"关于 - {APP_NAME}", + about_text, + QMessageBox.StandardButton.Ok, + ) + msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat + msg_box.exec() \ No newline at end of file diff --git a/source/config.py b/source/data/config.py similarity index 100% rename from source/config.py rename to source/data/config.py diff --git a/source/pic_data.py b/source/data/pic_data.py similarity index 100% rename from source/pic_data.py rename to source/data/pic_data.py diff --git a/source/main_window.py b/source/main_window.py index af1a85a..bcba254 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -1,167 +1,27 @@ import os import sys import shutil -import webbrowser -import requests -import py7zr import json -from urllib.parse import urlparse -from collections import deque -from PySide6 import QtWidgets -from PySide6.QtCore import QTimer, Qt, QThread, Signal -from PySide6.QtGui import QIcon, QAction -from PySide6.QtWidgets import QMainWindow, QFileDialog, QApplication, QMessageBox, QPushButton -from Ui_install import Ui_MainWindows -from animations import MultiStageAnimations -from config import ( - APP_NAME, APP_VERSION, PLUGIN, GAME_INFO, BLOCK_SIZE, +from PySide6 import QtWidgets +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QMainWindow, QMessageBox + +from ui.Ui_install import Ui_MainWindows +from data.config import ( + APP_NAME, PLUGIN, GAME_INFO, BLOCK_SIZE, PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE ) from utils import ( - load_base64_image, HashManager, AdminPrivileges, msgbox_frame, - load_config, save_config, HostsManager, censor_url + load_config, save_config, HashManager, AdminPrivileges, msgbox_frame +) +from workers import ( + DownloadThread, ProgressWindow, IpOptimizerThread, + HashThread, ExtractionThread, ConfigFetchThread +) +from core import ( + MultiStageAnimations, UIManager, DownloadManager, DebugManager ) -from download import DownloadThread, ProgressWindow -from ip_optimizer import IpOptimizer -from pic_data import img_data - - -class Logger: - def __init__(self, filename, stream): - self.terminal = stream - self.log = open(filename, "w", encoding="utf-8") - - def write(self, message): - censored_message = censor_url(message) - self.terminal.write(censored_message) - self.log.write(censored_message) - self.flush() - - def flush(self): - self.terminal.flush() - self.log.flush() - - def close(self): - self.log.close() - -class IpOptimizerThread(QThread): - finished = Signal(str) - - def __init__(self, url, parent=None): - super().__init__(parent) - self.url = url - self.optimizer = IpOptimizer() - - def run(self): - optimal_ip = self.optimizer.get_optimal_ip(self.url) - self.finished.emit(optimal_ip if optimal_ip else "") - - def stop(self): - self.optimizer.stop() - -class HashThread(QThread): - pre_finished = Signal(dict) - after_finished = Signal(dict) - - def __init__(self, mode, install_paths, plugin_hash, installed_status, parent=None): - super().__init__(parent) - self.mode = mode - self.install_paths = install_paths - self.plugin_hash = plugin_hash - self.installed_status = installed_status - # 每个线程都应该有自己的HashManager实例 - self.hash_manager = HashManager(BLOCK_SIZE) - - def run(self): - if self.mode == "pre": - updated_status = self.hash_manager.cfg_pre_hash_compare( - self.install_paths, self.plugin_hash, self.installed_status - ) - self.pre_finished.emit(updated_status) - elif self.mode == "after": - result = self.hash_manager.cfg_after_hash_compare( - self.install_paths, self.plugin_hash, self.installed_status - ) - self.after_finished.emit(result) - - -class ExtractionThread(QThread): - finished = Signal(bool, str, str) # success, error_message, game_version - - def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None): - super().__init__(parent) - self._7z_path = _7z_path - self.game_folder = game_folder - self.plugin_path = plugin_path - self.game_version = game_version - - def run(self): - try: - with py7zr.SevenZipFile(self._7z_path, mode="r") as archive: - archive.extractall(path=PLUGIN) - - os.makedirs(self.game_folder, exist_ok=True) - shutil.copy(self.plugin_path, self.game_folder) - - if self.game_version == "NEKOPARA After": - sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) - shutil.copy(sig_path, self.game_folder) - - self.finished.emit(True, "", self.game_version) - except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e: - self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version) - - -class ConfigFetchThread(QThread): - finished = Signal(object, str) # data, error_message - - def __init__(self, url, headers, debug_mode=False, parent=None): - super().__init__(parent) - self.url = url - self.headers = headers - self.debug_mode = debug_mode - - def run(self): - try: - if self.debug_mode: - print("--- Starting to fetch cloud config ---") - print(f"DEBUG: Requesting URL: {self.url}") - print(f"DEBUG: Using Headers: {self.headers}") - - response = requests.get(self.url, headers=self.headers, timeout=10) - - if self.debug_mode: - print(f"DEBUG: Response Status Code: {response.status_code}") - print(f"DEBUG: Response Headers: {response.headers}") - print(f"DEBUG: Response Text: {response.text}") - - response.raise_for_status() - - # 首先,总是尝试解析JSON - config_data = response.json() - - # 检查是否是要求更新的错误信息 - if config_data.get("message") == "请使用最新版本的FRAISEMOE Addons Installer NEXT进行下载安装": - self.finished.emit(None, "update_required") - return - - # 检查是否是有效的配置文件 - required_keys = [f"vol.{i+1}.data" for i in range(4)] + ["after.data"] - missing_keys = [key for key in required_keys if key not in config_data] - if missing_keys: - self.finished.emit(None, f"missing_keys:{','.join(missing_keys)}") - return - - self.finished.emit(config_data, "") - except requests.exceptions.RequestException as e: - self.finished.emit(None, f"网络请求失败: {e}") - except (ValueError, json.JSONDecodeError) as e: - self.finished.emit(None, f"JSON解析失败: {e}") - finally: - if self.debug_mode: - print("--- Finished fetching cloud config ---") - class MainWindow(QMainWindow): def __init__(self): @@ -170,45 +30,42 @@ class MainWindow(QMainWindow): # 初始化UI (从Ui_install.py导入) self.ui = Ui_MainWindows() self.ui.setupUi(self) - - icon_data = img_data.get("icon") - if icon_data: - pixmap = load_base64_image(icon_data) - self.setWindowIcon(QIcon(pixmap)) - - # 设置窗口标题为APP_NAME加版本号 - self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}") - # 初始化动画系统 (从animations.py导入) - self.animator = MultiStageAnimations(self.ui, self) - - # 初始化功能变量 - self.selected_folder = "" - self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)} - self.installed_status["NEKOPARA After"] = False # 添加After的状态 - self.download_queue = deque() - self.current_download_thread = None - self.hash_manager = HashManager(BLOCK_SIZE) - self.hash_thread = None - self.extraction_thread = None - self.hash_msg_box = None - self.optimized_ip = None - self.optimization_done = False # 标记是否已执行过优选 - self.logger = None - self.hosts_manager = HostsManager() # 实例化HostsManager - self.cloud_config = None - self.config_fetch_thread = None - - # 加载配置 + # 初始化配置 self.config = load_config() + # 初始化状态变量 + self.cloud_config = None + self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)} + self.installed_status["NEKOPARA After"] = False # 添加After的状态 + self.hash_msg_box = None + self.progress_window = None + + # 初始化工具类 + self.hash_manager = HashManager(BLOCK_SIZE) + self.admin_privileges = AdminPrivileges() + + # 初始化管理器 + self.animator = MultiStageAnimations(self.ui, self) + self.ui_manager = UIManager(self) + + # 首先设置UI - 确保debug_action已初始化 + self.ui_manager.setup_ui() + + self.debug_manager = DebugManager(self) + self.download_manager = DownloadManager(self) + + # 设置退出按钮和开始安装按钮的样式表,使其在禁用状态下不会变灰 + button_style = "QPushButton:disabled { opacity: 1.0; }" + self.ui.exit_btn.setStyleSheet(button_style) + self.ui.start_install_btn.setStyleSheet(button_style) + # 检查管理员权限和进程 - admin_privileges = AdminPrivileges() - admin_privileges.request_admin_privileges() - admin_privileges.check_and_terminate_processes() + self.admin_privileges.request_admin_privileges() + self.admin_privileges.check_and_terminate_processes() # 备份hosts文件 - self.hosts_manager.backup() + self.download_manager.hosts_manager.backup() # 创建缓存目录 if not os.path.exists(PLUGIN): @@ -222,32 +79,14 @@ class MainWindow(QMainWindow): ) sys.exit(1) - # 连接信号 (使用Ui_install.py中的组件名称) - self.ui.start_install_btn.clicked.connect(self.file_dialog) + # 连接信号 + self.ui.start_install_btn.clicked.connect(self.download_manager.file_dialog) self.ui.exit_btn.clicked.connect(self.shutdown_app) - - # “帮助”菜单 - project_home_action = QAction("项目主页", self) - project_home_action.triggered.connect(self.open_project_home_page) - about_action = QAction("关于", self) - about_action.triggered.connect(self.show_about_dialog) - self.ui.menu_2.addAction(project_home_action) - self.ui.menu_2.addAction(about_action) - - # “设置”菜单 - self.debug_action = QAction("Debug模式", self, checkable=True) - self.debug_action.setChecked(self.config.get("debug_mode", False)) - self.debug_action.triggered.connect(self.toggle_debug_mode) - self.ui.menu.addAction(self.debug_action) - - # 为未来功能预留的“切换下载源”按钮 - self.switch_source_action = QAction("切换下载源", self) - self.switch_source_action.setEnabled(False) # 暂时禁用 - self.ui.menu.addAction(self.switch_source_action) - + # 根据初始配置决定是否开启Debug模式 - if self.debug_action.isChecked(): - self.start_logging() + if hasattr(self.ui_manager, 'debug_action') and self.ui_manager.debug_action: + if self.ui_manager.debug_action.isChecked(): + self.debug_manager.start_logging() # 在窗口显示前设置初始状态 self.animator.initialize() @@ -256,437 +95,159 @@ class MainWindow(QMainWindow): QTimer.singleShot(100, self.start_animations) def start_animations(self): - self.ui.exit_btn.setEnabled(False) + """开始启动动画""" + # 不再禁用退出按钮的交互性,只通过样式表控制外观 + # 但仍然需要跟踪动画状态,防止用户在动画播放过程中退出 + self.animation_in_progress = True + + # 禁用开始安装按钮,防止在动画播放期间点击 + self.ui.start_install_btn.setEnabled(False) + self.animator.animation_finished.connect(self.on_animations_finished) self.animator.start_animations() self.fetch_cloud_config() def on_animations_finished(self): - self.ui.exit_btn.setEnabled(True) + """动画完成后启用按钮""" + self.animation_in_progress = False + + # 启用开始安装按钮 + self.ui.start_install_btn.setEnabled(True) def fetch_cloud_config(self): + """获取云端配置""" headers = {"User-Agent": UA} - debug_mode = self.debug_action.isChecked() + debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False self.config_fetch_thread = ConfigFetchThread(CONFIG_URL, headers, debug_mode, self) self.config_fetch_thread.finished.connect(self.on_config_fetched) self.config_fetch_thread.start() def on_config_fetched(self, data, error_message): + """云端配置获取完成的回调处理 + + Args: + data: 获取到的配置数据 + error_message: 错误信息,如果有 + """ if error_message: if error_message == "update_required": msg_box = msgbox_frame( f"更新提示 - {APP_NAME}", "\n当前版本过低,请及时更新。\n", - QtWidgets.QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.Ok, ) msg_box.exec() - self.open_project_home_page() + self.ui_manager.open_project_home_page() self.shutdown_app(force_exit=True) elif "missing_keys" in error_message: missing_versions = error_message.split(":")[1] msg_box = msgbox_frame( f"配置缺失 - {APP_NAME}", f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n', - QtWidgets.QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.Ok, ) msg_box.exec() else: # 其他错误暂时只在debug模式下打印 - if self.debug_action.isChecked(): + debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False + if debug_mode: print(f"获取云端配置失败: {error_message}") else: self.cloud_config = data - if self.debug_action.isChecked(): + debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False + if debug_mode: print("--- Cloud config fetched successfully ---") print(json.dumps(data, indent=2)) def toggle_debug_mode(self, checked): - self.config["debug_mode"] = checked - save_config(self.config) - if checked: - self.start_logging() - else: - self.stop_logging() - - def start_logging(self): - if self.logger is None: - try: - if os.path.exists(LOG_FILE): - os.remove(LOG_FILE) - # 保存原始的 stdout 和 stderr - self.original_stdout = sys.stdout - self.original_stderr = sys.stderr - # 创建 Logger 实例 - self.logger = Logger(LOG_FILE, self.original_stdout) - sys.stdout = self.logger - sys.stderr = self.logger - print("--- Debug mode enabled ---") - except (IOError, OSError) as e: - QtWidgets.QMessageBox.critical(self, "错误", f"无法创建日志文件: {e}") - self.logger = None - - def stop_logging(self): - if self.logger: - print("--- Debug mode disabled ---") - sys.stdout = self.original_stdout - sys.stderr = self.original_stderr - self.logger.close() - self.logger = None + """切换调试模式 - def get_install_paths(self): - return { - game: os.path.join(self.selected_folder, info["install_path"]) - for game, info in GAME_INFO.items() - } - - def file_dialog(self): - self.selected_folder = QFileDialog.getExistingDirectory( - self, f"选择游戏所在【上级目录】 {APP_NAME}" - ) - if not self.selected_folder: - QtWidgets.QMessageBox.warning( - self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" - ) - return - self.download_action() - - def get_download_url(self) -> dict: - try: - if self.cloud_config: - if self.debug_action.isChecked(): - print("--- Using pre-fetched cloud config ---") - config_data = self.cloud_config - else: - # 如果没有预加载的配置,则同步获取 - headers = {"User-Agent": UA} - response = requests.get(CONFIG_URL, headers=headers, timeout=10) - response.raise_for_status() - config_data = response.json() - - if not config_data: - raise ValueError("未能获取或解析配置数据") - - if self.debug_action.isChecked(): - print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}") - - # 统一处理URL提取,确保返回扁平化的字典 - urls = {} - for i in range(4): - key = f"vol.{i+1}.data" - if key in config_data and "url" in config_data[key]: - urls[f"vol{i+1}"] = config_data[key]["url"] + Args: + checked: 是否启用调试模式 + """ + self.debug_manager.toggle_debug_mode(checked) + + def save_config(self, config): + """保存配置的便捷方法""" + save_config(config) + + def create_download_thread(self, url, _7z_path, game_version): + """创建下载线程 + + Args: + url: 下载URL + _7z_path: 7z文件保存路径 + game_version: 游戏版本 - if "after.data" in config_data and "url" in config_data["after.data"]: - urls["after"] = config_data["after.data"]["url"] - - # 检查是否成功提取了所有URL - if len(urls) != 5: - missing_keys_map = { - f"vol{i+1}": f"vol.{i+1}.data" for i in range(4) - } - missing_keys_map["after"] = "after.data" - - extracted_keys = set(urls.keys()) - all_keys = set(missing_keys_map.keys()) - missing_simple_keys = all_keys - extracted_keys - - missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys] - raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}") - - if self.debug_action.isChecked(): - print(f"DEBUG: Extracted URLs: {urls}") - print("--- Finished getting download URL successfully ---") - return urls - if self.debug_action.isChecked(): - print(f"DEBUG: Extracted URLs: {urls}") - print("--- Finished getting download URL successfully ---") - return urls - - except requests.exceptions.RequestException as e: - status_code = e.response.status_code if e.response is not None else "未知" - try: - error_response = e.response.json() if e.response else {} - json_title = error_response.get("title", "无错误类型") - json_message = error_response.get("message", "无附加错误信息") - except (ValueError, AttributeError): - json_title = "配置文件异常,无法解析错误类型" - json_message = "配置文件异常,无法解析错误信息" - - if self.debug_action.isChecked(): - print(f"ERROR: Failed to get download config due to RequestException: {e}") + Returns: + DownloadThread: 下载线程实例 + """ + return DownloadThread(url, _7z_path, game_version, self) + + def create_progress_window(self): + """创建下载进度窗口 + + Returns: + ProgressWindow: 进度窗口实例 + """ + return ProgressWindow(self) + + def create_hash_thread(self, mode, install_paths): + """创建哈希检查线程 + + Args: + mode: 检查模式,"pre"或"after" + install_paths: 安装路径字典 - QtWidgets.QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - f"\n下载配置获取失败\n\n【HTTP状态】:{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n", - ) - return {} - except ValueError as e: - if self.debug_action.isChecked(): - print(f"ERROR: Failed to parse download config due to ValueError: {e}") - - QtWidgets.QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - f"\n配置文件格式异常\n\n【错误信息】:{e}\n", - ) - return {} - - def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path): - game_exe = { - game: os.path.join( - self.selected_folder, info["install_path"].split("/")[0], info["exe"] - ) - for game, info in GAME_INFO.items() - } + Returns: + HashThread: 哈希检查线程实例 + """ + return HashThread(mode, install_paths, PLUGIN_HASH, self.installed_status, self) - if ( - game_version not in game_exe - or not os.path.exists(game_exe[game_version]) - or self.installed_status[game_version] - ): - self.installed_status[game_version] = False - self.show_result() - return + def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version): + """创建解压线程 + + Args: + _7z_path: 7z文件路径 + game_folder: 游戏文件夹路径 + plugin_path: 插件路径 + game_version: 游戏版本 - self.progress_window = ProgressWindow(self) - self.start_download_with_ip(self.optimized_ip, url, _7z_path, game_version, game_folder, plugin_path) - - - def on_optimization_and_hosts_finished(self, ip): - self.optimized_ip = ip - self.optimization_done = True - if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box: - if self.optimizing_msg_box.isVisible(): - self.optimizing_msg_box.accept() - self.optimizing_msg_box = None - - if not ip: - QtWidgets.QMessageBox.warning(self, f"优选失败 - {APP_NAME}", "\n未能找到合适的Cloudflare IP,将使用默认网络进行下载。\n") - else: - if self.download_queue: - first_url = self.download_queue[0][0] - hostname = urlparse(first_url).hostname - if self.hosts_manager.apply_ip(hostname, ip): - QtWidgets.QMessageBox.information(self, f"成功 - {APP_NAME}", f"\n已将优选IP ({ip}) 应用到hosts文件。\n") - else: - QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", "\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n") + Returns: + ExtractionThread: 解压线程实例 + """ + return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self) - self.next_download_task() - - def start_download_with_ip(self, preferred_ip, url, _7z_path, game_version, game_folder, plugin_path): - if preferred_ip: - print(f"已为 {game_version} 获取到优选IP: {preferred_ip}") - else: - print(f"未能为 {game_version} 获取优选IP,将使用默认线路。") - - self.current_download_thread = DownloadThread(url, _7z_path, game_version, self) - self.current_download_thread.progress.connect(self.progress_window.update_progress) - self.current_download_thread.finished.connect( - lambda success, error: self.install_setting( - success, - error, - self.progress_window, - url, - game_folder, - game_version, - _7z_path, - plugin_path, - ) - ) + def after_hash_compare(self): + """进行安装后哈希比较""" + # 禁用退出按钮 + self.ui.exit_btn.setEnabled(False) - self.progress_window.stop_button.clicked.connect(self.current_download_thread.stop) - self.current_download_thread.start() - self.progress_window.exec() - - def install_setting( - self, - success, - error, - progress_window, - url, - game_folder, - game_version, - _7z_path, - plugin_path, - ): - if progress_window.isVisible(): - progress_window.reject() - - if not success: - print(f"--- Download Failed: {game_version} ---") - print(error) - print("------------------------------------") - msg_box = QtWidgets.QMessageBox(self) - msg_box.setWindowTitle(f"下载失败 - {APP_NAME}") - msg_box.setText(f"\n文件获取失败: {game_version}\n错误: {error}\n\n是否重试?") - - retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole) - next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole) - end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole) - - msg_box.exec() - clicked_button = msg_box.clickedButton() - - if clicked_button == retry_button: - self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) - elif clicked_button == next_button: - self.next_download_task() - else: - self.on_download_stopped() - return - - # --- Start Extraction in a new thread --- - self.hash_msg_box = self.hash_manager.hash_pop_window() - - self.extraction_thread = ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self) - self.extraction_thread.finished.connect(self.on_extraction_finished) - self.extraction_thread.start() - - def on_extraction_finished(self, success, error_message, game_version): - if self.hash_msg_box and self.hash_msg_box.isVisible(): - self.hash_msg_box.close() - - if not success: - QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", error_message) - self.installed_status[game_version] = False - else: - self.installed_status[game_version] = True - - self.next_download_task() - - def download_action(self): - # 询问用户是否使用Cloudflare加速 - msg_box = QMessageBox(self) - msg_box.setWindowTitle(f"下载优化 - {APP_NAME}") - msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。") - msg_box.setIcon(QMessageBox.Icon.Question) - - yes_button = msg_box.addButton("是,开启加速", QMessageBox.ButtonRole.YesRole) - no_button = msg_box.addButton("否,直接下载", QMessageBox.ButtonRole.NoRole) - - msg_box.exec() - - use_optimization = msg_box.clickedButton() == yes_button - self.hash_msg_box = self.hash_manager.hash_pop_window() - install_paths = self.get_install_paths() + install_paths = self.download_manager.get_install_paths() - self.hash_thread = HashThread("pre", install_paths, PLUGIN_HASH, self.installed_status, self) - # 将用户选择传递给哈希完成后的处理函数 - self.hash_thread.pre_finished.connect(lambda status: self.on_pre_hash_finished(status, use_optimization)) - self.hash_thread.start() - - def on_pre_hash_finished(self, updated_status, use_optimization): - self.installed_status = updated_status - if self.hash_msg_box and self.hash_msg_box.isVisible(): - self.hash_msg_box.accept() - self.hash_msg_box = None - - config = self.get_download_url() - if not config: - QtWidgets.QMessageBox.critical( - self, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" - ) - return - - # --- 填充下载队列 --- - for i in range(1, 5): - game_version = f"NEKOPARA Vol.{i}" - if not self.installed_status.get(game_version, False): - url = config.get(f"vol{i}") - if not url: continue - game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}") - _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") - plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) - self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) - - game_version = "NEKOPARA After" - if not self.installed_status.get(game_version, False): - url = config.get("after") - if url: - game_folder = os.path.join(self.selected_folder, "NEKOPARA After") - _7z_path = os.path.join(PLUGIN, "after.7z") - plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) - self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) - - if not self.download_queue: - self.after_hash_compare(PLUGIN_HASH) - return - - if use_optimization and not self.optimization_done: - first_url = self.download_queue[0][0] - self.optimizing_msg_box = msgbox_frame( - f"通知 - {APP_NAME}", - "\n正在优选Cloudflare IP,请稍候...\n\n这可能需要5-10分钟,请耐心等待喵~" - ) - # 我们不再提供“跳过”按钮,因为用户已经做出了选择 - self.optimizing_msg_box.setStandardButtons(QMessageBox.StandardButton.NoButton) - self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal) - self.optimizing_msg_box.open() - - self.ip_optimizer_thread = IpOptimizerThread(first_url) - # 优选完成后,需要修改hosts并开始下载 - self.ip_optimizer_thread.finished.connect(self.on_optimization_and_hosts_finished) - self.ip_optimizer_thread.start() - else: - # 如果用户选择不优化,或已经优化过,直接开始下载 - self.next_download_task() - - def next_download_task(self): - if not self.download_queue: - self.after_hash_compare(PLUGIN_HASH) - return - # 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务 - if self.current_download_thread and self.current_download_thread.isRunning(): - return - - # 在开始下载前,确保hosts文件已修改(如果需要) - # 这里的逻辑保持不变,因为hosts文件应该在队列开始前就被修改了 - url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft() - self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) - - def on_download_stopped(self): - """当用户点击停止按钮或选择结束时调用的槽函数""" - # 停止IP优选线程 - if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning(): - self.ip_optimizer_thread.stop() - self.ip_optimizer_thread.wait() - if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box: - if self.optimizing_msg_box.isVisible(): - self.optimizing_msg_box.accept() - self.optimizing_msg_box = None - - # 停止当前可能仍在运行的下载线程 - if self.current_download_thread and self.current_download_thread.isRunning(): - self.current_download_thread.stop() - self.current_download_thread.wait() # 等待线程完全终止 - - # 清空下载队列,因为用户决定停止 - self.download_queue.clear() - - # 确保进度窗口已关闭 - if hasattr(self, 'progress_window') and self.progress_window.isVisible(): - self.progress_window.reject() - - # 可以在这里决定是否立即进行哈希比较或显示结果 - print("下载已全部停止。") - self.setEnabled(True) # 恢复主窗口交互 - self.show_result() - - def after_hash_compare(self, plugin_hash): - self.hash_msg_box = self.hash_manager.hash_pop_window() - - install_paths = self.get_install_paths() - - self.hash_thread = HashThread("after", install_paths, plugin_hash, self.installed_status, self) + self.hash_thread = self.create_hash_thread("after", install_paths) self.hash_thread.after_finished.connect(self.on_after_hash_finished) self.hash_thread.start() def on_after_hash_finished(self, result): - if self.hash_msg_box and self.hash_msg_box.isVisible(): - self.hash_msg_box.close() + """哈希比较完成后的处理 + + Args: + result: 哈希比较结果 + """ + # 确保哈希检查窗口关闭,无论是否还在显示 + if self.hash_msg_box: + try: + if self.hash_msg_box.isVisible(): + self.hash_msg_box.close() + else: + # 如果窗口已经不可见但没有关闭,也要尝试关闭 + self.hash_msg_box.close() + except: + pass # 忽略任何关闭窗口时的错误 + self.hash_msg_box = None if not result["passed"]: game = result.get("game", "未知游戏") @@ -694,94 +255,95 @@ class MainWindow(QMainWindow): msg_box = msgbox_frame( f"文件校验失败 - {APP_NAME}", message, - QtWidgets.QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.Ok, ) msg_box.exec() - self.show_result() + # 重新启用退出按钮和开始安装按钮 + self.ui.exit_btn.setEnabled(True) + self.ui.start_install_btn.setEnabled(True) + + # 添加短暂延迟确保UI更新 + QTimer.singleShot(100, self.show_result) def show_result(self): + """显示安装结果""" installed_version = "\n".join( [i for i in self.installed_status if self.installed_status[i]] ) failed_ver = "\n".join( [i for i in self.installed_status if not self.installed_status[i]] ) - QtWidgets.QMessageBox.information( + QMessageBox.information( self, f"完成 - {APP_NAME}", f"\n安装结果:\n安装成功数:{len(installed_version.splitlines())} 安装失败数:{len(failed_ver.splitlines())}\n" f"安装成功的版本:\n{installed_version}\n尚未持有或未使用本工具安装补丁的版本:\n{failed_ver}\n", ) - def show_about_dialog(self): - """显示关于对话框""" - about_text = f""" -{APP_NAME} v{APP_VERSION}
-原作: Yanam1Anna
-此应用根据 GPL-3.0 许可证 授权。
- """ - msg_box = msgbox_frame( - f"关于 - {APP_NAME}", - about_text, - QtWidgets.QMessageBox.StandardButton.Ok, - ) - msg_box.setTextFormat(Qt.TextFormat.RichText) # 启用富文本 - msg_box.exec() - - def open_project_home_page(self): - """打开项目主页""" - webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT") - def closeEvent(self, event): + """窗口关闭事件处理 + + Args: + event: 关闭事件 + """ self.shutdown_app(event) def shutdown_app(self, event=None, force_exit=False): - self.hosts_manager.restore() # 恢复hosts文件 - self.stop_logging() # 确保在退出时停止日志记录 + """关闭应用程序 + + Args: + event: 关闭事件,如果是从closeEvent调用的 + force_exit: 是否强制退出 + """ + # 检查是否有动画或任务正在进行 + if hasattr(self, 'animation_in_progress') and self.animation_in_progress and not force_exit: + # 如果动画正在进行,阻止退出 + if event: + event.ignore() + return + + # 检查是否有下载任务正在进行 + if hasattr(self.download_manager, 'current_download_thread') and \ + self.download_manager.current_download_thread and \ + self.download_manager.current_download_thread.isRunning() and not force_exit: + # 询问用户是否确认退出 + reply = QMessageBox.question( + self, + f"确认退出 - {APP_NAME}", + "\n下载任务正在进行中,确定要退出吗?\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.No: + if event: + event.ignore() + return + + # 恢复hosts文件 + self.download_manager.hosts_manager.restore() + + # 额外检查并清理hosts文件中的残留记录 + self.download_manager.hosts_manager.check_and_clean_all_entries() + + # 停止日志记录 + self.debug_manager.stop_logging() if not force_exit: - reply = QtWidgets.QMessageBox.question( + reply = QMessageBox.question( self, - "退出程序", - "\n是否确定退出?\n", - QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, - QtWidgets.QMessageBox.StandardButton.No, + f"确认退出 - {APP_NAME}", + "\n确定要退出吗?\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No ) - if reply != QtWidgets.QMessageBox.StandardButton.Yes: + if reply == QMessageBox.StandardButton.No: if event: event.ignore() return - if ( - self.current_download_thread - and self.current_download_thread.isRunning() - ): - QtWidgets.QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - "\n当前有下载任务正在进行,完成后再试\n", - ) - if event: - event.ignore() - return - - if os.path.exists(PLUGIN): - for attempt in range(3): - try: - shutil.rmtree(PLUGIN) - break - except Exception as e: - if attempt == 2: - QtWidgets.QMessageBox.critical( - self, - f"错误 - {APP_NAME}", - f"\n清理缓存失败\n\n【错误信息】:{e}\n", - ) - if event: - event.accept() - sys.exit(1) + # 退出应用程序 if event: event.accept() else: - sys.exit(0) \ No newline at end of file + sys.exit(0) \ No newline at end of file diff --git a/source/main_window.py.bak b/source/main_window.py.bak new file mode 100644 index 0000000..547b7df --- /dev/null +++ b/source/main_window.py.bak @@ -0,0 +1,656 @@ +import os +import sys +import shutil +import webbrowser +import requests +import json +from urllib.parse import urlparse +from collections import deque + +from PySide6 import QtWidgets +from PySide6.QtCore import QTimer, Qt +from PySide6.QtGui import QIcon, QAction +from PySide6.QtWidgets import QMainWindow, QFileDialog, QApplication, QMessageBox + +from ui.Ui_install import Ui_MainWindows +from core.animations import MultiStageAnimations +from data.config import ( + APP_NAME, APP_VERSION, PLUGIN, GAME_INFO, BLOCK_SIZE, + PLUGIN_HASH, UA, CONFIG_URL, LOG_FILE +) +from utils import ( + load_base64_image, HashManager, AdminPrivileges, msgbox_frame, + load_config, save_config, HostsManager, Logger +) +from workers.download import DownloadThread, ProgressWindow +from data.pic_data import img_data +from workers import ( + IpOptimizerThread, HashThread, ExtractionThread, ConfigFetchThread +) +from core import ( + MultiStageAnimations, UIManager, DownloadManager, DebugManager +) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + # 初始化UI (从Ui_install.py导入) + self.ui = Ui_MainWindows() + self.ui.setupUi(self) + + icon_data = img_data.get("icon") + if icon_data: + pixmap = load_base64_image(icon_data) + self.setWindowIcon(QIcon(pixmap)) + + # 设置窗口标题为APP_NAME加版本号 + self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}") + + # 初始化动画系统 (从animations.py导入) + self.animator = MultiStageAnimations(self.ui, self) + + # 初始化功能变量 + self.selected_folder = "" + self.installed_status = {f"NEKOPARA Vol.{i}": False for i in range(1, 5)} + self.installed_status["NEKOPARA After"] = False # 添加After的状态 + self.download_queue = deque() + self.current_download_thread = None + self.hash_manager = HashManager(BLOCK_SIZE) + self.hash_thread = None + self.extraction_thread = None + self.hash_msg_box = None + self.optimized_ip = None + self.optimization_done = False # 标记是否已执行过优选 + self.logger = None + self.hosts_manager = HostsManager() # 实例化HostsManager + self.cloud_config = None + self.config_fetch_thread = None + + # 加载配置 + self.config = load_config() + + # 检查管理员权限和进程 + self.admin_privileges = AdminPrivileges() + self.admin_privileges.request_admin_privileges() + self.admin_privileges.check_and_terminate_processes() + + # 备份hosts文件 + self.hosts_manager.backup() + + # 创建缓存目录 + if not os.path.exists(PLUGIN): + try: + os.makedirs(PLUGIN) + except OSError as e: + QtWidgets.QMessageBox.critical( + self, + f"错误 - {APP_NAME}", + f"\n无法创建缓存位置\n\n使用管理员身份运行或检查文件读写权限\n\n【错误信息】:{e}\n", + ) + sys.exit(1) + + # 连接信号 (使用Ui_install.py中的组件名称) + self.ui.start_install_btn.clicked.connect(self.download_manager.file_dialog) + self.ui.exit_btn.clicked.connect(self.shutdown_app) + + # “帮助”菜单 + project_home_action = QAction("项目主页", self) + project_home_action.triggered.connect(self.open_project_home_page) + about_action = QAction("关于", self) + about_action.triggered.connect(self.show_about_dialog) + self.ui.menu_2.addAction(project_home_action) + self.ui.menu_2.addAction(about_action) + + # “设置”菜单 + self.debug_action = QAction("Debug模式", self, checkable=True) + self.debug_action.setChecked(self.config.get("debug_mode", False)) + self.debug_action.triggered.connect(self.toggle_debug_mode) + self.ui.menu.addAction(self.debug_action) + + # 为未来功能预留的“切换下载源”按钮 + self.switch_source_action = QAction("切换下载源", self) + self.switch_source_action.setEnabled(False) # 暂时禁用 + self.ui.menu.addAction(self.switch_source_action) + + # 根据初始配置决定是否开启Debug模式 + if self.debug_action.isChecked(): + self.start_logging() + + # 在窗口显示前设置初始状态 + self.animator.initialize() + + # 窗口显示后延迟100ms启动动画 + QTimer.singleShot(100, self.start_animations) + + def start_animations(self): + self.ui.exit_btn.setEnabled(False) + self.animator.animation_finished.connect(self.on_animations_finished) + self.animator.start_animations() + self.fetch_cloud_config() + + def on_animations_finished(self): + self.ui.exit_btn.setEnabled(True) + + def fetch_cloud_config(self): + headers = {"User-Agent": UA} + debug_mode = self.debug_action.isChecked() + self.config_fetch_thread = ConfigFetchThread(CONFIG_URL, headers, debug_mode, self) + self.config_fetch_thread.finished.connect(self.on_config_fetched) + self.config_fetch_thread.start() + + def on_config_fetched(self, data, error_message): + if error_message: + if error_message == "update_required": + msg_box = msgbox_frame( + f"更新提示 - {APP_NAME}", + "\n当前版本过低,请及时更新。\n", + QtWidgets.QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + self.open_project_home_page() + self.shutdown_app(force_exit=True) + elif "missing_keys" in error_message: + missing_versions = error_message.split(":")[1] + msg_box = msgbox_frame( + f"配置缺失 - {APP_NAME}", + f'\n云端缺失下载链接,可能云服务器正在维护,不影响其他版本下载。\n当前缺失版本:"{missing_versions}"\n', + QtWidgets.QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + else: + # 其他错误暂时只在debug模式下打印 + if self.debug_action.isChecked(): + print(f"获取云端配置失败: {error_message}") + else: + self.cloud_config = data + if self.debug_action.isChecked(): + print("--- Cloud config fetched successfully ---") + print(json.dumps(data, indent=2)) + + def toggle_debug_mode(self, checked): + self.config["debug_mode"] = checked + save_config(self.config) + if checked: + self.start_logging() + else: + self.stop_logging() + + def start_logging(self): + if self.logger is None: + try: + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) + # 保存原始的 stdout 和 stderr + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + # 创建 Logger 实例 + self.logger = Logger(LOG_FILE, self.original_stdout) + sys.stdout = self.logger + sys.stderr = self.logger + print("--- Debug mode enabled ---") + except (IOError, OSError) as e: + QtWidgets.QMessageBox.critical(self, "错误", f"无法创建日志文件: {e}") + self.logger = None + + def stop_logging(self): + if self.logger: + print("--- Debug mode disabled ---") + sys.stdout = self.original_stdout + sys.stderr = self.original_stderr + self.logger.close() + self.logger = None + + def get_install_paths(self): + return { + game: os.path.join(self.selected_folder, info["install_path"]) + for game, info in GAME_INFO.items() + } + + def file_dialog(self): + self.selected_folder = QFileDialog.getExistingDirectory( + self, f"选择游戏所在【上级目录】 {APP_NAME}" + ) + if not self.selected_folder: + QtWidgets.QMessageBox.warning( + self, f"通知 - {APP_NAME}", "\n未选择任何目录,请重新选择\n" + ) + return + self.download_action() + + def get_download_url(self) -> dict: + try: + if self.cloud_config: + if self.debug_action.isChecked(): + print("--- Using pre-fetched cloud config ---") + config_data = self.cloud_config + else: + # 如果没有预加载的配置,则同步获取 + headers = {"User-Agent": UA} + response = requests.get(CONFIG_URL, headers=headers, timeout=10) + response.raise_for_status() + config_data = response.json() + + if not config_data: + raise ValueError("未能获取或解析配置数据") + + if self.debug_action.isChecked(): + print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}") + + # 统一处理URL提取,确保返回扁平化的字典 + urls = {} + for i in range(4): + key = f"vol.{i+1}.data" + if key in config_data and "url" in config_data[key]: + urls[f"vol{i+1}"] = config_data[key]["url"] + + if "after.data" in config_data and "url" in config_data["after.data"]: + urls["after"] = config_data["after.data"]["url"] + + # 检查是否成功提取了所有URL + if len(urls) != 5: + missing_keys_map = { + f"vol{i+1}": f"vol.{i+1}.data" for i in range(4) + } + missing_keys_map["after"] = "after.data" + + extracted_keys = set(urls.keys()) + all_keys = set(missing_keys_map.keys()) + missing_simple_keys = all_keys - extracted_keys + + missing_original_keys = [missing_keys_map[k] for k in missing_simple_keys] + raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_original_keys)}") + + if self.debug_action.isChecked(): + print(f"DEBUG: Extracted URLs: {urls}") + print("--- Finished getting download URL successfully ---") + return urls + if self.debug_action.isChecked(): + print(f"DEBUG: Extracted URLs: {urls}") + print("--- Finished getting download URL successfully ---") + return urls + + except requests.exceptions.RequestException as e: + status_code = e.response.status_code if e.response is not None else "未知" + try: + error_response = e.response.json() if e.response else {} + json_title = error_response.get("title", "无错误类型") + json_message = error_response.get("message", "无附加错误信息") + except (ValueError, AttributeError): + json_title = "配置文件异常,无法解析错误类型" + json_message = "配置文件异常,无法解析错误信息" + + if self.debug_action.isChecked(): + print(f"ERROR: Failed to get download config due to RequestException: {e}") + + QtWidgets.QMessageBox.critical( + self, + f"错误 - {APP_NAME}", + f"\n下载配置获取失败\n\n【HTTP状态】:{status_code}\n【错误类型】:{json_title}\n【错误信息】:{json_message}\n", + ) + return {} + except ValueError as e: + if self.debug_action.isChecked(): + print(f"ERROR: Failed to parse download config due to ValueError: {e}") + + QtWidgets.QMessageBox.critical( + self, + f"错误 - {APP_NAME}", + f"\n配置文件格式异常\n\n【错误信息】:{e}\n", + ) + return {} + + def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path): + game_exe = { + game: os.path.join( + self.selected_folder, info["install_path"].split("/")[0], info["exe"] + ) + for game, info in GAME_INFO.items() + } + + if ( + game_version not in game_exe + or not os.path.exists(game_exe[game_version]) + or self.installed_status[game_version] + ): + self.installed_status[game_version] = False + self.show_result() + return + + self.progress_window = ProgressWindow(self) + self.start_download_with_ip(self.optimized_ip, url, _7z_path, game_version, game_folder, plugin_path) + + + def on_optimization_and_hosts_finished(self, ip): + self.optimized_ip = ip + self.optimization_done = True + if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box: + if self.optimizing_msg_box.isVisible(): + self.optimizing_msg_box.accept() + self.optimizing_msg_box = None + + if not ip: + QtWidgets.QMessageBox.warning(self, f"优选失败 - {APP_NAME}", "\n未能找到合适的Cloudflare IP,将使用默认网络进行下载。\n") + else: + if self.download_queue: + first_url = self.download_queue[0][0] + hostname = urlparse(first_url).hostname + if self.hosts_manager.apply_ip(hostname, ip): + QtWidgets.QMessageBox.information(self, f"成功 - {APP_NAME}", f"\n已将优选IP ({ip}) 应用到hosts文件。\n") + else: + QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", "\n修改hosts文件失败,请检查程序是否以管理员权限运行。\n") + + self.next_download_task() + + def start_download_with_ip(self, preferred_ip, url, _7z_path, game_version, game_folder, plugin_path): + if preferred_ip: + print(f"已为 {game_version} 获取到优选IP: {preferred_ip}") + else: + print(f"未能为 {game_version} 获取优选IP,将使用默认线路。") + + self.current_download_thread = DownloadThread(url, _7z_path, game_version, self) + self.current_download_thread.progress.connect(self.progress_window.update_progress) + self.current_download_thread.finished.connect( + lambda success, error: self.install_setting( + success, + error, + self.progress_window, + url, + game_folder, + game_version, + _7z_path, + plugin_path, + ) + ) + + self.progress_window.stop_button.clicked.connect(self.current_download_thread.stop) + self.current_download_thread.start() + self.progress_window.exec() + + def install_setting( + self, + success, + error, + progress_window, + url, + game_folder, + game_version, + _7z_path, + plugin_path, + ): + if progress_window.isVisible(): + progress_window.reject() + + if not success: + print(f"--- Download Failed: {game_version} ---") + print(error) + print("------------------------------------") + msg_box = QtWidgets.QMessageBox(self) + msg_box.setWindowTitle(f"下载失败 - {APP_NAME}") + msg_box.setText(f"\n文件获取失败: {game_version}\n错误: {error}\n\n是否重试?") + + retry_button = msg_box.addButton("重试", QtWidgets.QMessageBox.ButtonRole.YesRole) + next_button = msg_box.addButton("下一个", QtWidgets.QMessageBox.ButtonRole.NoRole) + end_button = msg_box.addButton("结束", QtWidgets.QMessageBox.ButtonRole.RejectRole) + + msg_box.exec() + clicked_button = msg_box.clickedButton() + + if clicked_button == retry_button: + self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) + elif clicked_button == next_button: + self.next_download_task() + else: + self.on_download_stopped() + return + + # --- Start Extraction in a new thread --- + self.hash_msg_box = self.hash_manager.hash_pop_window() + + self.extraction_thread = ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self) + self.extraction_thread.finished.connect(self.on_extraction_finished) + self.extraction_thread.start() + + def on_extraction_finished(self, success, error_message, game_version): + if self.hash_msg_box and self.hash_msg_box.isVisible(): + self.hash_msg_box.close() + + if not success: + QtWidgets.QMessageBox.critical(self, f"错误 - {APP_NAME}", error_message) + self.installed_status[game_version] = False + else: + self.installed_status[game_version] = True + + self.next_download_task() + + def download_action(self): + # 询问用户是否使用Cloudflare加速 + msg_box = QMessageBox(self) + msg_box.setWindowTitle(f"下载优化 - {APP_NAME}") + msg_box.setText("是否愿意通过Cloudflare加速来优化下载速度?\n\n这将临时修改系统的hosts文件,并需要管理员权限。") + msg_box.setIcon(QMessageBox.Icon.Question) + + yes_button = msg_box.addButton("是,开启加速", QMessageBox.ButtonRole.YesRole) + no_button = msg_box.addButton("否,直接下载", QMessageBox.ButtonRole.NoRole) + + msg_box.exec() + + use_optimization = msg_box.clickedButton() == yes_button + + self.hash_msg_box = self.hash_manager.hash_pop_window() + + install_paths = self.get_install_paths() + + self.hash_thread = HashThread("pre", install_paths, PLUGIN_HASH, self.installed_status, self) + # 将用户选择传递给哈希完成后的处理函数 + self.hash_thread.pre_finished.connect(lambda status: self.on_pre_hash_finished(status, use_optimization)) + self.hash_thread.start() + + def on_pre_hash_finished(self, updated_status, use_optimization): + self.installed_status = updated_status + if self.hash_msg_box and self.hash_msg_box.isVisible(): + self.hash_msg_box.accept() + self.hash_msg_box = None + + config = self.get_download_url() + if not config: + QtWidgets.QMessageBox.critical( + self, f"错误 - {APP_NAME}", "\n网络状态异常或服务器故障,请重试\n" + ) + return + + # --- 填充下载队列 --- + for i in range(1, 5): + game_version = f"NEKOPARA Vol.{i}" + if not self.installed_status.get(game_version, False): + url = config.get(f"vol{i}") + if not url: continue + game_folder = os.path.join(self.selected_folder, f"NEKOPARA Vol. {i}") + _7z_path = os.path.join(PLUGIN, f"vol.{i}.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) + + game_version = "NEKOPARA After" + if not self.installed_status.get(game_version, False): + url = config.get("after") + if url: + game_folder = os.path.join(self.selected_folder, "NEKOPARA After") + _7z_path = os.path.join(PLUGIN, "after.7z") + plugin_path = os.path.join(PLUGIN, GAME_INFO[game_version]["plugin_path"]) + self.download_queue.append((url, game_folder, game_version, _7z_path, plugin_path)) + + if not self.download_queue: + self.after_hash_compare(PLUGIN_HASH) + return + + if use_optimization and not self.optimization_done: + first_url = self.download_queue[0][0] + self.optimizing_msg_box = msgbox_frame( + f"通知 - {APP_NAME}", + "\n正在优选Cloudflare IP,请稍候...\n\n这可能需要5-10分钟,请耐心等待喵~" + ) + # 我们不再提供“跳过”按钮,因为用户已经做出了选择 + self.optimizing_msg_box.setStandardButtons(QMessageBox.StandardButton.NoButton) + self.optimizing_msg_box.setWindowModality(Qt.WindowModality.ApplicationModal) + self.optimizing_msg_box.open() + + self.ip_optimizer_thread = IpOptimizerThread(first_url) + # 优选完成后,需要修改hosts并开始下载 + self.ip_optimizer_thread.finished.connect(self.on_optimization_and_hosts_finished) + self.ip_optimizer_thread.start() + else: + # 如果用户选择不优化,或已经优化过,直接开始下载 + self.next_download_task() + + def next_download_task(self): + if not self.download_queue: + self.after_hash_compare(PLUGIN_HASH) + return + # 检查下载线程是否仍在运行,以避免在手动停止后立即开始下一个任务 + if self.current_download_thread and self.current_download_thread.isRunning(): + return + + # 在开始下载前,确保hosts文件已修改(如果需要) + # 这里的逻辑保持不变,因为hosts文件应该在队列开始前就被修改了 + url, game_folder, game_version, _7z_path, plugin_path = self.download_queue.popleft() + self.download_setting(url, game_folder, game_version, _7z_path, plugin_path) + + def on_download_stopped(self): + """当用户点击停止按钮或选择结束时调用的槽函数""" + # 停止IP优选线程 + if hasattr(self, 'ip_optimizer_thread') and self.ip_optimizer_thread and self.ip_optimizer_thread.isRunning(): + self.ip_optimizer_thread.stop() + self.ip_optimizer_thread.wait() + if hasattr(self, 'optimizing_msg_box') and self.optimizing_msg_box: + if self.optimizing_msg_box.isVisible(): + self.optimizing_msg_box.accept() + self.optimizing_msg_box = None + + # 停止当前可能仍在运行的下载线程 + if self.current_download_thread and self.current_download_thread.isRunning(): + self.current_download_thread.stop() + self.current_download_thread.wait() # 等待线程完全终止 + + # 清空下载队列,因为用户决定停止 + self.download_queue.clear() + + # 确保进度窗口已关闭 + if hasattr(self, 'progress_window') and self.progress_window.isVisible(): + self.progress_window.reject() + + # 可以在这里决定是否立即进行哈希比较或显示结果 + print("下载已全部停止。") + self.setEnabled(True) # 恢复主窗口交互 + self.show_result() + + def after_hash_compare(self, plugin_hash): + self.hash_msg_box = self.hash_manager.hash_pop_window() + + install_paths = self.get_install_paths() + + self.hash_thread = HashThread("after", install_paths, plugin_hash, self.installed_status, self) + self.hash_thread.after_finished.connect(self.on_after_hash_finished) + self.hash_thread.start() + + def on_after_hash_finished(self, result): + if self.hash_msg_box and self.hash_msg_box.isVisible(): + self.hash_msg_box.close() + + if not result["passed"]: + game = result.get("game", "未知游戏") + message = result.get("message", "发生未知错误。") + msg_box = msgbox_frame( + f"文件校验失败 - {APP_NAME}", + message, + QtWidgets.QMessageBox.StandardButton.Ok, + ) + msg_box.exec() + + self.show_result() + + def show_result(self): + installed_version = "\n".join( + [i for i in self.installed_status if self.installed_status[i]] + ) + failed_ver = "\n".join( + [i for i in self.installed_status if not self.installed_status[i]] + ) + QtWidgets.QMessageBox.information( + self, + f"完成 - {APP_NAME}", + f"\n安装结果:\n安装成功数:{len(installed_version.splitlines())} 安装失败数:{len(failed_ver.splitlines())}\n" + f"安装成功的版本:\n{installed_version}\n尚未持有或未使用本工具安装补丁的版本:\n{failed_ver}\n", + ) + + def show_about_dialog(self): + """显示关于对话框""" + about_text = f""" +{APP_NAME} v{APP_VERSION}
+原作: Yanam1Anna
+此应用根据 GPL-3.0 许可证 授权。
+ """ + msg_box = msgbox_frame( + f"关于 - {APP_NAME}", + about_text, + QtWidgets.QMessageBox.StandardButton.Ok, + ) + msg_box.setTextFormat(Qt.TextFormat.RichText) # 启用富文本 + msg_box.exec() + + def open_project_home_page(self): + """打开项目主页""" + webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT") + + def closeEvent(self, event): + self.shutdown_app(event) + + def shutdown_app(self, event=None, force_exit=False): + self.hosts_manager.restore() # 恢复hosts文件 + self.stop_logging() # 确保在退出时停止日志记录 + + if not force_exit: + reply = QtWidgets.QMessageBox.question( + self, + "退出程序", + "\n是否确定退出?\n", + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.No, + ) + if reply != QtWidgets.QMessageBox.StandardButton.Yes: + if event: + event.ignore() + return + + if ( + self.current_download_thread + and self.current_download_thread.isRunning() + ): + QtWidgets.QMessageBox.critical( + self, + f"错误 - {APP_NAME}", + "\n当前有下载任务正在进行,完成后再试\n", + ) + if event: + event.ignore() + return + + if os.path.exists(PLUGIN): + for attempt in range(3): + try: + shutil.rmtree(PLUGIN) + break + except Exception as e: + if attempt == 2: + QtWidgets.QMessageBox.critical( + self, + f"错误 - {APP_NAME}", + f"\n清理缓存失败\n\n【错误信息】:{e}\n", + ) + if event: + event.accept() + sys.exit(1) + if event: + event.accept() + else: + sys.exit(0) \ No newline at end of file diff --git a/source/ip.txt b/source/resources/data/ip.txt similarity index 100% rename from source/ip.txt rename to source/resources/data/ip.txt diff --git a/source/ipv6.txt b/source/resources/data/ipv6.txt similarity index 100% rename from source/ipv6.txt rename to source/resources/data/ipv6.txt diff --git a/source/icon.ico b/source/resources/icons/icon.ico similarity index 100% rename from source/icon.ico rename to source/resources/icons/icon.ico diff --git a/source/resources/images/After/voaf_ga01.jpg b/source/resources/images/After/voaf_ga01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9412413369a9195127b8ed0abb632c8e42bcfbd0 GIT binary patch literal 165408 zcma%i1ymK?_vlFuVPpYWB8j}jD=Mp5^1!P!H|LWN z(FQ0^*gVn3qxK%fe`r5_h%(td>Uep9s!SEzZKG*@1h-oA2Ukj}*|stCu)jn6C1zm| z5~UTu6~j*la7+NW`4E>0Y{%DPQLrgkz=Q?Z2a);bKY#`M{!$z6FZs^!dgB(1PWKL^ z3u3+c*7mN|lBMW)mSUdk9Eh9MG2XJMdj;37wxYzozA;0Ah$ywIHhRs#^v zkTER)d#Shj0q{gH JPADkAKv74W3b55tpgV#G71IT; z1X4A4G>B*~(Le!cI1MAwj`@mi0z+?3#9|uy@zk44)#R4Rr~dvo=PNsj5z)3OVV3;v zM8SA(^`V}SPhG=LbEq%}mbK8k6uZBNHPA9X$5a3Py?LO~Xq#AsVwkkJWP7XPU^9O6 zyjSN!Tp6Lbr&LaC@E9hRqa@2LXgdFrq{pCgN-YU$` Ks@1$~SM iMhD?hazNge!udJmdWVZ#d}6uPsg7memIdh8Oryrn=MLUK*@va9j^Xz-ySoz zH@mnu{qU=)jb<{*^fGHqNtB{OjxtvmjYZg`+y+k!zPpJbK=9%xZb0d|`Z+@l(;9>| z(0FJti#qL|jbCW+Y_&YkL|x5V;e(0`;(424`d817 Q@)i=ENXo$C03^M?Z07<` zTKdRwlAwmN$^t;)P#h4zsf3A9jQE%79=S5_FV})k+d!Qb2y$2(;5r5%`~;XmQ9=Q< zv8H>$gLgx?d+-w=r`de o zU=tpp5I`3E)Q>v>0kuZ#<3qSwK&2K#G*-?~^Q7YCrJM(w+g$&CT$py`=Ek?1t`=zZ zKyASTAexzQibF;c5#jOMlznrnoO!1)ii?@1D)_0%`d$1DJ*UGQBhO-8IsctM{E1Th zDgAAI&|auORQVg*GR~!|iO95_`d92lOUt#M%+QL|tlfx(bUOj@_U=C=WP9hy9+P1u zrg6L%HMaF+cUNj1N+B6Yqk;t sk?jog2^i z(I^VH)g%`DrJDvEVMjT_ V=Z>F2kY;xe%Et0PZTdJUQf}5Z8HX<=4L|)({^wt~k&@qbLl94q%jkLM4~Q6$K!k z1YDJn9;K29Wd-ajx)jtZm*uq*D6&9NgzN=iP6uF4!@AI6h>t`X14k4%$oE(Cx;}~} zsCloGn|#kRkJ{uDi$8gKvgcu#?BuJpts^fM9GwJ_LO5H9Cq|t7jh3f`pMF1Cx;x}s zefgBvm_)cdK|zMEjf=L7{*B+Y@5!DGp(8ixl0y
pW7 zA2+ZPw%G5O5Sr3=Yf;)-zgp`nm6B{RT75&DE}vX{(kRZ3V>23G$WkBKmxk^8Ri#E@ zt|`JJ_hep|z~9w&&3U-MRFa9=v3zG%30tPKq0*z%*$&eN& zgQ5s-2ed*Vi+=?W;@|GV2>?F}qX`s3kQa|8>R;8*0y;RL4K{=}en`z-NCeL#Um|!d zCV8N#Oi3Mb rm ztKctKo&_POBrVUBC^(c@g2Y*XG613t k36u~><^e7ad zJNqF`9h9b*UofCJajMFEs{5=aHN{_XGj89T#4ns(5R6JHm%`3u!-gB-J@!Rqrzg^^ z@3OsI_SNj}!tDd_#jMh_I7tJc$86;{cUb|kOQ-QwlXN+oct{mxLaKcG{EqtByoxJ^u)-5Qp(k>TpLEgQrT#M4ytY8M{#|$LEn?qftd)rW z0BrdQM+ VA<$0@(JY@R&7#B;B~bU*AuCEj{@z@vk`S=+Zn*dcbOkKYnCDL9ra zBFLOe3405BAXtdBzNj>`h+^7KV(65g^LVtejj>tAR8O-nXEKU-g(gxRX+}+GOi*J- zyZM?ZLB~(dHhxB!t^UrW>8&jf&bpYuXoQ?4&DEy}N7}7wQtFJf8IB{B+4mBP4d!{g zoHu(yeb+6O=3> &jYh8 qL%^x3nY|rng;+)is30| xeFi$&Uj1}?=kARf2RTtA zsGU-2eR}sbbqVcu9;?-<8I3t*US;$H8_hLC0MB;!Yl@qqJN `sf)?9PoGq@08h+TZh%|4a=WY11 zwZ!e^enHZ~lZY@lVy@5r-Ay+%UFTZSXJAC$F;<1q`0V6EoTmfzhbkfL^;Pi}45Th& zU;M(=MAr{?x31)fe>C~)4HO@MrH;|lf@oWh?5mI;8jy8whgXth0%eOl>U<^iqSJG( z&Hi+D;n(5OY2hRbAwK)BuB+3KN4&FP0u L81tJsn4-|&be^fk)W3c?*Q6(5`piX*I-d?pk- &&PY+YEp~~ zCZX{FRIfky+rCq+zYXhUdN#>Rq*^eT+|~SKnFdbg@@{n8k{lLJPEHDjLk9S_WOTXm z(Oj7lT6Fc#*YA%mW#7ug>uC7Wqq7&y?7tFHn!1cXktB(2b)xgwQYnvMdZwxQjz6TF z$d*@PCwGLeVS9FJvW+pb&E>*v{e4nHVFtWzBJ1tmGnu7?XkV`fpbnJVq!A0dQFq2) zxq=&7w=TR?^GHAaB;IBgKPyoErNw`#-o5nkM^XaIGwhnmYE2!P7~Zw&I^U&3w}H%H z?np;Pv5aze`hC}#BG!>NGHG^8KRRtegL$jdqL(pj7)^qO0Y{M@pzkf6)7VM-!7gOF z->fzlVc~Sp;$`~)jJzS@T)N8vdQsr@m5F35x2=_JF5P#Q9s%)QB@s^a-aELg&P >I3C;oZ*D4tz5y_+}o zR~TuFzXNI{qscm!9LpYn+Wxw?4fG8Mg5 xC!5B zNl=^~Pxc#Lc}_eupK7H!A(NWQ=Pde}qt>}7&7`^@{e?(F$O9m6*~iN>CC|xn;qiSt zN&O(#gUkhkwfE;(D%SXV;_LmGl&6=)q$LWn!j^BBN(Le^HIAyX*{{5(~3CC5xxw zb1@0A4-pH9AY1BUL#m!9r5nh(dzADklqH}1=+p1JZuC(xXkkeEXdr*Ct71Cbs5KwK zDHMqj25ihV(XV;Wx?4}KT8-7_lY9!*s3@5(xr-aq{d}L**Ee(bju&6PotLmm-mVMw z;)rTB@6u-FvuatDQ|4h1{e{4#o=IKV{TjCA)aEB{Xs0L9(nV(TRE@a$=BpKd&O3|O zRe|5eFG3Njkf!wFSvB2`Gqp-0zC&S%xB 8i_i-Z4C{*IBIySXf&pCp!1s^7 z01J{DsG-29>szQ46bkFH%B3!+Bc%8n?*v0{vlhT&Bov L0^(-Kd`SM%};UbP;rPj!UB&Nk3mnG< mVJQ>F#=gta7T?Kb1w6h3r1y;Etf zW;g3V-iZDjVS9L6GFI*>|0ZfFb=S(Z&N|UxvSUxJ#?SnR?Qa3n7w^*WQ2{b%%LbCb z&Zpdz6*(F!3VI*27B5rSO@|Vft9i|=J-#kGvuHD1E9k4H;_IeVFqE2k^PJL5&sI@= z8LNLCCdDyjCZlDVkYe9wZ{)AEu1v!*)Ds!}SruP_4UV~3xb8sTWs5=siJabU5XZKO z-S(Q`_jo9^*xcDq3hr9fS#|MExptR{eX^?l!El!_v{oB)to3u^g7}0Y#Yh6lk7{JQ zG?r&X=`DE0bIxxSOZTq1@y^c4?4B*zE1Z@4pQ61P7KzGL(lHK$=2I&Z8tejwyBPd3$Z%mwD0#$1(P@o_ Qj6Y|E|Z@{PUnJllT6vd;0*(|R)rtmm&7@<4vrVWtlhOH zvstt6*AnJcenhlt5~!XC@ns3LDp%3m0Pv}Kvs#?4+V!@t%@!~8N=1$3LTR`r2lMhj zMVVG|k!PvZCazmVXw>dqvft B=2k3K1)DG0zBhU~RI(676d#LXiw; =FZk`Cs6S3arZZ}nkgYT)Y>UGCo};>BCcoICgZQ^NTZ|>X z6p;lMs8&%XkSrgaCQ$dK=LbRxzJcCV;2?e$%}Zqk01<#wr0kvXWP7EyaG)j}vzOOR zJcK(*XEajfT{7}#plIn}vO${}cM)fyVf*IB@9k?0Igcg9Q?{q6ZTzz>?iwcN(TXoW ziAPYnLkxx}*eXq}J)UV_{^H*5ZjL^ y?x|EZvY#ao* z;`?~seu^xLU=7&}(O%V#we?G+YBlhuMK!o5YX0WcX3#M2`dfplqJKL r!!W>VH1E+K@$NVxE zsh8Z+^R-yCqQuxpUcPNi9dnJvNPP3P?XB~#Mt*`Hu6FwU&Q)$sCdwKGjjoM%U$1)_ zOh`W8VFd$xJ`2B}M{<(AeiOsoUE}`%a892!JOJ)D`2LwmGky|lb*4iJbw;Km_T1ED zFHIXDMVn~02b|yeaaD(`$`VrVQWgg+i|r@A3Qx;n)h|p9wx;ok9eqDWJ+3f#03vcL zVwuJ7q=#=mw#u@{VzZLuys=k_BG0Zr99OQsB#f5w8Cr28t-1CS!Zs@zJbgzxsW?_; zR$Zd2-@@BGe3253^l8txd%mboO-GB{hMb<+@;zg4%!z2cr{pT4n8qtb)cG`xS7*|7 zKfMIB^x_;(X^h(ZXNfO<&~Dbs;yNBQYT10~kI<}@>lU<=4?0`z)Ob_&t}dK$@z1#M zCB~s_i}C=dbhrd_NW|+6O)4N{0<7UTSmI9Ayfv2@@hF>7C6gr=gr+{N^E4}Lm!pG1 z!c=YucPB)Iy;g++wcnq%^w#YiRk*6D HT^7`O~x-EsU0AxlkZCMaItovXdYxVbmS{l&=f=6CdKmhas+ zl1vlc`#IsyPm0PKIosnpD)t=sebwF_x9P98Z>kzLOA&0&Tes=@h$!1Jt=4FK0=+Kc$wxKmFOLZ-$Mj+_OxFLHR6(}8dCjl<0|s@L zdJC0 vUq zfdLj$BMEI0DU&<&1qZ{i&ob0s9e#cYbTaXMZZgyL3o(O$;D^Q^PSd!hp{d^Dy*O%j zO=tF5lT7L@o8Pf%&7biYsfS}7L)Gl0iChrt0% =J^+X=6 zIIbO<&;T=`*j|;9?<3KJ15W0tMyUZzqNMV6Vr2dYMKhN0jOAGyKN#5Q5wuu;s1Uv6 z$;r9W=O!J9y-%eOd#j3eCqTZvYMSCrD ER!m|{Z&?yVtkAOD3FAB02)=f((tF)ffgM?RExMA`Z z72B7QrU-MR8E%!N=eE{e&n`_l=1G}p3;VAsXnfEJi<{&a=f$jz&Stps+^x^K-Z050 z{St3qsA;)&G?y!tk1Yr^5%sQnBHxysRi(bfHjsF(GO^^iyF4n+Or_8IRlEs$Q4TS% zDAYj}+np~9u{3fK5LeZub{?ruMj42Vf6;`~P)LM8oqXZj_W*QH-HM?+>aqNjrL|a% z8RtK^O(>GeXQQvL;%dAQg0QjqqYam)K6`aoJZ1@$zZ(zy_1xeHunM 1$%9(}kz&MW %}bwQg)bmQ629qTFzBhG`>jAG!%_Zd5k<^X2&Pd2|$)yq3|g6_EAkx5|}j+oT7FL zhcm&&7MOiP{t5BJ>mKcs?>XPPGoSc%%?=iBZY?QEXqiUqe~JzZQdD$kXtf%#(N$!~ z#HypD?t?Nk+%aF<38q&3Dm}33Zf4`NIBO~-c(R)X)B$c_DGUc>(Gq}v^XT3h!9ed5 zP)~l$!w smi2C7YG^Gb#I0@RQ5k%;04*Ju14cN3^(S)STPdz`fvM)oNvv zkM`VbWy3l5W^dX~((~N|mZ+p8+kK4s6$iex@l2 tnH z1gtU}x5sApRsOjDmh7`VC?7W L zn7Hjn+uGNn#gri{pP6_L#yr1imKX$y;;XSqA#^f$duj-k#53DrF8bJJdMuf~*|Otz z$?ap1lpB;uW3e;-9<5pC-WLzmcq6&l^Gdy3{HdzRANpF`v;uAWG;)QbwEU&1TdyZZ z;ixZTi76VT!*Gg^i@x(K`j*W7xGi{FHa!rxE0E})EAy%K^JiZ#(-EtgcKWNka69{9 z65Bb_DtEH{)sy44W3=m+R8}OA$)Dm^bHe2JZ&H_LJ1WjOmJVk0wY=mQY#0P=GzqIs zdSfE>&vTTr(sZIxPFDtWbmCUW>g~Eym4BLc?4eSR^zQb(=x%bBwVxO3*sYL!07AzZ zGPZ9&KLAa;1IJ9?7@YcE;?H(}TM9jf4tZOb=#9TlsBepT!-r(K k`y0) ztW_=1yklbAP18V*5{^JxSJL2a^ZBhb?*m}dTlHNdQajVqbje+KN5@S_Gt2^8jUi@> zGD>_yGE|X%v3je30O>V`xYvnv(fM-L-Ox`4j-?l?7rUn6%*@a6^;-4ChBMSB+f~9F z(%c0YO$HKNygZk}IyNxKSNIZ2SIdc4yc&K=ogIzZ9L%hekiMsti+FAj@`Qo@OWTs4 zwd7(!Fkb<)vWA9f2fK#Bq55#+9I->)j6V-$a|`i~zv?!jADk{}p5pfU=a5gV2c9Vg zy^8LX8XN5l)}DSH69Wr93uE>P#aAjFda`dv?qip4+>PVVcGb2s6a|e%wXkG@CO<6x z2wI$puS?LMj7nOn)Ile;Ik!KdH(+%p;4W!)e%oWKRjE)drIE@pN!9=KT-K{ZLo|~1 zk2bhJp2O+;;%#Ofy$gfoLj%Yfk-!^5utua77Xtd77SKXa*CIut601PQySWl<8KBG5 zn4(qsMvBz-;^JD|@T>gKg%FR3DYxZ9 j`B+Y*!~)ima6e`@z}gYRWiSh>ANWy$(F&+{x$kECojb^eU_vo4 z5J4tn!c-sI5=YZ=PsP}sm#6w^t0jZ(>%D_wcoI#rw^#DTVOo$;H+^SlU#jFZ&z`=< zQB%?PZ?|2CnP0q@xk>y!?be rmkXaHwrC9Q9Q$R6b*H zw8jYa_wT32W@17y4Q@BK(!wu@zUqeTq`eisE7aJHQxabJ^5lI+yZR=S43( 2p&B%D@ZNrb7rqtwhTgG*-NlnA^8Lp4L==eyBSPbvci31|k{Oq+B_gW-DZ z2+560=iqY;PfPkjl%ne4X$jtAw}W4mkunOwDggl7X6(j!D?|G?F|OcjQ=6t9)WRQ= zE}Mr2J4S- cPGV*_wJI9l*Jr0NU=5K(6T4rAVWtrENGC!D6n}_%cU=ZaiPf(Z zxGy@neIz=8`&`!QmXfX0YI0vo5xuDPWw+3dd~5lNuuU%oeiAw7t0t>hZzftbzK0?l z4}m<=MUcT*A^T!n%8OHI7GF;`TV|n`iPI=CJ6CIn%3J>+bAi{?E80|^*sZ)|H~#2% z2sV2x3+>K!Y_j+-RBW!6Mx83fMj}n#84d(Ye^tZ1@$lN`yxFz=^2N+gcCPPNjor(@ zsB3<|b*f;$%oS?WA&ap)kErcLKgmwgiUpI-8FYMxyb`E78HtVgRa@CKoBUX|`c~(k z!rOs`bElbTW=66dn|TxMUx*rV-?6780hh~+=S`Kj-vUS7E55DBK7V1`gYQ=BjYe+U z_pzTto<>g#R)rMXW@_H^j^bA?#kwypz-IxtJbGl1DHt5WqX%jGk17S7G*npN6pS?g zAAj CUb*y(VLu_7$aZ<6WxY85$xp{#}4>E!qM zivj8Zs!Ej4s>P*-LELDbDlXM3BYxS7t|B`K-)I`zzeLyS*Kk;2O~In3q(F`98U#4P zCI2l`Ktolc4#{WXNCi-pDZo003T)xltebryzkG%cLA9LebT;i49=4-YVLE5-gJIGK zpnz6Ra6^#FmEoIb5D!f#6`Z;_i6C|Jvc}c4+V{B4-R}xxP4;hCyY$AlJ4SifZgQoQ z=r?&2HPZ{+q^Aohb0>9V%Oaiqb|*ZWkMO8kIM*!?8TYqU2TRZYjO^V#06%}u^O^ie z)TcmBN h$ zQ=u_~5Ayf)h`PDB+ QGa2MBvRPgYy`!Z7kf! zS8`64lWGZ_Ra@T7YM1RR3S&H5$te?w3>CzwQ90M+r{pSGOI5jUTuPO4@&m*ky2s_O zAAr8*`-Z!@GY;k^(+8m8 z4}1Uyw=Vn)_F%4lyEARFi1=W4(tBK#Y9?H38pr5oM2o|dIKI>=<(aP!lA9;13d&E* z`+XJ;;z(0G2KJ+}x)xbacIIh@KN7~8j`j?OsXkYQn ~P71SkfHXKUu|^4LQ}t0~Z&Wy=NCh?0*h8akX!% z$P-(34jRw(IrPL%wzw7|nIx9W1BH3R`px14rn^Xj1T&g5jl)Tai$j;kCrAAH#dX?s z-&n65JZU<|Uz_x2cmJI9PNIAHOPCgQu(Vmiuf9^5C 1uKF zarU2mS@8EGlgBQ7YRR>{3hs!#azH1aJ$!O)U_;XUZhNg}uxq!iVsJ3o({Zq8JZ(1i z`J6`WO^Jk1y85AlpMSIVqOxS&o|cg2UCsE?sZQZdNYYB)^n~a^*|*PHd0;jo3UGU> z$P^{u;H=xGzmuq5PDP-I1;ce0qTVk KN8`GCkQ{Grn`5H72IFk+M1=X zrL?J@lV+>Bp{)nw`Rdy1{)&$J7{qkg_oC>Ap4(=I?R{+EsA=Ga$O;`vhlRtr+Eq 1;UkBECP6l2oWFi zp0KVg4BP-BFh2u_4iKH8?N5T0p VrHi2T)6WZ!2BD0h)8}Mn`MjxZJC-60YqLcsA4$w}nFeqB4}NQ<^=?vA z<2;+f!oI;KTw4Ejutbykgi)pKEU5AzZZ7orPO9Asr`7g2wpSoGS}JO-JiFeMY)ipT z{8Dg5lEKHv&1A*rBOXmXmFzjjnlQ){VXEAHhTAP14Ilov6{_vYPbY-iXGl>FQ7~}~ z$32n8M}1!E*BlYI(eK ~f*yiP$mTuX#UPc(ifuROIhSI8j2f_926zh=j`c2k8KM zW1NHpUv2C>oc!X(T3GJ%U@UiFJ8SQi$CnJv@9vQ ;-YdbyRKK@m{D+Yw z&kw_aGfq*;2;2B|ECu0iA!eBdDD0i__LlAKJH?2_w}G=3#7!}K20x2H_ihz_(QA@q ztX0Q$En9EE?)4!1J2CQWEtEGiRtW|@2PG7Z-AuN=a(h%hG;H@gejQQ3J&}tH+$~Mn3ImO zqp(i1!cMl*$UYSLZq SH!Jo>Na|vA^U4qTp>>t4~jnAN@$+RyKj&(Dh6omIGI*`$v>AI<1 z9gTU*r8pit)ju vc{L<Ka5wr96+Ly@WL-lq%4%j(LQ5-Q 72&U|A1 zC!3+wR-US4k>katj}3#=_n{P#DE=1Sy3U>c2OvLf=BLV=>YJ6kRbqmCihBzi&THzs z4o8N&4iAU3*+o~OXM Z>&IaEow(gW?580gqpc4*4;f)jIJ8(ZVhbeA^dAYWBg$ z{OJt!bwZ3ivRJRJhHaj72LNGUQW~Ix{*7?JWd}gzSp)L}_~j{}@?fbyD8!J{Z}Xb{ z{evi19jL~nLRp_7g))!JhO@La&QI)9XyjeXo~&Y$U*pQzcM>E=TQG?`xBpns^U-Z= zQAqXnZAblW{KF#og<}<{eAm0Xf*c) *MRsdC`dqbY`c{Sr??yX3_q;pK
oWaB%7Ah)AWqYIf$=vQ|z6krGg^O z@OI_IErY*)-#<~e=Pc8*pk^*iW(9kViBIa+%G3BpbC)x%blJ2Wh2(28gS74UHa@QI z)3FsvcI5I#w*xD)em(sagtfX!%)x)Yf>lPnI9=}}%{kAg?qyx-(Nb>jZ<~#bxFp0* zH_$^TcsuFYpW%-=G*=iI4SHEit_kfNPbQaeRlr}@&%M; aEw3Kz5;&)^^E zc{Pz48iZDLzPC4R6;f7{d$0ceu4bV*r|@Dn)%_z$#9j5t;OR&l3}vvClg|8mYomJm z+;WSjw|ZA#ot?Tq6+wQguuV+ {1z4G zQO7oqM_ONgv{ZSqrZ-nr_GA4#+Qv^f;P=~nf#I3Ss*2-lglqfip4mToE$3vXAD%B^ zB7YzFWF>Ek5c$;TC1#74cG=N|^=xmvBU|m-+u}Q;d$I}n3%11rfATYqFDC<8K4T`P z14x0DXh=_cyw%99f0@?H6u8>Bl|&hEW~RRw*oI`1s(AUlxx&%$8>%@deq-1D9$n`> zGPTr^-5(15Ha=;>U~$U;m0Ccwo&!0XyVch l z;(DWlAu(GqKH{wzf2D~bxO}_MY=XNNe!SFGY`kT%)s2$iV)T>Zy=E-&%uGT$7-L^Z z9%EX401)UMSuN)v!Zn=*hriF%u1=b7KOFIAhqs esnz!&&2mpTYQhk{vb3NV1LfQa5lf&UKCxu-sWhSias_T7u_2nH=|LX+j^ zd{ht-OY`V7smkn{#QF^U8D%$7H;U#>&tNv&9Q(dEWNYD3(UGX$-MmqSBlooLINi>w ziqtDp5C?0Q$q8&S3};n74DxsK6-J60T2Fb0_J=-K!5QxDheSW$**d N|Y+okD+7*k_@tB(e3ToEUfcmCyHtgdNF`-%PR#60%ja|E>}Afjd zL?tPmvU7-&&wC5~#$b3~?Dy{J6k%mMQ`cuqq%>YfC$eRkR;7R^f6~ <`MGG;?zLQ%_6?n li4j+{dN41SR4s(4 zA!B}U8ad8~c@^~T8uqcdy=8Szl3q<%F)S0lsAMAB_R6EF#i~8 E0Sj z#{6s^niBANrfxCz`KiPgSHW4%NDG6;_!`zt7=RxD&j=uE!y{{hh65M_2!Ka -5R z!Sq{)wXF%Pheb`;|f zb;S*6S$YwpdbyIX5#;qzG4QfKZNH?B)xA(p9Cq8zY;s-wuC4XFXV+tGeN##z mLwf9oz+R*t~ zZZ3DxVp}85T3jgpj&Ku@2!MxnBT<^z;lbuVy3q?@96ta=^wI IP@ P#l;!YnT{W#^ zUl~4G-A^*lWnaq$t#QlO823cKa2%1dQDi}igN;fH*kGxJHj;gH9qPIW4&N@xqcfp@f|*e6KyNf1j*qkL=5MGyzLCdaInwUa$3FKdG)xZ*c$y7hIF@wK_(> ze?c^ irEDe&g1@{beQ(z{h2s2f%KPU{6EtIPAp+;a&Nv1D|ZMGr5046^>6|=#;~d z@d(Y{uk8$3htt=4NJ*@Eig%d@DQxT6eOl}5UXsx+xGty9-;NC0g0VrJ?Dn^I&ouc% zSGdquscC vP*V-BDamsuG-mk<6j{vdGk?DaoylhS~9(3qP~efn)W2c zXm4dd`^0Um?}YD&nVOu9SBTiAI~CiNhtEVUOqg}{j$v#h-Pw!OZp=wpqe9K9j;q=~ zqnzNoR^p81om{|5b49_)y}XSM&9kbZmJ-7rCb-NoiT)Ul#i`UFWRKK6m*!_BmpWUn zKD1wbtg95{>TX^t 6Es>y4Li<1V?K%pqE; hqh$nDlfrsy7(fSNb|`w z$O6~D8%sS|o9z^XZ1V8$Jj0oCx!Uk*be`E8D5L%9z~Ob-F%gLq-svy*i(^)CH YFs(-t()7;X-_P#`{y~4YQvXn~4p}qzSM?r=uH*wZ4-* zZ>_Y9Oh&oVU%y#ruceb<>Z9v%PCsRgcg&2nCjTWetdirM2rdAAb`B;cgDC=GQ1DaP zu3V72h`@h)i0BlE(}2e&Qzyh<4N6N9ZQk9R RJ~_@-TJk-7BHi|NlZdMz!#}vnfRq?R*6aIFPnE;V4ZHa1UETXgd?87 zHO^UwSfa59$dG=k7kU$9h6wKQ@vB;=kA}b25@oCgIDEPU;55srDz9pl{HWPfYFmgk z8_}=0wT4o^$gP&!yMF6TKRo=kg1_wW!Q#hQ{S-Lknjf{LtZj8j+RDyT7{9IBW9zK< zWku3)s#3d5H*ft~VGftym{{tj%-C1+&HkQ;o>CThUrTX~ldrGV!%d`-3IdC#NYv~! zzbVsXb1WC@3_9V9hMah?%L)#t;V(cIn6?mnhWkgymHKDbaRsEeLKIBrDLmzuO)h?Y z%X6}n+C2O$!XXf+#S7@<)BAcIibL#&x+%W@|jpoJr2#=3{~o7*S%4M^wZ7XihM(ZBbFYC z*4A-1Q_*&*nAy$l7;9coS6k<^Zp&WM`Q59$-lt3(5Ge^6e ;Dnr;8iA z#?9h^0`8fC#C%Q9ctzRLB-74lPAiAnTEbdtt(MY=-U0E&-bgs}<2NlOB$m%L34aXD z+Q{c;rxwf6w~8H?phwC2G mfdiQ&joGIB%T&|6*WK)03HR%z&%Ra|2Gf-PKzKAMZUs-^8(3#ZUcb| zVDn#b3>>xpCrp3;x3l;E^LSz--12c=Ln@Mvy~65yekYp7L)UDRZB`DrZdxDQ7ZJun zt?G0yn{x8co^v??k#q;%{A9}7^j+!SWXYvH*DWKV>^B?B7cvor6;hI`^z?PJ2Y-&n zS8L3Twm;!DAL#Os-!{JFV#qwk?;mcvUPDbZN&yzdo^)bS;lD`pFDX1-T$PV0@bNp+ zDM@r`(Ts1oJu3b|H%%azj%DX4#iU!FCQbopIcYttY@Fq4R^_yNZZt2`OcTK3NNFP_ zvVWq2%R;4X6i%vzsMkz6Iy7&JF;%*fGnYLy2^y)7+;pM`aSK8lB}MR-<9r@>cGZ&s z7pk;T&)e7?eJNNg3Q4>)l1^g MPc5yu!LJB`!MUQ6waa5|Ca;8O1{qnZJB$FTUIQ^`~;ZsYJ5Ord-oZLXCg{ z#pW=I0&IpPY-+mDrZCI0?oumQS&Yw$+p@_n#s`t@n@_MFmMve%uHdZBGI ea zN}X%rD>uKrr>oaUR!w{jmp2G?H$k<%(~$DYG5gO%z*_~cQYrw*LqqKVh-zr+3gGG> z*1w?uFXe4&%Ii6@`S5$kTx4v5n}mLZfe!EJFi-6FY|FTI7&0PU?gFXhT>1%h{v77? z$+PWjFHBJVPJD (6o7lYP=(wyO+ZjVh+W;LlVG!Pd z;ikX2x xK_?%E6Yfn44k>B^JjDb#^)lm$fP3fu zxEBAlO5Urk=6O5vk~;Dd=JV>>;uBEDg6-&XgO4FTB&7nQ;=nT8sp#uK!4TkxW_lfY zK&fDM%ex-9?ZyuxRLF1nOh-C1m+6iB)Hw9=^^H(9$Vs;=BUYBd)J(uuA-VO5DVV{V zvG8__b?U>i-ErnKoEy3 6EknlxNz)RjK@NEG3izN$&vZVe@L7JHe@d?&HZ j?P~_3#e#P %6GjpdE7i)rDC2Pl&w3%3u&QD4GLoR%*f*!k_Rb75K+VmETS(lONB8_q*UTSzd zt TVT^*&; zN5xx{HRt}?(hdX$=ogsM`5i|jQM21QHpe;2)HF$mWc+Bdz$squ?d!^)r7{r&ig9p` zX?-^4-$gk{bepQSQwXg$6%(oa!wTzHusdlQ3zZ-ZsR>oalG=;%9r_jH8G2 V!0HqMaiUN5=1)a7oy$j(EyJ~= zk6%i-N3Y5pV$n;2K@}Amd>cAp&blqX=;H4R)$?}dv()e5cX(XwZw+&{3Z2c!O!N^l zn%UW$`jXpB9k*PFR(F(5RNb7&?IlgF`@@mbtv)TYSr8W)hGIYe0 vVOBg|Df>XhJ_nM{}o?^!~^`0{o)`x`B?UocI z1Gw*#X@%3OKW9Wfn_+4bpeowGD9@KzPnsGqSFig8c%n?_EIP!~WM-y=6a|e@$&;%I zCr~24Jz$#KUpZ-NUMm$V%nAz!mKRb~atFRrkO(+QjsFEuVfpGbcP5u+QwEAm{GeG0 z6ivib9WP0=B|L4XAQU3AfXd8yvs+b# !O#A=xXCi0<=181D3S6)C;9>5 z-52zPlJzCxl@zuyI;@9=1(oh==b3QEGxHtQB_hcsz6hal3 v(pit4o`mAoyBGU M9p&@*k9 zB8$&Slx&Ux74()NT%&Q}ENVE!)cCA+i3clgrK2i(i83>OujP!N{xq-b8>+OB!OI_I zKq5mv9**V6RyPq4lr^2B1A5EWe8_LQ7X>^XaU ?Ly3Dqn|=Z zmP1D`V-L _hE(cEh@^*^^E{gZO|#YYldcPC zw+k{SP-{}kW8zvg>cV!rojZzeNH#U25-7;tu(`up@Gdbt$8@r>7ch|P>TG2RVpoal zr$o^-Q7TS5%A$r>#IaGF%kUBeO~-s)NEgB#W>S+`BDYy-h0I28MB;>PasLbW_9neg zT?Pu< >0W=%5` z` 0>9N!&)qH{2<`Cs_|mj*e=C`x)ph zCDIR8W(BvrT(NP<1{`CTh#b7Oi4vcwSK_=Ts0YV(sU;^TaBrt6>&dijDWVf|0?f#R zts7+w2rQIU-)hcqxedD!q#UO$H%_cZhK3Pw)JBs@140|7ysYef juloT8In`u!?IjcIQsVNb5$et({B1Zi!4g;^c}>*i|_84 z-G2c;I1Uewo$8w T$Ifi}(A7l+hIo_d)>MB(n`($LAQviri!uY8b7vxhc~f2t;r?sP$UD!ib;TjKrM zd1NF`IuU;q|Ft@VK2e2`E=5K+5&`WK7-xYlGK4-9V#=f- ^7N6$q!71om#g zu1%;2f;1*yV;OYc%FK9Ah6iJypw-bzJB56*~B8#JBK fFVP!llKzrF-xQHyD zO6T+Y7k#3D|B}r3n)79-lI~}@1_*aH0%}Z50b264pD)fLr%uc=@qI*Fn7%$H@opfn z>jL_TNq}=i<`c=n@fDI80;+GNzs|O%kO0e@tabH#g1^7mdY$K!yX&pz;ZgU|h5m_n z2US|8gYGd8+l S2%RR)f!6Sk7o*>|&;SqBi=V?yfA!;q2}VG`+I04}mC$=hhxUx3~Ub~3wM z%T4Ep5WM$rnj7@ji3BcoR& #Yd1fB`kYfC^+|`ML^> z`$^Cq>83iWR$6jcdnE*A#d`VY$9Vm4#oke0qX>@VY{3E(#%ZW@22`@W4lUEi9_bZ5 zquN=j*khpBaF-JYlof7}_Fn+M2Fc|LUk{GCe{#3<+r3!e!)UNh~ChltQ M9&i5z|29T!qfKmh5Ol*La?MF`wUM>dJLG`<`bw>XJaMC89kSo-%c# zFP)VDn~mM7Lu|GxSVK{xpJwR oK6c` zHvNVI6olpvEkzTkzOrLXF$L0yHd@kvsvCSadrE`hWIgSFI=h1LVW)5BMn$_`LZgBc z&5h={Y$W4Y&5X4<6M}CO<%?^tZH^r7tNqcDz459v{jEUJMRD_~TgoU(|B!+J8D+F2 z>6B|p`^WJ8UzLZe#;uuVFVzi*s)W)oG!#h_&P#T^S!3C1TjX%B&2{gJ`(J=%8Q*mB z5Qt93zc|hWuTcdXD#EkOT+|~s)LO<8c^vi$ PNp(G1gslenF)$ z%e1*>PXyq8(?vCBx;u6HPKf-uA$cX^d(_$cTJdA~)CdbRCfALNk*r|F`RZq_q{GI| zjCh!8KXsGe^_jnkdPWH(lTWB_xX(k%2ZVn4u)c}a?5$QN_I#asR$O9pQoYot0PWDT zv73T@;$%YX6!wad(B naLFG04w?)T`W0gz*1Xz4956%u!w2e&mKkff-7>5GqT9sf|0P5d^>fd_V&_AQ# zKYq`@XO}woD+JNOQ2^Iqf_di=rHpyJg*Qx{z5m=qfwI5fsBy21LC!{r##9oj7m`Q( zms`W=kG)EzaBX7o&mYIhLF=V2(InL+4;1`RU146&>ApQqrK>8N=~~6wmE%zSY{`Tv zyfVNDIz+bSc$uN2LIsbVPeKv7W7n{D&jbM&CzMsp95Y`fzT|vEo$-en@2W6x?fX{U zD|3;^!5Cj+(2+qt&y6)zZ?MC-ushUmv0$@`=xq2Gz`oR`*no$fcp~;Ek)}=->J+Dv zLnvv5DB&=l{zgny`Jj3CH7v}vgyJQF%a+ZGuna?N{A#LX*AUw>D m%8bW<_$f z{B#Pxz!MEJg!dmQ$<$H71Ro4l1H1gl02lz)JlK&$1e^Up2$?nj1Qrrgq$e0_oIrjD z&m7)X%Oo;%3Kep^qaXr~0>Bdhn8&vS2$ux>CnLeHfKpiu3@7>xGDt 4A+d0KA?d-{cqKrK5z5>Cn9g6$|L!~Ij9{a8yWoACEh z%kOj_|LKXUW4oU NX4eUv2uBAl9?=)k{8MT`qUZ) P^lA0`PcEzA*9DOKnfKuaWHwl((P4{+f@99()irQ1lPERRh zO;{r|$H^Na32gwve<>b-6$@ZO2QZ<9_(5KBmLYC)2($eH1_;6`-%<$Bxc=B54%f4e z4(QmA!?9&2ic8g%oJ8;THScNIufxSDxD~dy<6rNSajJ{jTGO>?P&!wMcZ+6p!}D(~ zfL8|oT+p6+Y~g4x-VhR9!jol&Q*iw1yaDQD6H}%k%*)7eX<(dv{VH?*Qk~abaxRA; zkr|r)YB~XiTUT^ESIDDqU*k*9e97>?EXdvD*N13keYj|0MkEEm`I_jKGy2#k zaZR51Y_`5@RIJBkIi-R2TlkdH&gu(yyl#nqQZsfcGZTv+Hof(lZKI-iCiJc@m)01y z1%3c;ZwN3MoHpBqO1xLIZo~y8+ 0I)S;%5 V!Jne zv?vb8w&($C!DC|Y`}~{+|0v6j-q@he>UXSMl#&T9Vfj1a1Z0zus+X|fjS?W}bBg^> z5Ndkgp|X>|m%!Fvz_E1>EXlH5q2a9DpKe;bZXHlZfNIxubjP;WW`+Js^fLEO9*Gi; zDP;lfEwR sa{j#Qu3Qr03ReCF?yiN(a0z<_v^&8nGk2i6oC<*&8D#X+rTCz zLC}6WaZs{P`D7ehityNKm#ok|=hBtv@}jVz=iQFD;4?vGoRvw3S2RosZu5rVvbsN~ znof)RqCK}i7igxP=<|UMpDT9KkLFBx2nC2T@KFLg1pYBZz;&p9T{v)@^1tHpUuWI) zU#j`v=|TkFYLcP+?=4*7k0SAN+|@59Y6(t_v0jj1 K9YE3R}S z-IqTq57<4ZPs~Sk8;n$em22 BqApQjd@8zE-!?Sh(SIhC*yQ$Tw^C5y!v<4zVFxggAMqN ^gD@th9GSxOr@-F~+o%NCp7VfuRe68K6add+zYS$%hcR&rb zLz0QFkE#MDwS_ce98B2m4>Dg>xy?C=4Q9`H lfn&U(0;da9e^9f`)pE67Soi-I%x$kr5rfrA0IQ)>_A{LNp83-PecfX%vM zD2M6x%dz>4WZ2aF1mBZx48co-s+5^97nwzT=aBPjf(OsN0o_AR?h20P?>_`GIXWv6 zL;uizkjqHm#F=xRH8P(SQH0PIpsxZJn=rtCIT=`f0@VJ6(fTf6S6T)@o&^JTqyHUP zbl}sf2$B`Z#!d?;Q-NVxi>`r>#s)$lERa%9E1<~e<_tx{Grlt{ZupPMN>CH>%Ha8e z`cmhno5F?!4&Y};Slo!R7@%4jrCh!gKICLvQ-8> zxr >dUG{k6^A24p_z)6qj+dxE-XU-`0)hHc`n1;-x;_YAr 0zaEMztdCTJS)F2w=p0{X> zM2!EMjuFvAH3yI0*4Ddx ! }l5C%Tk=_)E zgo+GTmO-q@asohFI7SAaTKB@-c`!?L_Q*qJION9TLyY0yBs6Mt$^3^%obZG&Z|0C6 zWs3~*|1e^Qiop8H#X^fYAB*LKF)6bKX_cEmdAoNY@|uwEa-d<38;|bY;fTCwI377X z#3fPPQzCxO1F{zUk0j@)Ex$x*j3jeeybFFB!9W8$J|2nFBkBXFxL(0LJj`|R#Og%R zrjh5*g9F=ZhCX zTH;i^<>ll&)5|XDTZHR-YJX+J 90k8Y$!08|ryg^cNDJ$AJF2EyvK4ntGw`4o b85k%j}2+?#f;3CM!dKZ L9w5U^wcgy~i=Z(QtJ{$Q3%7 zx#VP4Y&Xc?NpkGCFbf!(a)d{z6&(CdbN7L{DIlbgdJ{1p6n92gpL=Gb&&Bw3aeN~B zU^$*<>+fAt+;O*Dr|B2d5Gy3wrqszxtet(*N1cZ8 UWsbje+luPS+%OPjrr`s8L?*8ZED4_iFMe&5r0RD)2gbP1RUIt03YmMm) zKP)KPdn4one`>y9{RK#Jw~94U--Kn|?mjs|`Q_PiL837U+UaB*z&G8qHWkTK%6`(h zF|$7#PqyqhVVX(2P;cQr-0_$6x_Dq}rX!uRX(Ek!PF75&9}Cq_C|ELm2*lQNniD-9 zYbW=5e>L0oZaDVwPhb|w-f)c&jF5r|4Wx45rK4H5JsS6{QOIdNq`aiw=%I_keQO`j zjNK+6(4R5xsReXY`?BfX*#|B#CB}A1#G~10BDRq6=an?|B? g`JS*@p3*R-8{u?sd3e05JiPQw*H&t8vm9OadXrV+?X3B%O}}Bd)xz@- zq WZJvC+fyNd$ z*QZF>mr;!cm(Asn|JqYuD{)~%;3c8JG!;l%GwlmN>tlNf&ij(zn(UyT& n0(j?0m3wBk+lZaEW&d!6 zZ%PlX`30FGI_FmKdJOn7mJP$*JDyCw4oae}uPu%Lc@Hij5pTBq25B%2V5Arq#Yj}W z=2?@5Y+pAD?N5=w1yvyuTTIiC7{^4R)`pjkO@t?v&6WGuFzTS6BKyI&Ywrc&+8})p zzb?n%uZdxLpBc7%`jp}!BD~a9^3EtJ*BLy{I8eI96x< zhaua?kfY0~W9$2CJg4sZ=O=raTk~`+=A2|^E~DAY#Lu>k!1{t2rikf5mPRMsjqDVQ znoUc!#{K>XpZVMtS-flT@>Z ~k6 zwti8giEUB{!dDTQ+yPX216hXnijW>F8dXZ^A2A|C%5{1NX{|arWNuyVkRm}OT9^;e z*P|EF&q4coIaAZ>LE!DV<*>Ppn e0B^yq7N{Sx zYq{*w*FKqM=wNq$`6=%<>HY8>o4;~fD=56Cs5n6|e;WEam%%A*>sn%xltgUBmw0Yd zDJn63IRpCarp%^r8MwPco~wr>JPTA%=mj~^b0PYneghx0VjsBL^TuYAIRWZ14+0UJ zdw`(T-0%nf9YtxXU^Sl$lF(ltaoc7yzv5H;GtJ>Lsq3EzN0?DJ@p)hD3q4`)uXX!L zmh&UY)lJ>)!pz#xX%dcNQrHbqRig+6e3zGOzR#!WG)fLp!Iy@pPC%^F(_w(59 )xe^tuW65K@DV z!+3)polknAP~`7X$N!ZpIOho%6usq*5{%l#Y4O6&y3*ehR=Vn!Jb8=zft*UijPn>U znie$k+v;NzzlDC89=a*rN_`fYav*VP5bAbuA1my|huwAWEcnym-SSGWOsfv4Zje3c z;3)hlT)+82rZl4U-MnQ*)2MOU?ucm38c*j}&Ye8!M R4Doxg%j0WqaWq`d3 zDL8fe2eL&4Gj;8t@i`f7pg5mfR@5yEUzM%MZFPsNbv7(=bIdZ1%LAW#^@8s02ji)} zZ($iZTPAcjhtB8QW-sPzN$F2gQw~cVV)~D&Y%0UQy855~>UvnMeOmcVTdpJ^{-!L| z5ua`m{dJTSm7uDtxv_)v5#6BFA{<_sOvWVZ+`)(KbGu`w(K0}cE);AAwvqCJ7h3?H z 6`fYTBsunPYK$2MZi6!1WJg2=Q{C9<8Q)m6axfwk(fQ#1 z0%|mz)fqzqy=w61V|TA8*>Y#Y_j==EG?m>^$ZIV02!mP2a9xfp zVE)1><18HfGCz!7zGtjYv|?)~n&mpgx~)H2Q3`##N9 z_y|~R`4D`wnptX^PM19>a%I@s@H;;opOgzuj-{3ayMpQ}&*nF;zT50&GiQeFq$WMK z)Vs+I6OMKdsl&t(goB3S&5hp8tJOFk0jY!e?N_~D#C;>UVz8n(ctx-{n!HH8n+_CR z2pG-M0D0GxjXi4hm1SxP70FsS6u*GwcC;y9Ti-zIAW-xNTI rp;GFWz3na zL=?}B{vAH1?{2T2TQuC2#!g#XXzX7(8w6L)-gMS!nI1GDV;|Re+BaVk6%!hi%kNgZ z8ecsinai?LJvDu0PXKIUrHPHp^sP)YCxzoR#HQ(i#a4ts?_eV!buH+aSSD{R*=yGI zI)~BIljsEQBfN&Llk>}rMbdNCrBXCM`0f&vFPpH%o>h$g{t>W6GE9JkA`=w|8?ZF{ zXE-WC0_IuEQ~>H=%aIlU3;|ys;wVDsqXMA_z*(M$vCT3U4Eh zU>ikO2I)9}Z%&WF617q$k);A5x4oH!L7=q8ZKa_i(N&(_TK9~yI6hxwu fv@hieUD4URZfbaQIRQwp=~%DPw-?UhYLt zoSk=AFeGzRBC!Pc;22cF)rH1Ox7pd%WdtWm{(ePoxi4^+-D5QW<4PE;!hxSNGU-}^ z&!cr&)&`7c4xkZr6KWT|*XWCKYbc5nIPpkf+iterrl7f5u*5wpy-bVO;HhoXSKxh{ z)Vv5jKj;r$)-pUq#>!J{Gs%9Vp * 9TXVM~b_ae0RN u~f zSg>nNa(BE;g|}3uw|a!ROmwp1-mh7i@rqf}r rQU9o(m0YEm_d%#fKt&X zb|ixe-3K0B{iH0b|BmENZ_qo=t$Xa~J~K<%%-7p6nw|AdnbRvKDUjsfcU}iSH2CE> zYo{<(>F}cct?iWpR97z8jMM&&A~dFP%o@y$-+E{h_yQ@Dd>TLD_%8o7pYAQ0B{NC+ zjW0C*BV?^|M^RSuj3GbbHCW?v%RUS{Pagf8My9vd-nr!4G3rEQ?p1f5d*S{}>CDIB zN2zMG3bFWQjWAA-@b5UW783g4E}1=XKU}r?YPgnEjc4myI8-7qA@V?w+D(T?&^BkJ z=`SGTy`u5@NYQ*Y!IR6phd=^73`4F2~ zy}Bi5NTge+gbBfwR)4SDYS>4i@^283wCK{9&98!6!>1V*lB{bqjwyK4dR4E+7jFX$ zW9QPVH@XH|t9m8`m|D(F)53e+^fQbA&w`n>9t3@^D!ZFEJ+8FvN>xvZ-XD8Y1hWSW z)}tfYL>h0=P|=7&mm}Jn8|uo5gWFeMbcwWn2#us&0J%-dC!oLS8@#|wkQl oZL@jqpjYeY|7t~-G@_eIM!cCAxr%(Zd z0+Rqx-HP&cx0uZfEmYQj0X!+fM>4{t`aO6uzW6u8L3(M0JN*5cIwE+)TLQ!z1TEJx z755s|ZE#K9o=$Yr&paJdJ|JNOJizS-Nh Y?6nh zL`VCX))ZPjdH!*pANcyK^xtHp*eNojx`^%kMNjBLrolHeC5^>RQ}baQ%2kXO-8B6n ztR6YGll-LvLO$l`pE^3lGj%o{q*m@J{KH~hUD-?83`#LGFu2HMX4-}S1f2vDj5(^F zO(bm*B_r%!_?U)J)jBTbE>(B?RbLug?Wc_Dtsj?MLm P@ifMD4!cNJH2 zl2S6h8ae?Ie3XwMS3xX8fE`c~SWU`BblvCBRq=|(OemPpT@ID3#4nK%FAC=Ze}>pL zeQwmX_l9R*5!@{gdV%%}xxu*|%AJ#53L++ RSuXdb;n$i`yMl4s1uj5)66FtAcJ8#tiHNS-_ z@7xbBJChHQi>gI#+Hpp6E)XgYW>g{)28^cYiQ<@ym1$T|B*%@RoJybDg$hY3T)$Cd z-Xe{5`yGy--%kX4)r~I(Z!+k1e?+Zn%Lq5(=CIE}u@= zH$>Xb469eO?v~VW9d_=yeOt+{n~UBQKHudW3$}ra uC0Y6XE zm$ 6o)80joQPx@F?i`0Q~W*#^PkhV&Li0tGB9& zBCJF32_#5Zsqm8t@7C5}+5>(%J3pVuw(VZ=c_Ez_qYSxk&vr(HIioIoVcs!8*tGk; z>1FeeZC-7?T~9?@U@l>8FJt|sC-W=wZgO~MpM;FrDDMHZY#9qFe;Wq6tYkmR=FbO# z^fsTmqm|WDy#!q$!iWLP%7+5&dWYP6ae;j5?OzsZ%fBqL27a?()__-Zt%eIh_gu~q zkShkQ1WM)MTGN#i8!n;tyOExvrG{>C&H0V4?l+~^%9IT87=>NoInocXh`;M_wZi#? z8K|C8=MWArnnZZv(^y&&+B&W1du5=hMa*6oj*1>y^j}vTWfKlfudY~jY{}uSx}pz1 zrqSiLFpW6~I$AUxlGOjfRbc*UZ}4P+3^s`AAVb4B0a#;Li(v0qT?AkQl|v`T5QhDU zM{p9b6>@*oYdxEr5bHMMkwJs`JDTv;1ld@dUPf0OI>rnfa`JBBXicTj_LvEDd%;3C z-~MT*pWaxh+y+I9BjzTeB{0@{E+w);kMU8kb}T(9FWcVVx-DEv-Gvs{NkDS3BG}-n zf6ofb0Z_aGL+F+ID1O&KQx1h7Cqj5bhOiJZgu!mQrQ?EZZ;I(XTE=*PNxdR$ddj@7 zLN*;wPMeEWd~UdY^ZP7Fl!=K9y`g6@wNwP=rOtz)5(w| ;>dde6h={?iXykmr~Ca7duXvERm5AzUhO=YCNDNr^6JUKgeWq@TxC|)%%4H8^s zi8}A8gqTvoLvpK3;h5Di2=vHW$yv`B_`$&zqI2{VCH)tmoHWu%66VAxq*&=!ookjK zi@mh_^o9t#+k0@#8;v`fYM-a;&>VD8UoS+jsB$*H6YxZ3Hg(R0$Iu36n}q7fN9Tc~ z3sFJkRQ%N&-z YgB!?@5`e31>gVIenOaT6Xjt1tpI^XZY zH2s3JpYH4KD8}rsLFyl7siQre%5XH-*Ke9F#EM9dc4nJjmfn##s>!!O+f|^Rb6L<+ z5g6i1`O7@T>l{FG6 hGtzmxX>fAe>md<}pYr6-s7dP52_0L|ic}ZIwtd&I zO)SI^N3C%|#@=q_D=lnz6T;6u*+{atp)QTWzS5z )-qoW6-uqtDh%aEqE-4e^ax bQD>&5$;M)hued-I@;oxR20kN^fcR&z6ecQ0Kd}Injq%q^UEFcuM zru|01>JX6UF*WAsBF79|CH-nvi;8V22rnpFElW))NeVWS|FvUamj^CCga1GxOF_Gh z{NCo%Kiuo*56Ty*9?QX)WsDCRClW)fSpAr7#-}Rl!isHgqHziuuWf?4?UusXG!`c5 zVL}-JbeLr6-@RT{l5*q4Gw%NWx kJ3k+P5;VYMskJH%Hben2tgIPCytr z`WH-rM7iOPJCM^7D{+v8)c2s7(oyvHHOOXdhPsu^u!P;Ws ;nO`S{v#8v^8xj1P4KvVo67$& 8r25ecd0yRd2mro2RANOYbz@gJ-Cm&R*tnD6+_^xvVGd z#Z5E}>P#EgT3p`s=ghTispj``Ac~+( r$xh44N(^)Oh;X_|j)m15NP znq--j$h44lzQyC?_}H# P|xz@z=gc8Tc9>rl$VPY~1rC!QfSK=URk?)nIfjM?1xA^Djx;N>XB1cjn_YuOkTC z) `oR$B1^AqjC&D zUpG7@-)a%3m!FfjEkwy-uiQ53$R_d-ra_G)@Y=Vt4et_f*56yMU!X8zaL_@3^*zN5 zy6!9roGZ=+P~CJ0KUp0~836`TiH_aIGM(^yxLK0Kctp2d;~ioG7sgpLs;i8aL!O9k znF0f6WOg60y^+1p(RPgQ_JYP@;@0Ks`(DM%1hNv6t)0lB?1u&MXwqMRoqkYtFS&z1 zhwYOIFPwpQ`}q&*mojWrq{1XBV){zxSt!UTFrEOsyhv9BUTIWUfXo3fflpahu$B)6 z*x+hW(g_XS0qPeR@dy$$h+m|UWCVI(zUptT)jL@n1OoF@Fbfe37%Q*ET9XxBqoxpf zz8Q=Sc!f?py!=+yX8|KMlqA}T4$uATB^)VzF3M$b?8zwI8}0Ge2D5+Y73XDTPvmUj zm>}QnAcRSqFqz5%qicYVM2)je;-3)6}4}28+lcw4gZvLuQI&3bOJ(nh(vumOdQQ z*XH4RI;{s%pGdX`2R_XWws!#_2%~>ljs53%Upp3zoe5S9EZP#-sfP}d3X#-Z%h!RF z{KWaMuEf7p_905ec8+53H7hRHMtyy5#1CdxGv_LLNm2gfJr{QJs*`Pm0Ed Tau1OY&D_yj%8tO60Pkey5XYN%~u!_#830)8U8x?U!aoAx~fy#!6yN z!}N2yL0pCtR9FH41%S{~FXbQ$Bs Jub8W zX?}nzZUem-5#_I66xkBd{vrPG7jRj^$+A^c`dm$H;oVkZkS&B0a2sxd_r#Ih7=F2l zz^XnLu@#TU`P|{{{Ry!v;YQ$h;Mp(#e8reTLu=dOuEc{GI8Zc$&$#N*S0%Z8m{k?q zN&QOd5fv!D(nG)KKnJF8MNk!?y>hh;3 _%Y-0&2aa|t+f<7@rF4WHS(Fto#K4CdbtQ! zLl115>v|653FA3r++2S$apEBXSO^O7J4l?kg;@+=k?Aj>H;luiho|_JaVH)sxV}^_ zm#88~<5A=Kml5O%x;95K!;z71qrXn+26}3ytqLJFZN #juO*bPgUcy z3iwEo9HO-Ik{`ViW%hOIV!XI^sNeg+^$vBN@yQ4e7U(cZ_43wRt=C$ Gc8G?(PdWWl6(D%;)U^T z<&$ml^R3ppvaF`H KIpPur_T z)HW2xGPQ^;e!kgSn4Wb&-7Rmf0H(wn*tDCu42>(mON-S>(O*KDLK8El_YAUJSGsdF zQnWv&m}G?{=CnN?uwTsKU{!}4Sev2#0)##}|5=5-XNgCpBQ;Q& 8G)qgbdI9d4)D8E;;cMbMzL@uqA; zw4jRqRD+1nU (|m1E#w^h|k%!N7&hQtID2Zr@ z`y;1huI*1SZm~{OOKD3|50w5z#c!g5>N!7GMy*uO)1B=jM^@*gDjzq=_Bf5t YkQ@qR?LWF65tis=R9X3h*r7miH#0 zFa-QztbNzM={0dn&8^|mGAB(8_k@(5QWw3V4pBBXs4=jd7;*A+i++BW75yT%ncz$E z{>i19-0HcCAf@RTPACjM`vU} fs%}aOQqwaZU zsNs^|Id?&^qxwRhTS*}~x283@yq8}j3Dvgt&&8^}Rzh3fvt&t)O^ZpuezFV!XgC|v zYc3A#AC{Fgx6d @4e#2$&=ikQG8vbJ|`VU5}EvR!WyQIC TqaXmdDyW$411M&2E{GWvzphwrk*%L)v~I`YH}U_!}?jx_e;9^S(L(M{jNv8#rtU zo+T#}WLJ^A+0qxxpoAk Yv?{P3iFHNi<0bBhY&u_< zI|DOxUYxh(5uX;M@ !R_&|by~K}O63)BXS2%5BZheRF}IcY=r2TE9i9vu=ER#? z6_DR79m&ohUm_6XA^ro0G|ZoASANiv5d<2S=+s50mf~zYO@qULu|z_(r<&*wtkd1; zRdtG0l%A)YCIWhTU(k0+&&zATgXkz^D4ZE#lc0)ql0Kbx0yd+`GrFP%ne8X5HDyZX z;W9p1tp(9&L)iP#rfAivExBq3VP&P@9b1e;2bo`YE)kml@D@xCq $%+QDN9qGqu znb<5d*+m=9sQNSD2O$Zyps#6)ek6COSmO-&mO@g`CV}Nk?xL?@Wc?!z{cIKWb;ZMu zTjoS>%lQDp@j~h^z^lec9!mCm4VoA&E^HD6*aG;MMf{5vpuvMnYY3DAc#^>a=!rlv zEyr%|#lHN)9k$9)2+Ywnj|=9EWCTbKdK^uY9dPDuh ENVxPYCB|P6TiP^ft4khI|CNke$jQw|Z3-)Fq=%vy57p7p zA*fU+$>aCrX3&KfP;MNU@?ol8|6g>1Ex>NlY`=-#g zLifn|_kREmLGiu-VTVxT;%)M#MiTjZMC^yoM(~+bli?(_$A^KnQJWI4#FnVHt&^X_ zXQ Xd}eoRYY0`+VJomo7Ozq zxG$w8`#Apq)U9t@#!htSBOuyKStGr~YxhT|?;hV(UyC0V?IC;4i(&$2EJ-zKSlz_{ zBEo %e5NZGrDF8G8G=MVDFyfGdPzPF=!>u3`9*P7l9E_&m&;!Wk z4(jEaDb!gfjM>2MzYxp#TH>Q>rcSqLl8bnfCI0|A27${qAasA`rxuUb#eG$p_@KV6 zJ-;GS9Pwo#gDVTGsk)GRK^zTKH%T8uny0pjDz-7?c>%j=l)48cT7oeFpj|Fa_)|i= zlsT{>q*Ng!N_Y`SvbHPx_7q8z212O+05Hjmli98*91AtM>Awoo)ynH=^$fJ =^(v` !oGn6YyI4S1MVy-NzDt{btz=O>exv83H2@MwwVvcA=Vi%15MgMm)J*YpIVQ za2XL39G;@f?+-& BR9O7RV6ocz$r^3zsF6s6&FU9x$AFjY zIT8b;e#&qy?XaQ^RKfn@B*yYeW|`Vqo1BhrZQMHm^a^Pj7MdVNMlJ(KpO-4Zi-|N6 z&oi4UAejMXJ%C=PMX6S2G=As%8JEt7* d%pPCZ$e7hHX%HFDAk^xeN+n!Ct&6UT+_b^ibsOKbEN?Dh(h5us%4GCtYR zn;bxl&Hn&5BXE&>qLHD-g^!>^8$Z)fPj)8tG%uMyKYCxIc_#Lw82b*V~FLBG0J2-~Q$dTreSA=NtF?JEJB> 4LFl&yO)T&r;Gv+=C|%3GD_u4_#%>oXK`3wk2Xm( ;5L@~I~bP%+zQ4-~kpw&x^BdpO!e{nn}5)Jgag zS7;?ttW+u06OIg~rMlFP#@q6XtT7;w_D#ZNj!flTtQil59Q%)M<8T2T{<^`RACpEq znsVveT&r-~CTpAW$CZy0=#9@mx_I&?)mGD?D^@i#KbSC2jQf*B#U)r-k;eiGsT@D6 zSsM8N021{65}mhn&Ng;DQ{+oD{$hqY JeXc= z#6fn2Rly9_`y;p4T5g#|85xDL=kGg&*)j%(3AFxc1TcUP%L{+M#fPJfJbEeh3t9_? z4$*~!0EZSK8V-Hr_p$AvvJx&~9n^$D?>in2aA{HiCtfEpZiIS@+gYKnQgyRI;Mvm) zZ-Wv(Q7)VImToT8RY04O=GW2Nj&>eK xtFBiIT;UD<%#^VA+)FM&hhF5Npje){N_>H_^z_077t} zL4xu1(*VexG3lf +3`l$Zy@2SF(Nl}1F20_SE!K}9zha`NYW>5GOuD}0dJx~%hT(tU34b` zj9M2nMR6N8w+b%FApZanM{nX@+K%FaR~I|tky<8H$&81LgLS#?J+aCE0FEM{)Cr+j zWQrnXjZApcx$`4G;LcBpNbC-Hk58_s%TFbhbEDjOs;C?RdIA6~K;UPapB6Y!-YI0b zh7oN}M>}@`+TE2Z)v`$uQwbj?38!H5d6rN;)KUp5fcm(eXX>p~qzcv(HaMB`{nkw< z{)r5KT}}8aTcD R27Q)DVK21_7i1PykQ?Xb@5;7zTj^BAFnjfB=d>7Jven z1vCH#z!G^5bO8;MxX-oj2+~B*PLs6cJ=Qj|gB^$rq_=qDKAOs%mQGvO(A=?CCO#?a z>Kkf@Cwk4v$lWJt_caS3^HF@LL)a*S2D*RdFeKFbJ-ScB@fFh@vDc5vy_>{zTp$ z+dP=^! |1%)v%7IgVBQ+;%=>ZNkh9 za%w|k=%T%=uQTrlij%ZIRiRDzId<3+9~FvXWlFydiy68{k|f{ j @%cMMqZ#U$x3!6Jhs z#@C0363rh0?OPhI*wZeqX1COi#Oo|l%CpBLCCdb521Q_;&4-C?+i~rxEs3J~<*kX$ zyQncTlr}C*bA!ZaU`@ST>aB@9WTbnU s?I~E|5)vebzj5-|JIMk!9(Aqf8y=7cVmJ9A-GTSkzqPV2P4eWgmH3C2Mzr zJ8GNIvbH@-H ||t?Zg$GO#l~u-S$1Rrt`A8f>9pT z<(TFc)Xramq-*x1C*2+N>rPq&$;0CGE1f@+i@5CgdxkoZ7DRfLPlrnaRmOlysnicD z&1!LQ&Z4@qmE@O5=|F`9Vwj|<{LtzhpJ(X(#thkj%iJJncKlJsNo52PVc3dCiH`yG zRoE$OSwTl*Dy4Ni_m7p0kGHzzMqev)*6E{=dxb`8@zOj*dn>1TZca^fsza=l9&|{? z;wuGS%c9!;yPAb{Cdb=8-#+8IVr}^1`5AL0QTiG}5EJMZxv4!6q<8acu2XdDMTevE z e%qu^YXwx&J`Q{ z%xJM=tZJp?AtOLp*zyVm$@(fJjJK?M=X=@us~T)6M6zO$H2BQ6P+3a(E}-3%Ms400 zwd>M!uqxyCoPN$;RyIGDKQ(qu`Xu-{yr3*I_L+z;2Cg1tedrH6aJOJIX2ayFwktn1 z`X>`$-j^?Kj3(W=n@IxjSdI0(U=B$7E(feuj;?2F!`&6-tZFg9pJvX(jtS(JEyda0 zr3x7&nnnRjTZmAgDDD+ylTX-E7b9$*3EmYckkR|?j^1jEce0BL69OMAal=hWp|Yf2 zaY8lQ_K&(frlhI_Bpa6Lxo;7`6(GWd8D&_{m{_R+5v2Eh_6EyM)gn+&=0+mMmQF$> zX1%UOhdWeO;zj!tD{k95TW`zT<%=&59x*F @~?nPa@C%YSM*m*nNMm8q* zls3JC0~6xIO8N_9t1 !dVUHo>na z4+`K6ow)P=0Eso`Pp&%Y*4$D?JL@mn4l0a|zb_2%WxJxDc5%zt=xWR8(;_PlSDmoJ zN%47ThuA`&(NK?wBT<&)HZCbL NpYQn zO%<7BQV5z8?o5PmC$YzWvZZQ~XjtP&viaDqA)Ils_Zs~8c3-xW0!A$LU`|XX8JToX z6Y+{<{4O|LdU(}tq8rPZn}}4!BIIGo{0ScS{f1$G5cP5TYbEi)vmYDi^sp(^dU#8U z{ Q$p JM=xI**;;YXs@9)yd9xHqjoBWjKE)&T)cBB-wTQN< zRzqT8k)$W@I+IaqSeD|LL^LlU9xpVgJ;7r@ZBuBA8t0aMI(^{lr}m_8Ek8-AtqD~U z-ck6bo%jSn1PR=W#If|T`hcv>=r+MLfC@kb00pQ3v;b2;0BMXNph0LDUUUotKm{}a z27mz40FDU(N7^>_b3ag}X9$&6qpj}^GGfKUoybgyKqtCXMrD6mwR3Bc%QaEPtHB+H zxY$`zWjl#79tMhMxKA=z JO}0J0;a22+eA*yvV2d8GK`#NW?2+8X5)xq!}`np zpeZ`jRHjNAvfN>by5eI^G!CrGtj`evjpLf(g_i#0!2KeMy820FsM#61w$Co=XF=uU zWI-|j!H@|;+&EUZi0H93Cud?+l~^Yvp4dMY4VFz3kKYQ%>n5|MsRX gflgGf|>v+paL}j5xG+W3Ps{+pb>SZ0&FN4ldS+E;(!owOhPUw z0Qg_V_E5%5Cm~1$mz5y~6o49F2&BL;6aaBR2BZLiKnD~6VL*ii0}i5q3@8D7Xp6YD zAS%6S)dlj7$+xxA76U3p`%@3FE!iJ$;`LPPUnDI23T&Gs+m!zR3e020gevYcp|J`$ zt}I9IH#a?>MOW=5>@TxnZP`#^ZOx4cgUm#Ybb;l=2=f>z=A%k!;CpH|Qne>ry6oV_ zGk#$2$<=vFvE{T|JWkRsJinb7iuz(v=4xPMwy%1vw^F=UsO#Od>XTo`p`di1%x$}M z0vxmOS)=c8g}W{v?D6~7ta&gxnrW7|*uFuS%5=8_>8eF)Nbxeoxzm!TjEU-8r8|78 z$H8m3`@nskAA6NsgIlk7I>jySG4Gt4)Z2$$%Z_96v9NJ* 053jmbq`A9tUh1G?GhIF-I94qh@9T?;>l<7 z{9f|gPk0>at74I>wXwbQAIuGttXn1sV12uB#6dKeb2>O^4-Yyi7QvNI2H* cDrLvsYoe^aD z9n?7a{WB)hkuomrhdh#;LvZE^0-rGf=DP2Da=#kK+s4(~@XnW0Ds@%zk) ;_nZP+1~8)}ShyQ 22X5pm);E|x Q>bjKJn6daN$SAzQm zM(d47S1gp&%`MyB1I7DI;&0PXs?eKbX5aM3EL(K8&YfQ!7z3#zaAT3d^sUr!?Y&C1 zmRDv!>7JXKS@AZVxiw|w$gspuYri*d!lLYb0 7&4oDAJPgDs;bISs ze_dbgDE1F_Jh<_ fV8e2YZck6d}Ug10E)9 zW28!~IIA!VDzN2x!rU@yz6$mxblVnK= Aqhgyb zKpRiGhQm=%Tn;^jlmqCix_J5INQYwlXxZmk3uI + zEsKpLIrtgJ7juN(Ibf6n?Y}$GC^a^{r>XM3VEnY29LZUFn`j7+0z}{DBadH_`nZ9t z61VRaO>4sXY{@-QoNfEoMxh>-$JNhTvp4D$uX2`)HU{G^LM$>n%@T5sTNuawUHreL zYA&KBG8YO=c6Sjopzy5ai1jRL5+(kvMIlIz(;T_8{#X(nW;l{Hu+s`_dk ^SHN@^LpOz{+nc2P$ualV1%JHOvwZ*pW~lU1siKSD}Ai?zS$kkCQBG zDM22g;ta~RU~I#Q>>!Hrc2MB%Cn`jQAxDmM9);B3pjx9NfR83Jjx8hllqmYiqCy6k zn~KYyk&YKYgUe*OxhmtC;5$W0tpOIiNtO2Oe7sOwaoIw7uyZqFqZU$~?21`!>-1LI zK1n~o&1J>4;=|m@A%k* me3{5&U$#TEP!pggQO_ntM-$Bk7P zEs?e%wsl7Rwn^q~`SW5(K^mcLJP5ZT!?(Jm=$_(NXtZ>%&CaRWvk|hg Xk)`n$zfY$@zcxf(qi(;ZQ^Z&(oLZL^7Em30{I1d2xnS4-VlPddeV8WT)+ zxt}H}vSWnLjN Jd5;t08eRaI%&hygY@=+oo$nmb|rJ_;Qc2AUrH> z^zf|L#OlrX^D8ZPBzk){7b|GS_<2%Nv4xRiV8vW;bzljx4SUs I;s4Q}C5 z`c5ZewUJB;?RmQP*yUlRWb>vV^8m`qKBTC+>R8sJ?XCx(8y_-N(yrcmq37XJv3(lZ zzKW|QwSrp@*xc15Lem3{A)vIz0i*#<00E`}T7U>>1QgIP4FD9104bng8bAdA9~=Vm z6ZRpIhpxeY-m3Dygq1fybu!M;V?0EMiz%29#G5HL9C9*g(XQY4px6DO$lJcmefGooCaMsG2TT@+hSiMds^ z$8Sx@yp&EQ7AU6kdQTA(-f #w6`DRK5%DoC+CrJIDf>bA`kL$YGgh9pit<-M zBU{YDXaJ^w2-l_o4KN|398a#02)gj50xT(vO@#m=;(!owO$b57AO{zO;+V;V )K!p@g0)S8fm2PMOk5=^u{lBsog@D|0?-mg4 zULHAZ_yOVgtxl>%X9%P8hSQOmml%it08=IWr*9CA0sP6|zfJgewLFi(8k(*qHZBe< zSU8KO2)p>RF*j#p+RS*9P-Muix8B!k*>mUDp~fe}M%|&aSow@SD+W87bgbDCvxH*F z1LEWPa*a882hYnNb)0aI{{Ss4zPr|j<&`A`mu|vLeW)u7Pnb2hKV}u;2k%ujdY2^# zvh-(B?$KxC?M*Vvhb`79vI+1o1ggg%yDp+E1&H)jf8(p{O?G&%h|sb3INDDogF}!% zz?5loZq@8@>08!2;&pJ%l(lZAu0*pNB6%W>Bl8ZiBM!ikFmr#R)vKkBZ$5^8?bUI| z`-~&6I*T85>;3mGIfU5%01&X^y4y&KDAhu(y8~*j%_|8blU7drpARR@ED50Td?XnY z$@>;q&;J07sAbqJdT(ui%99+Q7BiT*{oi>%utj3B0j}{7OpGkhY}oSJL;nCR{4Mle zogU~Q^Zx)|^kmq-EH{tx-d5$d?zubE5fC*DStipYTrXlVnOVQtSJ!02mZl{JMmBUT zBP0XOPX-)p8|f6wHzTK!F?YFp+AB9GbAU*AJF8Y(P zl}{1hQl@(4_*#O?bnLap^+o5YbGJSBr?EEZeBF*s%>mvrCrKh~TyTKT8eA!P8Q4al z4effxiji$~kJQ_2R=s^E^CfmpkLeDX*dmi^#!Q_MgARQ5npXr|FlH7P{2) g2 zQIl;Y4^Zv>R+!6&xMAnxWA#ob8a$`-3||r#lE(VnI2@nXS7g;eJl#L0{vB&=YW-LB zL*Mlqu)Y@Sv~^}W_+}r=vzT($nxw~L`$}YAqO)VyLoCBH4Dtt&xW(>REWfgOA7M79 zUqZ*~KZVtl5PT$Xq{b3PY-yJ<1 i kti#2O}W_@{o-k% z8sGYQnP%%9))|lFJ}x#pdL7Hl8xKfz;Z$~hQgj&n-06AxXRPOZc-Blk#C%}IUH}&) zOXXjOz6sJtZA%7DRU>t&Uc=qCiBkB^=_vQ1M=0R>P@mOT5QDO}xL3mYb3}jU2%Gev zsi8q*>0R1vSn)D3<-ExY%jPn#6O$u16$4N3)oQn@8!Z^!TjYquxY0O@R>)!i1(}2M z{=d~w;3Uds*)H3amD5<++M ){|kGRxyr? zmYD^Blu0wJM(8dX5pT f4nB{vUprktT4AcSMRmP3s nu+NF-$(D2C8eNW? zvJf~@Z$KL29X&$h{w^|~YB?XSrTwH<<+Ep8)0r8=Lnc7w7$9n1WL%MNLGP>jt@4i^ zO7k?UsWZ7iag)o)f#JuHS%s~n-uD)|@IBSP>G~N~M0vaBR{fQ`^+saJD12-w8y6Ta zyQU&ln10&=Ptn<1FxC{5 j}5ujG9Jo`fl0EF~Z(5;(xQ}#~h38)1cK0DjYK~>K>Kr zAD6qtIgHH_SH2$Wj8B&2eUf_(xubn`Ei8td8@EFI-08lf+wr2v&4&x9Gy#!?De}X; ztbDTG7FX`SqOo5SPuOa$r46?(f! %<*B z^<^Deixp?Rsqrnmm_JnvEx)NUmR+YKrnl5*e5kRdX2y407I?UcBh`EOU6;Dma>p7y zQ3~X(wOSh|$>}~~?%O1*o`-P4O)r7DW=dml$KFeoXOS()+uK#&3Qy3DyNbGe#hJ0P z*K(T!agq#C8u^R^lN_9Q@nDzJsCapC?d-O=(o(e{{!_`O#NHG;ZcK6F;-h6qA6|A! zS;9i1;X`ml)M}iq&ZXF)Y8$6^si%fK$>J%87zPN>_>FS $ =bB`T7=~~6ZmGO9muX@9DxoNSonO&