mirror of
https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT.git
synced 2025-12-24 15:56:46 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf9f5e6c2 | ||
|
|
f9715f91f7 | ||
|
|
98e51d443e | ||
|
|
c8985f1a85 | ||
|
|
5bd83bfcda | ||
|
|
36f30571f3 | ||
|
|
f202925333 | ||
|
|
2e6f71d962 | ||
|
|
ffcb527adc | ||
|
|
12ca55a372 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -173,3 +173,4 @@ cython_debug/
|
||||
nuitka-crash-report.xml
|
||||
build.bat
|
||||
log.txt
|
||||
result.csv
|
||||
236
FAQ-en.md
Normal file
236
FAQ-en.md
Normal file
@@ -0,0 +1,236 @@
|
||||
<div align="center" style="margin-top: 20px; margin-bottom: 20px;">
|
||||
<img src="./introduction_imgs/main.png" alt="FRAISEMOE Logo" />
|
||||
<h2 style="margin: 10px 0 5px 0; font-weight: bold; color: #e75480;">🍓 FRAISEMOE NEKOPARA Addons Installer NEXT🍓</h2>
|
||||
<p style="font-size: 1.1em; color: #555;">An application for installing patches for the Nekopara series games.</p>
|
||||
|
||||
<p>
|
||||
<a href="./FAQ.md">简体中文</a> |
|
||||
<a href="./FAQ-en.md">English</a>
|
||||
</p>
|
||||
<blockquote style="color: #c00; font-weight: bold; border-left: 4px solid #e75480; background: #fff0f5; padding: 10px;">
|
||||
The English version is not updated in real-time! Please check the Simplified Chinese version for more updates! Thank you for your support!
|
||||
</blockquote>
|
||||
<blockquote style="color: #c00; font-weight: bold; border-left: 4px solid #e75480; background: #fff0f5; padding: 10px;">
|
||||
Please strictly follow all the rules in the <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">User Guide</a>. The developers are not responsible for any violations.<br>
|
||||
This tool is for educational and communication purposes only. Do not use it for commercial purposes.
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Applicable Games:
|
||||
|
||||
- **NEKOPARA Vol. 1**
|
||||
- **NEKOPARA Vol. 2**
|
||||
- **NEKOPARA Vol. 3**
|
||||
- **NEKOPARA Vol. 4**
|
||||
- **NEKOPARA After**
|
||||
|
||||
---
|
||||
|
||||
⛔ **Special Reminder:**
|
||||
|
||||
> **1. Patches cannot be installed for NEKOPARA Vol. 0 & NEKOPARA Extra ❗**
|
||||
|
||||
> **2. Patches will not be installed for games you do not own ❗**
|
||||
|
||||
> **3. This tool is only for installing patches, not for installing games ❗ It only runs on Windows x64 systems ❗**
|
||||
|
||||
> **4. The tool requires administrator privileges to run ❗
|
||||
> Reason: To prevent installation issues caused by the game running, the tool will get game process information to close the game before starting ❗**
|
||||
|
||||
> **5. Before using this tool, you need to understand the basics of patch installation:**
|
||||
>
|
||||
> **5-1. Why install the patch? What's in the patch?**
|
||||
>
|
||||
> **5-2. Based on the documentation, why do errors occur? Or why does the installation fail?**
|
||||
>
|
||||
> **5-3. After a successful installation, how to check the patch settings to confirm it was installed correctly?**
|
||||
>
|
||||
> ***If you are completely unfamiliar with the above, please do not use this tool or watch the tutorial video. If you have already downloaded the tool, it is recommended to move it to the Recycle Bin and delete it.***
|
||||
|
||||
> **6. Make sure you are using the latest version of the application (please regularly check for updates on the mirror site or GitHub and download them) ❗**
|
||||
|
||||
---
|
||||
|
||||
## **🔄 Usage/Flow:**
|
||||
|
||||
1. Download "FRAISEMOE Addons Installer NEXT.exe" from the repository.
|
||||
2. **Close any running games from the ["Applicable Games"](#-applicable-games) list.**
|
||||
3. If the application asks for administrator privileges, grant them. **If administrator privileges cannot be obtained, the application will not run and will exit automatically.**
|
||||
4. If the application asks to close a running game, select "Yes". **If the running game cannot be closed, the application will not run and will exit automatically.**
|
||||
5. After launching the application, select "Start Install" and choose the **parent directory of the game directory.**<br />
|
||||
|
||||
> **Important Note 1** ❓ What is the "parent directory of the game directory"? How to get it?
|
||||
> Taking Steam as an example, find the "Library" tab at the top, then find a [patchable game](#-applicable-games) in the game list on the left;<br />Right-click the game in the list, select "Manage" -> "Browse local files" to get the game directory. Then, click the "←" button in the address bar. If you see the [game's folder, e.g., "NEKOPARA Vol. 1"](#-applicable-games) in the file explorer, then this is the parent directory of the game directory. Select and copy the full path from the address bar.
|
||||
|
||||
> **Important Note 2** ❓ How to use this tool for games from third-party installers?
|
||||
> Since the installation path for third-party games is not fixed, please copy the parent directory path of the game directory yourself, similar to the method for Steam.
|
||||
|
||||
> Example (for illustration only, do not copy directly):
|
||||
> Game folder: C: (drive letter may vary)\Steam\steamapps\common\NEKOPARA Vol. 1
|
||||
> Parent directory of game directory: C: (drive letter may vary)\Steam\steamapps\common
|
||||
|
||||
6. In the folder selection dialog, **paste the copied path into the address bar** and click "Select Folder".<br />**(Note whether the text next to the "Select Folder" button is the last folder name in the path. If not, you may need to re-select).**
|
||||
|
||||
> √ Correct Example (for illustration only, do not copy directly):
|
||||
> Path entered in the address bar above: C: (drive letter may vary)\Steam\steamapps\common
|
||||
> Folder name below: common
|
||||
|
||||
7. After selecting the folder, you may encounter the following situations:
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Status</h5></td>
|
||||
<td><h5>Action</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game exists, but patch is not installed</td>
|
||||
<td>Proceeds directly to download task</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game exists,<br />but patch is installed from another source or patch file is corrupted</td>
|
||||
<td>Asks whether to reinstall the patch from this tool. If the patch from another source is usable, you can choose not to reinstall</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game does not exist</td>
|
||||
<td>Skips the patch installation step</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game exists,<br />but the corresponding patch version cannot be installed with this tool</td>
|
||||
<td>Repeat the previous installation steps</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
8. Confirm the final installation result, then select "Exit".
|
||||
9. Enter the game and check if there are more options in "Settings". Or if you have previously entered the extra story and it appears in the "EXTRA" option, it means the patch was installed successfully. If none of the above is observed, repeat the installation steps.
|
||||
|
||||
---
|
||||
|
||||
## 🔰 Software Features:
|
||||
|
||||
- Detects installed patch files for ["Applicable Games"](#-applicable-games) and compares their [Hash (SHA-256)](#-hashsha-256-checksums) to verify integrity. If normal, it skips installation for that version;<br />If patches from other sources are used or patch files are corrupted, it asks whether to reinstall. If you choose to reinstall, it will automatically delete old patch files, download the patch package, and reinstall it.
|
||||
- Detects all unpatched versions and performs installation tasks.
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ & User Guide
|
||||
|
||||
---
|
||||
|
||||
<h4><u>【Important】Why did the download fail?</u></h4>
|
||||
|
||||
1. Please check if the "final folder" in the address bar of the "Folder Selector" matches the "Folder Name" below (above the "Select Folder" button). If they do not match, it will not work correctly. If this is not the issue, proceed to the next step.
|
||||
2. Please check if the selected folder contains the [game folder](#-usageflow) (refer to step 5 in the usage flow). If the game folder does not exist, it will not work correctly. If this is not the issue, proceed to the next step.
|
||||
3. Please check if your network environment is normal and the connection is stable. If this is not the issue, proceed to the next step.
|
||||
4. If the installation result is displayed directly (i.e., the installation step was skipped), it means the path is incorrect and the game could not be identified. Please check the path and try again. <b>If you are using a non-Steam version, please find the relevant resources to install it yourself.</b>
|
||||
5. Please go to [GitHub](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT) or the [domestic mirror site blog](https://blog.ovofish.com/posts/c54d3755.html) to <b>check if you are using the latest version. The application will not work correctly if it is not the latest version.</b>
|
||||
|
||||
---
|
||||
|
||||
<h4><u>【Important】Encountered an error and need to report it? How to submit a bug report?</u></h4>
|
||||
|
||||
1. First, please rule out if it is a local network problem.
|
||||
2. Second, please enable debug mode, run the program again, and <b>save a screenshot of the error along with the log.txt file from the same directory.</b>
|
||||
3. Finally, <b>please [submit an Issue on GitHub](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues).</b>
|
||||

|
||||
|
||||
---
|
||||
|
||||
<h4><u>During Use</u></h4>
|
||||
|
||||
- The application is open but loading slowly.
|
||||
- Please wait patiently for a while; the program is not unresponsive.
|
||||
- Do not open the application multiple times during loading to avoid unnecessary issues.
|
||||
|
||||
<h4><u>"Application is already running" / "In use" prompt when opening</u></h4>
|
||||
|
||||
- This is caused by opening the application too frequently, causing Task Manager to fail to refresh. Please manually open Task Manager, find "FRAISEMOE Addons Installer NEXT", end its process, and then restart.
|
||||
|
||||
<h4><u>1. Download Errors</u></h4>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Common Error Types</h5></td>
|
||||
<td><h5>Error Information</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contains "403" / "Access denied by server"</td>
|
||||
<td>Access denied by the server. Check if you are using a network proxy (VPN), reset the network proxy (or exit the VPN program), then "Restart the application" and try again.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contains "port=443" / "An existing connection was forcibly closed by the remote host"</td>
|
||||
<td>Download interrupted. After other tasks have verified file integrity / download tasks are complete, use "Start Install" again and select the previously entered "parent directory of the game" to install.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contains other messages</td>
|
||||
<td>1. Mostly user network status is abnormal. Check and fix your network status before trying again.<br />2. In some cases, it may be a server failure. Please report the problem via GitHub Issues.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h4><u>2. Problems During Download and Installation</u></h4>
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Common Problem Types</h5></td>
|
||||
<td><h5>Solution</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Download progress is slow or seems to have stalled</td>
|
||||
<td>If the progress has stalled but no error is reported, wait a moment.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Window flickers when verifying file integrity</td>
|
||||
<td>This is normal, no action is needed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>The download progress window pops up and is covered by the hash check window / the window close button turns red</td>
|
||||
<td>Some patch files are large, and calculating the hash value takes longer. Please wait a moment. If the wait is too long, you can manually click the main window/download progress pop-up/hash check window to refresh the status.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4><u>3. Cannot exit the program during download</u></h4>
|
||||
|
||||
- To ensure the effectiveness of the patch, do not exit the program during download and installation. The user is responsible for any negative consequences of violating this notice.
|
||||
|
||||
<h4><u>4. Forcibly terminating the program during download</u></h4>
|
||||
|
||||
- This may cause patch file corruption. Restarting the application will automatically overwrite downloaded files. The user is responsible for any negative consequences of violating this notice.
|
||||
|
||||
<h4><u>5. Network speed significantly decreases after multiple downloads and installations, despite a normal network status</u></h4>
|
||||
|
||||
- To ensure server stability and resource security, download sources are divided into domestic and international. To guarantee download quality for more users, domestic sources have a download limit. Tasks exceeding the limit are forwarded to international sources for download.
|
||||
|
||||
<h4><u>6. Found an identical/similar repository/application outside of this repository</u></h4>
|
||||
|
||||
- It may be modified by other developers or use patch files from unknown sources. Do not download/use such repositories/applications.
|
||||
|
||||
<h4><u>7. User obtained this application through non-free means before using it</u></h4>
|
||||
|
||||
- This application is free and open-source. If you obtained it through a paid channel, please request a refund immediately and take action to protect your rights.
|
||||
|
||||
---
|
||||
|
||||
## 💫 HASH(SHA-256) Checksums
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><h5>Game Patch</h5></td>
|
||||
<td><h5>SHA-256 (Hash creation date: 2024/07-2024-08)</h5></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.1</td>
|
||||
<td>04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.2</td>
|
||||
<td>b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.3</td>
|
||||
<td>2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vol.4</td>
|
||||
<td>4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a</td>
|
||||
</tr>
|
||||
</table>
|
||||
28
FAQ.md
28
FAQ.md
@@ -2,19 +2,10 @@
|
||||
<img src="./introduction_imgs/main.png" alt="FRAISEMOE Logo" />
|
||||
<h2 style="margin: 10px 0 5px 0; font-weight: bold; color: #e75480;">🍓 FRAISEMOE NEKOPARA Addons Installer NEXT🍓</h2>
|
||||
<p style="font-size: 1.1em; color: #555;">一个为 Nekopara 系列游戏安装补丁的应用。</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/" style="margin-right: 15px;">
|
||||
<img src="https://img.shields.io/github/stars/Yanam1Anna/FRAISEMOE-Addons-Installer?style=social" alt="GitHub stars" />
|
||||
GitHub
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1hn9UYwE6p/" style="margin-right: 15px;">
|
||||
<img src="https://img.shields.io/badge/Bilibili-视频讲解-00A1D6?logo=bilibili&logoColor=white" alt="Bilibili" />
|
||||
Bilibili
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">中文</a> |
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md">English</a>
|
||||
<a href="./FAQ.md">中文</a> |
|
||||
<a href="./FAQ-en.md">English</a>
|
||||
</p>
|
||||
<blockquote style="color: #c00; font-weight: bold; border-left: 4px solid #e75480; background: #fff0f5; padding: 10px;">
|
||||
请严格遵守 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">使用须知文档</a> 的所有条例,如有违反,全体开发人员不承担任何责任。<br>
|
||||
@@ -133,13 +124,12 @@
|
||||
|
||||
---
|
||||
|
||||
<h4><u>【重要】为什么开发者无视我的问题?如何提交错误报告?</u></h4>
|
||||
<h4><u>【重要】遇到错误需要反馈?如何提交错误报告?</u></h4>
|
||||
|
||||
1. 首先,每个人都会有没空的时候,请耐心等待回复或问题处理。
|
||||
2. 其次,文档和视频中已详细介绍了使用方法和常见问题解决方式,请检查你遇到的问题,或相似类别的问题是否存在于文档中,如果存在,一般不回复处理。
|
||||
3. 最后,如果遇到了未提及的问题,<b>请勿在视频站内或博客站内以评论,私信等方式报告你的问题,请到[GitHub中提交Issues](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues)。</b>
|
||||
4. 提交问题报告时,<b>请附上下载报错窗口的报错信息,而不是安装最终结果显示,</b>安装结果显示是给用户看的,不是给开发者看的。
|
||||

|
||||
1. 首先,请排除是否计算机本机网络问题
|
||||
2. 其次,请打开debug模式,再次运行程序,<b>将报错截图与同目录下的log.txt文件一并保存。</b>
|
||||
3. 最后,<b>请到[GitHub中提交Issues](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues)。</b>
|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -151,7 +141,7 @@
|
||||
|
||||
<h4><u>打开应用时提示应用正在运行 / 被占用的情况</u></h4>
|
||||
|
||||
- 由于开启应用动作过于频繁,造成任务管理器刷新失败,请手动进入任务管理器中找到"FRAISEMOE-Addons-Installer",结束其程序进程后再次重启。
|
||||
- 由于开启应用动作过于频繁,造成任务管理器刷新失败,请手动进入任务管理器中找到"FRAISEMOE Addons Installer NEXT",结束其程序进程后再次重启。
|
||||
|
||||
<h4><u>1. 下载报错</u></h4>
|
||||
|
||||
|
||||
93
README-en.md
Normal file
93
README-en.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 🍓FRAISEMOE-Addons-Installer-NEXT🍓
|
||||
|
||||
```
|
||||
🔊 Note: This repository is still under active development, and most of the documentation is not yet available. We appreciate your understanding.
|
||||
The English version is not updated in real-time! Please check the Simplified Chinese version for more updates! Thank you for your support!
|
||||
```
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">
|
||||
<img src="./introduction_imgs/main.png" alt="Logo">
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
If you find this tool helpful, please give it a Star⭐~
|
||||
<br />
|
||||
<br />
|
||||
⚠️ This is an unofficial tool and does not represent any official stance. ⚠️
|
||||
<br />
|
||||
<br />
|
||||
⚠️ This NEXT version is currently under active development, and stability is not guaranteed. ⚠️
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues">Report a Bug</a>
|
||||
·
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues">Request a Feature</a>
|
||||
·
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md">【Must Read Before Use】User Guide</a>
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<!-- LANGUAGE -->
|
||||
<p align="center">
|
||||
<a href="README.md">简体中文</a> |
|
||||
<a href="README-en.md">English</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📕 Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Usage Steps](#usage-steps)
|
||||
- [Versioning](#versioning)
|
||||
- [Authors](#authors)
|
||||
- [Important Notes](#important-notes)
|
||||
- [Special Thanks](#special-thanks)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 📥 Installation
|
||||
|
||||
Please download the latest version of the application from the [Releases Page](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/releases).
|
||||
|
||||

|
||||
|
||||
### ❗ Usage Steps
|
||||
|
||||
1. **Important**: Please be sure to read the [User Guide](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md) before use.
|
||||
2. **Detailed Tutorial**: Refer to the [Video Tutorial](https://www.bilibili.com/video/BV1hn9UYwE6p/).
|
||||
|
||||
### ⭕ Versioning
|
||||
|
||||
This project uses Git for version control. You can view the currently available versions on the [Releases Page](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/releases).
|
||||
|
||||
---
|
||||
|
||||
## 💡 Important Notes
|
||||
|
||||
1. **Do not use modified applications**: The authors and developers are not responsible for any personal loss resulting from the use of applications from unknown or modified sources.
|
||||
2. **Follow all rules**: Please strictly adhere to the rules in the [User Guide](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md) and this document. The authors and developers are not liable for any violations.
|
||||
3. **Free and Open Source**: This application is free and open-source. If you obtained it through a paid channel, please request a refund immediately and take action to protect your rights.
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Authors
|
||||
|
||||
- [ouyangqiqi](https://github.com/hyb-oyqq): Current maintainer of this repository.
|
||||
|
||||
## 🎉 Special Thanks
|
||||
- [Yanam1Anna](https://github.com/Yanam1Anna): The original author of this project, who provided extensive code and resources.
|
||||
- [HTony03](https://github.com/HTony03): Provided support for refactoring, logic optimization, and feature implementation for parts of the original source code.
|
||||
- [钨鸮](https://github.com/ABSIDIA): Provided support for cloud resource storage.
|
||||
- [XIU2/CloudflareSpeedTest](https://github.com/XIU2/CloudflareSpeedTest): Provided core support for the IP optimization feature of this project.
|
||||
|
||||
## 📖 License
|
||||
|
||||
This application is licensed under the [GPL-3.0](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE) license. Please see the [LICENSE](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE) file for more information.
|
||||
@@ -32,7 +32,7 @@
|
||||
<!-- LANGUAGE -->
|
||||
<p align="center">
|
||||
<a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">中文</a> |
|
||||
<a href="#">English</a>
|
||||
<a href="README-en.md">English</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
请从 [应用发布页面](https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/releases) 下载最新版本的应用程序。
|
||||
|
||||

|
||||

|
||||
|
||||
### ❗ 使用步骤
|
||||
|
||||
@@ -89,9 +89,9 @@
|
||||
|
||||
## 🎉 特别鸣谢
|
||||
- [Yanam1Anna](https://github.com/Yanam1Anna): 本项目的原作者,提供了大量代码和资源。
|
||||
- [HTony03](https://github.com/HTony03):对于本项目部分源码的重构、逻辑优化和功能实现提供了大力支持。
|
||||
|
||||
- [HTony03](https://github.com/HTony03):对于原项目部分源码的重构、逻辑优化和功能实现提供了支持。
|
||||
- [钨鸮](https://github.com/ABSIDIA):对于云端资源存储提供了支持。
|
||||
- [XIU2/CloudflareSpeedTest](https://github.com/XIU2/CloudflareSpeedTest):为本项目提供了 IP 优选功能的核心支持。
|
||||
|
||||
## 📖 协议
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
source/IMG/ICO/cloudflare_logo_icon.ico
Normal file
BIN
source/IMG/ICO/cloudflare_logo_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
11
source/core/__init__.py
Normal file
11
source/core/__init__.py
Normal file
@@ -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'
|
||||
]
|
||||
@@ -1,9 +1,11 @@
|
||||
from PySide6.QtCore import (QPropertyAnimation, QParallelAnimationGroup,
|
||||
QPoint, QEasingCurve, QTimer)
|
||||
from PySide6.QtCore import (QObject, QPropertyAnimation, QParallelAnimationGroup,
|
||||
QPoint, QEasingCurve, QTimer, Signal)
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect
|
||||
|
||||
class MultiStageAnimations:
|
||||
def __init__(self, ui):
|
||||
class MultiStageAnimations(QObject):
|
||||
animation_finished = Signal()
|
||||
def __init__(self, ui, parent=None):
|
||||
super().__init__(parent)
|
||||
self.ui = ui
|
||||
# 获取画布尺寸
|
||||
self.canvas_width = ui.centralwidget.width()
|
||||
@@ -141,6 +143,10 @@ class MultiStageAnimations:
|
||||
|
||||
anim_group.addAnimation(pos_anim)
|
||||
anim_group.addAnimation(opacity_anim)
|
||||
|
||||
if item["widget"] == self.ui.exit_btn:
|
||||
anim_group.finished.connect(self.animation_finished.emit)
|
||||
|
||||
anim_group.start()
|
||||
self.animations.append(anim_group)
|
||||
def start_animations(self):
|
||||
57
source/core/debug_manager.py
Normal file
57
source/core/debug_manager.py
Normal file
@@ -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
|
||||
515
source/core/download_manager.py
Normal file
515
source/core/download_manager.py
Normal file
@@ -0,0 +1,515 @@
|
||||
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 PySide6.QtGui import QIcon, QPixmap
|
||||
|
||||
from utils import msgbox_frame, HostsManager, resource_path
|
||||
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文件,并需要管理员权限。")
|
||||
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
else:
|
||||
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)
|
||||
|
||||
# 使用Cloudflare图标创建消息框
|
||||
|
||||
self.optimizing_msg_box = msgbox_frame(
|
||||
f"通知 - {APP_NAME}",
|
||||
"\n正在优选Cloudflare IP,请稍候...\n\n这可能需要5-10分钟,请耐心等待喵~"
|
||||
)
|
||||
# 设置Cloudflare图标
|
||||
cf_icon_path = resource_path("IMG/ICO/cloudflare_logo_icon.ico")
|
||||
if os.path.exists(cf_icon_path):
|
||||
cf_pixmap = QPixmap(cf_icon_path)
|
||||
if not cf_pixmap.isNull():
|
||||
self.optimizing_msg_box.setWindowIcon(QIcon(cf_pixmap))
|
||||
self.optimizing_msg_box.setIconPixmap(cf_pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation))
|
||||
|
||||
# 我们不再提供"跳过"按钮
|
||||
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()
|
||||
94
source/core/ui_manager.py
Normal file
94
source/core/ui_manager.py
Normal file
@@ -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"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
"""
|
||||
msg_box = msgbox_frame(
|
||||
f"关于 - {APP_NAME}",
|
||||
about_text,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText) # 使用Qt.TextFormat
|
||||
msg_box.exec()
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
|
||||
# 配置信息
|
||||
app_data = {
|
||||
"APP_VERSION": "1.1.0",
|
||||
"APP_VERSION": "1.1.3",
|
||||
"APP_NAME": "FRAISEMOE Addons Installer NEXT",
|
||||
"TEMP": "TEMP",
|
||||
"CACHE": "FRAISEMOE",
|
||||
BIN
source/icon.ico
BIN
source/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 264 KiB |
@@ -1,117 +1,28 @@
|
||||
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
|
||||
import webbrowser
|
||||
|
||||
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 MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -120,43 +31,43 @@ 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.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.config = load_config()
|
||||
|
||||
# 初始化状态变量
|
||||
self.cloud_config = None
|
||||
self.config_valid = False # 添加配置有效标志
|
||||
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):
|
||||
@@ -170,27 +81,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)
|
||||
|
||||
|
||||
# 根据初始配置决定是否开启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()
|
||||
@@ -199,397 +97,189 @@ class MainWindow(QMainWindow):
|
||||
QTimer.singleShot(100, self.start_animations)
|
||||
|
||||
def start_animations(self):
|
||||
"""开始启动动画"""
|
||||
# 不再禁用退出按钮的交互性,只通过样式表控制外观
|
||||
# 但仍然需要跟踪动画状态,防止用户在动画播放过程中退出
|
||||
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.animation_in_progress = False
|
||||
|
||||
# 只有在配置有效时才启用开始安装按钮
|
||||
if self.config_valid:
|
||||
self.ui.start_install_btn.setEnabled(True)
|
||||
else:
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
|
||||
def fetch_cloud_config(self):
|
||||
"""获取云端配置"""
|
||||
headers = {"User-Agent": UA}
|
||||
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:
|
||||
# 标记配置无效
|
||||
self.config_valid = False
|
||||
|
||||
if error_message == "update_required":
|
||||
msg_box = msgbox_frame(
|
||||
f"更新提示 - {APP_NAME}",
|
||||
"\n当前版本过低,请及时更新。\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 在浏览器中打开项目主页
|
||||
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/")
|
||||
# 强制关闭程序
|
||||
self.shutdown_app(force_exit=True)
|
||||
return
|
||||
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',
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 对于部分缺失,仍然允许使用,因为可能只影响部分游戏版本
|
||||
self.config_valid = True
|
||||
else:
|
||||
# 显示通用错误消息,只在debug模式下显示详细错误
|
||||
debug_mode = self.ui_manager.debug_action.isChecked() if self.ui_manager.debug_action else False
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if debug_mode and "详细错误:" in error_message:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n{error_message}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
else:
|
||||
msg_box = msgbox_frame(
|
||||
f"错误 - {APP_NAME}",
|
||||
f"\n{error_msg}\n",
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
msg_box.exec()
|
||||
# 在无法连接到云端时禁用开始安装按钮
|
||||
self.ui.start_install_btn.setEnabled(False)
|
||||
else:
|
||||
self.cloud_config = data
|
||||
# 标记配置有效
|
||||
self.config_valid = True
|
||||
|
||||
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))
|
||||
# 确保按钮在成功获取配置时启用
|
||||
self.ui.start_install_btn.setEnabled(True)
|
||||
|
||||
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:
|
||||
headers = {"User-Agent": UA}
|
||||
if self.debug_action.isChecked():
|
||||
print("--- Starting to get download URL ---")
|
||||
print(f"DEBUG: Requesting URL: {CONFIG_URL}")
|
||||
print(f"DEBUG: Using Headers: {headers}")
|
||||
|
||||
response = requests.get(CONFIG_URL, headers=headers, timeout=10)
|
||||
|
||||
if self.debug_action.isChecked():
|
||||
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 部分
|
||||
response_text = response.text
|
||||
json_start_index = response_text.find('{')
|
||||
if json_start_index == -1:
|
||||
raise ValueError("响应中未找到有效的 JSON 对象")
|
||||
|
||||
json_text = response_text[json_start_index:]
|
||||
config_data = json.loads(json_text)
|
||||
|
||||
if self.debug_action.isChecked():
|
||||
print(f"DEBUG: Parsed JSON data: {json.dumps(config_data, indent=2)}")
|
||||
|
||||
# 修正键名检查,确保所有必需的键都存在
|
||||
required_keys = [f"vol.{i+1}.data" for i in range(4)] + ["after.data"]
|
||||
if not all(key in config_data for key in required_keys):
|
||||
missing_keys = [key for key in required_keys if key not in config_data]
|
||||
raise ValueError(f"配置文件缺少必要的键: {', '.join(missing_keys)}")
|
||||
|
||||
# 修正提取URL的逻辑,确保使用正确的键
|
||||
urls = {
|
||||
f"vol{i+1}": config_data[f"vol.{i+1}.data"]["url"] for i in range(4)
|
||||
} | {
|
||||
"after": config_data["after.data"]["url"]
|
||||
}
|
||||
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()
|
||||
}
|
||||
Args:
|
||||
checked: 是否启用调试模式
|
||||
"""
|
||||
self.debug_manager.toggle_debug_mode(checked)
|
||||
|
||||
def save_config(self, config):
|
||||
"""保存配置的便捷方法"""
|
||||
save_config(config)
|
||||
|
||||
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")
|
||||
def create_download_thread(self, url, _7z_path, game_version):
|
||||
"""创建下载线程
|
||||
|
||||
self.setEnabled(True)
|
||||
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,
|
||||
)
|
||||
)
|
||||
Args:
|
||||
url: 下载URL
|
||||
_7z_path: 7z文件保存路径
|
||||
game_version: 游戏版本
|
||||
|
||||
Returns:
|
||||
DownloadThread: 下载线程实例
|
||||
"""
|
||||
return DownloadThread(url, _7z_path, game_version, self)
|
||||
|
||||
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是否重试?")
|
||||
def create_progress_window(self):
|
||||
"""创建下载进度窗口
|
||||
|
||||
Returns:
|
||||
ProgressWindow: 进度窗口实例
|
||||
"""
|
||||
return ProgressWindow(self)
|
||||
|
||||
def create_hash_thread(self, mode, install_paths):
|
||||
"""创建哈希检查线程
|
||||
|
||||
Args:
|
||||
mode: 检查模式,"pre"或"after"
|
||||
install_paths: 安装路径字典
|
||||
|
||||
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)
|
||||
Returns:
|
||||
HashThread: 哈希检查线程实例
|
||||
"""
|
||||
return HashThread(mode, install_paths, PLUGIN_HASH, self.installed_status, self)
|
||||
|
||||
def create_extraction_thread(self, _7z_path, game_folder, plugin_path, game_version):
|
||||
"""创建解压线程
|
||||
|
||||
Args:
|
||||
_7z_path: 7z文件路径
|
||||
game_folder: 游戏文件夹路径
|
||||
plugin_path: 插件路径
|
||||
game_version: 游戏版本
|
||||
|
||||
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 ---
|
||||
Returns:
|
||||
ExtractionThread: 解压线程实例
|
||||
"""
|
||||
return ExtractionThread(_7z_path, game_folder, plugin_path, game_version, self)
|
||||
|
||||
def after_hash_compare(self):
|
||||
"""进行安装后哈希比较"""
|
||||
# 禁用退出按钮
|
||||
self.ui.exit_btn.setEnabled(False)
|
||||
|
||||
self.hash_msg_box = self.hash_manager.hash_pop_window()
|
||||
self.setEnabled(False)
|
||||
|
||||
install_paths = self.download_manager.get_install_paths()
|
||||
|
||||
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()
|
||||
self.setEnabled(True)
|
||||
|
||||
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("\n是否愿意通过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()
|
||||
self.setEnabled(False)
|
||||
|
||||
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"
|
||||
)
|
||||
self.setEnabled(True)
|
||||
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.setEnabled(True)
|
||||
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()
|
||||
self.setEnabled(False)
|
||||
|
||||
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()
|
||||
self.setEnabled(True)
|
||||
"""哈希比较完成后的处理
|
||||
|
||||
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", "未知游戏")
|
||||
@@ -597,92 +287,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"""
|
||||
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
|
||||
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
|
||||
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
|
||||
"""
|
||||
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):
|
||||
self.hosts_manager.restore() # 恢复hosts文件
|
||||
self.stop_logging() # 确保在退出时停止日志记录
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"退出程序",
|
||||
"\n是否确定退出?\n",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
||||
QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
def shutdown_app(self, event=None, force_exit=False):
|
||||
"""关闭应用程序
|
||||
|
||||
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 reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
if (
|
||||
self.current_download_thread
|
||||
and self.current_download_thread.isRunning()
|
||||
):
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
f"错误 - {APP_NAME}",
|
||||
"\n当前有下载任务正在进行,完成后再试\n",
|
||||
)
|
||||
if 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
|
||||
|
||||
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)
|
||||
# 退出应用程序
|
||||
if event:
|
||||
event.accept()
|
||||
else:
|
||||
if event:
|
||||
event.ignore()
|
||||
sys.exit(0)
|
||||
@@ -1,13 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'install.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.9.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
from pic_data import img_data
|
||||
from data.pic_data import img_data
|
||||
from PySide6.QtGui import QPixmap
|
||||
import base64
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
@@ -42,8 +33,6 @@ class Ui_MainWindows(object):
|
||||
MainWindows.setAnimated(True)
|
||||
MainWindows.setDocumentMode(False)
|
||||
MainWindows.setDockNestingEnabled(False)
|
||||
self.action_2 = QAction(MainWindows)
|
||||
self.action_2.setObjectName(u"action_2")
|
||||
self.centralwidget = QWidget(MainWindows)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.centralwidget.setAutoFillBackground(True)
|
||||
@@ -140,8 +129,6 @@ class Ui_MainWindows(object):
|
||||
self.menubar.addAction(self.menu.menuAction())
|
||||
self.menubar.addAction(self.menu_2.menuAction())
|
||||
self.menu.addSeparator()
|
||||
self.menu.addAction(self.action_2)
|
||||
|
||||
self.retranslateUi(MainWindows)
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindows)
|
||||
@@ -149,7 +136,6 @@ class Ui_MainWindows(object):
|
||||
|
||||
def retranslateUi(self, MainWindows):
|
||||
MainWindows.setWindowTitle(QCoreApplication.translate("MainWindows", u" UI Test", None))
|
||||
self.action_2.setText(QCoreApplication.translate("MainWindows", u"\u68c0\u67e5\u66f4\u65b0(\u672a\u5b8c\u6210)", None))
|
||||
self.loadbg.setText("")
|
||||
self.vol1bg.setText("")
|
||||
self.vol2bg.setText("")
|
||||
18
source/utils/__init__.py
Normal file
18
source/utils/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from .logger import Logger
|
||||
from .helpers import (
|
||||
load_base64_image, HashManager, AdminPrivileges, msgbox_frame,
|
||||
load_config, save_config, HostsManager, censor_url, resource_path
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Logger',
|
||||
'load_base64_image',
|
||||
'HashManager',
|
||||
'AdminPrivileges',
|
||||
'msgbox_frame',
|
||||
'load_config',
|
||||
'save_config',
|
||||
'HostsManager',
|
||||
'censor_url',
|
||||
'resource_path'
|
||||
]
|
||||
@@ -9,17 +9,27 @@ import psutil
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
import re
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from pic_data import img_data
|
||||
from config import APP_NAME, CONFIG_FILE
|
||||
from data.pic_data import img_data
|
||||
from data.config import APP_NAME, CONFIG_FILE
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源的绝对路径,适用于开发环境和PyInstaller打包环境"""
|
||||
"""获取资源的绝对路径,适用于开发环境和Nuitka打包环境"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中
|
||||
base_path = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
|
||||
# Nuitka/PyInstaller创建的临时文件夹,并将路径存储在_MEIPASS中或与可执行文件同目录
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 在开发环境中运行
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# 处理特殊的可执行文件和数据文件路径
|
||||
if relative_path in ("aria2c.exe", "cfst.exe"):
|
||||
return os.path.join(base_path, 'bin', relative_path)
|
||||
elif relative_path in ("ip.txt", "ipv6.txt"):
|
||||
return os.path.join(base_path, 'data', relative_path)
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
def load_base64_image(base64_str):
|
||||
@@ -224,6 +234,7 @@ class HostsManager:
|
||||
self.backup_path = os.path.join(os.path.dirname(self.hosts_path), f'hosts.bak.{APP_NAME}')
|
||||
self.original_content = None
|
||||
self.modified = False
|
||||
self.modified_hostnames = set() # 跟踪被修改的主机名
|
||||
|
||||
def backup(self):
|
||||
if not AdminPrivileges().is_admin():
|
||||
@@ -241,6 +252,48 @@ class HostsManager:
|
||||
msg_box = msgbox_frame(f"错误 - {APP_NAME}", f"\n无法备份hosts文件,请检查权限。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
return False
|
||||
|
||||
def clean_hostname_entries(self, hostname):
|
||||
"""清理hosts文件中指定域名的所有记录
|
||||
|
||||
Args:
|
||||
hostname: 要清理的域名
|
||||
|
||||
Returns:
|
||||
bool: 清理是否成功
|
||||
"""
|
||||
if not self.original_content:
|
||||
if not self.backup():
|
||||
return False
|
||||
|
||||
# 确保original_content不为None
|
||||
if not self.original_content:
|
||||
print("无法读取hosts文件内容,操作中止。")
|
||||
return False
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来修改hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
lines = self.original_content.splitlines()
|
||||
new_lines = [line for line in lines if hostname not in line]
|
||||
|
||||
# 如果没有变化,不需要写入
|
||||
if len(new_lines) == len(lines):
|
||||
print(f"Hosts文件中没有找到 {hostname} 的记录")
|
||||
return True
|
||||
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
|
||||
# 更新原始内容
|
||||
self.original_content = '\n'.join(new_lines)
|
||||
print(f"已从hosts文件中清理 {hostname} 的记录")
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"清理hosts文件失败: {e}")
|
||||
return False
|
||||
|
||||
def apply_ip(self, hostname, ip_address):
|
||||
if not self.original_content:
|
||||
@@ -256,17 +309,23 @@ class HostsManager:
|
||||
return False
|
||||
|
||||
try:
|
||||
lines = self.original_content.splitlines()
|
||||
new_lines = [line for line in lines if not (hostname in line and line.strip().startswith(ip_address))]
|
||||
# 首先清理已有的同域名记录
|
||||
self.clean_hostname_entries(hostname)
|
||||
|
||||
# 然后添加新记录
|
||||
lines = self.original_content.splitlines()
|
||||
new_entry = f"{ip_address}\t{hostname}"
|
||||
new_lines.append(f"\n# Added by {APP_NAME}")
|
||||
new_lines.append(new_entry)
|
||||
lines.append(f"\n# Added by {APP_NAME}")
|
||||
lines.append(new_entry)
|
||||
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
# 更新原始内容
|
||||
self.original_content = '\n'.join(lines)
|
||||
self.modified = True
|
||||
# 记录被修改的主机名,用于最终清理
|
||||
self.modified_hostnames.add(hostname)
|
||||
print(f"Hosts文件已更新: {new_entry}")
|
||||
return True
|
||||
except IOError as e:
|
||||
@@ -275,6 +334,55 @@ class HostsManager:
|
||||
msg_box.exec()
|
||||
return False
|
||||
|
||||
def check_and_clean_all_entries(self):
|
||||
"""检查并清理所有由本应用程序添加的hosts记录
|
||||
|
||||
Returns:
|
||||
bool: 清理是否成功
|
||||
"""
|
||||
if not AdminPrivileges().is_admin():
|
||||
print("需要管理员权限来检查和清理hosts文件。")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 读取当前hosts文件内容
|
||||
with open(self.hosts_path, 'r', encoding='utf-8') as f:
|
||||
current_content = f.read()
|
||||
|
||||
lines = current_content.splitlines()
|
||||
new_lines = []
|
||||
skip_next = False
|
||||
|
||||
for line in lines:
|
||||
# 如果上一行是我们的注释标记,跳过当前行
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
|
||||
# 检查是否是我们添加的注释行
|
||||
if f"# Added by {APP_NAME}" in line:
|
||||
skip_next = True # 跳过下一行(实际的hosts记录)
|
||||
continue
|
||||
|
||||
# 保留其他所有行
|
||||
new_lines.append(line)
|
||||
|
||||
# 检查是否有变化
|
||||
if len(new_lines) == len(lines):
|
||||
print("Hosts文件中没有找到由本应用添加的记录")
|
||||
return True
|
||||
|
||||
# 写回清理后的内容
|
||||
with open(self.hosts_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
|
||||
print(f"已清理所有由 {APP_NAME} 添加的hosts记录")
|
||||
return True
|
||||
|
||||
except IOError as e:
|
||||
print(f"检查和清理hosts文件失败: {e}")
|
||||
return False
|
||||
|
||||
def restore(self):
|
||||
if not self.modified:
|
||||
if os.path.exists(self.backup_path):
|
||||
@@ -282,6 +390,8 @@ class HostsManager:
|
||||
os.remove(self.backup_path)
|
||||
except OSError:
|
||||
pass
|
||||
# 即使没有修改过,也检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
|
||||
if not AdminPrivileges().is_admin():
|
||||
@@ -299,6 +409,8 @@ class HostsManager:
|
||||
os.remove(self.backup_path)
|
||||
except OSError:
|
||||
pass
|
||||
# 恢复后再检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
except IOError as e:
|
||||
print(f"从内存恢复hosts文件失败: {e}")
|
||||
@@ -309,6 +421,8 @@ class HostsManager:
|
||||
def restore_from_backup_file(self):
|
||||
if not os.path.exists(self.backup_path):
|
||||
print("未找到hosts备份文件,无法恢复。")
|
||||
# 即使没有备份文件,也尝试清理可能的残留
|
||||
self.check_and_clean_all_entries()
|
||||
return False
|
||||
try:
|
||||
with open(self.backup_path, 'r', encoding='utf-8') as bf:
|
||||
@@ -318,11 +432,15 @@ class HostsManager:
|
||||
os.remove(self.backup_path)
|
||||
self.modified = False
|
||||
print("Hosts文件已从备份文件恢复。")
|
||||
# 恢复后再检查一次是否有残留
|
||||
self.check_and_clean_all_entries()
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
print(f"从备份文件恢复hosts失败: {e}")
|
||||
msg_box = msgbox_frame(f"警告 - {APP_NAME}", f"\n自动恢复hosts文件失败,请手动从 {self.backup_path} 恢复。\n\n【错误信息】:{e}\n", QtWidgets.QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
# 尽管恢复失败,仍然尝试清理可能的残留
|
||||
self.check_and_clean_all_entries()
|
||||
return False
|
||||
|
||||
def censor_url(text):
|
||||
19
source/utils/logger.py
Normal file
19
source/utils/logger.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .helpers import censor_url
|
||||
|
||||
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()
|
||||
14
source/workers/__init__.py
Normal file
14
source/workers/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .hash_thread import HashThread
|
||||
from .extraction_thread import ExtractionThread
|
||||
from .config_fetch_thread import ConfigFetchThread
|
||||
from .ip_optimizer import IpOptimizerThread
|
||||
from .download import DownloadThread, ProgressWindow
|
||||
|
||||
__all__ = [
|
||||
'IpOptimizerThread',
|
||||
'HashThread',
|
||||
'ExtractionThread',
|
||||
'ConfigFetchThread',
|
||||
'DownloadThread',
|
||||
'ProgressWindow'
|
||||
]
|
||||
65
source/workers/config_fetch_thread.py
Normal file
65
source/workers/config_fetch_thread.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
import requests
|
||||
import webbrowser
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
import sys
|
||||
|
||||
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()
|
||||
|
||||
# 检查是否是要求更新的错误信息 - 使用Unicode编码的更新提示文本
|
||||
update_required_msg = "\u8bf7\u4f7f\u7528\u6700\u65b0\u7248\u672c\u7684FraiseMoe2-Next\u8fdb\u884c\u4e0b\u8f7d"
|
||||
if isinstance(config_data, str) and config_data == update_required_msg:
|
||||
self.finished.emit(None, "update_required")
|
||||
return
|
||||
elif isinstance(config_data, dict) and config_data.get("message") == update_required_msg:
|
||||
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:
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if self.debug_mode:
|
||||
error_msg += f"\n详细错误: {e}"
|
||||
self.finished.emit(None, error_msg)
|
||||
except (ValueError, json.JSONDecodeError) as e:
|
||||
error_msg = "访问云端配置失败,请检查网络状况或稍后再试。"
|
||||
if self.debug_mode:
|
||||
error_msg += f"\nJSON解析失败: {e}"
|
||||
self.finished.emit(None, error_msg)
|
||||
finally:
|
||||
if self.debug_mode:
|
||||
print("--- Finished fetching cloud config ---")
|
||||
@@ -7,7 +7,7 @@ from PySide6 import QtCore, QtWidgets
|
||||
from PySide6.QtCore import (Qt, Signal, QThread, QTimer)
|
||||
from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog)
|
||||
from utils import resource_path
|
||||
from config import APP_NAME, UA
|
||||
from data.config import APP_NAME, UA
|
||||
|
||||
# 下载线程类
|
||||
class DownloadThread(QThread):
|
||||
@@ -182,5 +182,4 @@ class ProgressWindow(QDialog):
|
||||
|
||||
def closeEvent(self, event):
|
||||
# 覆盖默认的关闭事件,防止用户通过其他方式关闭窗口
|
||||
# 如果需要,可以在这里添加逻辑,例如询问用户是否要停止下载
|
||||
event.ignore()
|
||||
31
source/workers/extraction_thread.py
Normal file
31
source/workers/extraction_thread.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
import shutil
|
||||
import py7zr
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from data.config import PLUGIN, GAME_INFO
|
||||
|
||||
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)
|
||||
28
source/workers/hash_thread.py
Normal file
28
source/workers/hash_thread.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import HashManager
|
||||
from data.config import BLOCK_SIZE
|
||||
|
||||
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)
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from utils import resource_path
|
||||
|
||||
class IpOptimizer:
|
||||
@@ -28,13 +29,16 @@ class IpOptimizer:
|
||||
return None
|
||||
|
||||
ip_txt_path = resource_path("ip.txt")
|
||||
|
||||
# 正确的参数设置,根据cfst帮助文档
|
||||
command = [
|
||||
cst_path,
|
||||
"-p", "1",
|
||||
"-o", "",
|
||||
"-url", url,
|
||||
"-f", ip_txt_path,
|
||||
"-dd",
|
||||
"-n", "500", # 延迟测速线程数 (默认200)
|
||||
"-p", "1", # 显示结果数量 (默认10个)
|
||||
"-url", url, # 指定测速地址
|
||||
"-f", ip_txt_path, # IP文件
|
||||
"-dd", # 禁用下载测速,按延迟排序
|
||||
"-o","" # 不写入结果文件
|
||||
]
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||
@@ -53,17 +57,13 @@ class IpOptimizer:
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
# 立即向 stdin 发送换行符,以便程序在 Windows 下正常退出
|
||||
if self.process.stdin:
|
||||
try:
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
self.process.stdin.close()
|
||||
|
||||
ip_pattern = re.compile(r'^\s*([\d\.]+)\s+\d+\s+\d+\s+[\d\.]+%?\s+[\d\.]+\s+[\d\.]+\s+.*$')
|
||||
# 更新正则表达式以匹配cfst输出中的IP格式
|
||||
# 匹配格式: IP地址在行首,后面跟着一些数字和文本
|
||||
ip_pattern = re.compile(r'^(\d+\.\d+\.\d+\.\d+)\s+.*')
|
||||
|
||||
# 标记是否已经找到结果表头和完成标记
|
||||
found_header = False
|
||||
found_completion = False
|
||||
|
||||
stdout = self.process.stdout
|
||||
if not stdout:
|
||||
@@ -72,7 +72,7 @@ class IpOptimizer:
|
||||
|
||||
optimal_ip = None
|
||||
timeout_counter = 0
|
||||
max_timeout = 60
|
||||
max_timeout = 300 # 增加超时时间到5分钟
|
||||
|
||||
while True:
|
||||
if self.process.poll() is not None:
|
||||
@@ -97,16 +97,45 @@ class IpOptimizer:
|
||||
cleaned_line = line.strip()
|
||||
if cleaned_line:
|
||||
print(cleaned_line)
|
||||
match = ip_pattern.match(cleaned_line)
|
||||
if match:
|
||||
optimal_ip = match.group(1)
|
||||
print(f"找到最优 IP: {optimal_ip}, 正在终止测速进程...")
|
||||
break
|
||||
|
||||
# 检测结果表头
|
||||
if "IP 地址" in cleaned_line and "平均延迟" in cleaned_line:
|
||||
print("检测到IP结果表头,准备获取IP地址...")
|
||||
found_header = True
|
||||
continue
|
||||
|
||||
# 检测完成标记
|
||||
if "完整测速结果已写入" in cleaned_line or "按下 回车键 或 Ctrl+C 退出" in cleaned_line:
|
||||
print("检测到测速完成信息")
|
||||
found_completion = True
|
||||
|
||||
# 如果已经找到了IP,可以退出了
|
||||
if optimal_ip:
|
||||
break
|
||||
|
||||
# 已找到表头后,尝试匹配IP地址行
|
||||
if found_header:
|
||||
match = ip_pattern.search(cleaned_line)
|
||||
if match and not optimal_ip: # 只保存第一个匹配的IP(最优IP)
|
||||
optimal_ip = match.group(1)
|
||||
print(f"找到最优 IP: {optimal_ip}")
|
||||
# 找到最优IP后立即退出循环,不等待完成标记
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取输出时发生错误: {e}")
|
||||
break
|
||||
|
||||
# 确保完全读取输出后再发送退出信号
|
||||
if self.process and self.process.poll() is None:
|
||||
try:
|
||||
if self.process.stdin and not self.process.stdin.closed:
|
||||
print("发送退出信号...")
|
||||
self.process.stdin.write('\n')
|
||||
self.process.stdin.flush()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.stop()
|
||||
|
||||
print("--- CloudflareSpeedTest 执行结束 ---")
|
||||
@@ -135,6 +164,24 @@ class IpOptimizer:
|
||||
self.process.wait()
|
||||
print("CloudflareSpeedTest 进程已终止。")
|
||||
|
||||
|
||||
class IpOptimizerThread(QThread):
|
||||
"""用于在后台线程中运行IP优化的类"""
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 用于直接测试此模块
|
||||
test_url = "https://speed.cloudflare.com/__down?during=download&bytes=104857600"
|
||||
@@ -143,4 +190,4 @@ if __name__ == '__main__':
|
||||
if ip:
|
||||
print(f"为 {test_url} 找到的最优 IP 是: {ip}")
|
||||
else:
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||
print(f"未能为 {test_url} 找到最优 IP。")
|
||||
Reference in New Issue
Block a user