Files
FRAISEMOE2-Installer/core.py

254 lines
9.2 KiB
Python
Raw Normal View History

2025-07-03 23:39:31 +08:00
import os
import shutil
import requests
import py7zr
import hashlib
import psutil
import ctypes
import base64
from PySide6.QtWidgets import (QMessageBox, QFileDialog, QProgressDialog)
from PySide6.QtCore import QThread, Signal
# 配置信息(完全保持不变)
app_data = {
"APP_VERSION": "4.10.0.17496",
"APP_NAME": "@FRAISEMOE Addons Installer",
"TEMP": "TEMP",
"CACHE": "FRAISEMOE",
"PLUGIN": "PLUGIN",
"CONFIG_URL": "aHR0cHM6Ly9hcmNoaXZlLm92b2Zpc2guY29tL2FwaS93aWRnZXQvbmVrb3BhcmEvZG93bmxvYWRfdXJsLmpzb24=",
"UA": "TW96aWxsYS81LjAgKExpbnV4IGRlYmlhbjEyIEZyYWlzZU1vZS1BY2NlcHQpIEdlY2tvLzIwMTAwMTAxIEZpcmVmb3gvMTE0LjA=",
"game_info": {
"NEKOPARA Vol.1": {
"exe": "nekopara_vol1.exe",
"hash": "04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e",
"install_path": "NEKOPARA Vol. 1/adultsonly.xp3",
"plugin_path": "vol.1/adultsonly.xp3",
},
"NEKOPARA Vol.2": {
"exe": "nekopara_vol2.exe",
"hash": "b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42",
"install_path": "NEKOPARA Vol. 2/adultsonly.xp3",
"plugin_path": "vol.2/adultsonly.xp3",
},
"NEKOPARA Vol.3": {
"exe": "NEKOPARAvol3.exe",
"hash": "2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5",
"install_path": "NEKOPARA Vol. 3/update00.int",
"plugin_path": "vol.3/update00.int",
},
"NEKOPARA Vol.4": {
"exe": "nekopara_vol4.exe",
"hash": "4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a",
"install_path": "NEKOPARA Vol. 4/vol4adult.xp3",
"plugin_path": "vol.4/vol4adult.xp3",
},
},
}
def decode_base64(encoded_str):
"""Base64解码函数"""
return base64.b64decode(encoded_str).decode("utf-8")
class DownloadThread(QThread):
progress_updated = Signal(int)
download_finished = Signal(bool, str)
def __init__(self, url, save_path, headers):
super().__init__()
self.url = url
self.save_path = save_path
self.headers = headers
def run(self):
try:
response = requests.get(self.url, headers=self.headers, stream=True)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(self.save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
progress = int((downloaded / total_size) * 100)
self.progress_updated.emit(progress)
self.download_finished.emit(True, "")
except Exception as e:
self.download_finished.emit(False, str(e))
class AppCore:
def __init__(self, ui):
self.ui = ui
self.install_dir = ""
self.temp_dir = os.path.join(os.getenv(app_data["TEMP"]), app_data["CACHE"])
self.plugin_dir = os.path.join(self.temp_dir, app_data["PLUGIN"])
self._create_dirs() # 确保这个方法被正确定义
# 解码配置
self.CONFIG_URL = decode_base64(app_data["CONFIG_URL"])
self.UA = decode_base64(app_data["UA"]) + f" FraiseMoe/{app_data['APP_VERSION']}"
self.GAME_INFO = app_data["game_info"]
self.current_download_thread = None
def _create_dirs(self):
"""创建必要的目录"""
os.makedirs(self.plugin_dir, exist_ok=True)
def start_installation(self):
"""开始安装流程"""
if not self._select_install_dir():
return
if not self._check_processes():
return
self._download_and_install()
def _select_install_dir(self):
"""选择安装目录"""
dir_path = QFileDialog.getExistingDirectory(
self.ui, "选择游戏安装目录"
)
if not dir_path:
QMessageBox.warning(self.ui, "警告", "必须选择安装目录")
return False
self.install_dir = dir_path
return True
def _check_processes(self):
"""检查游戏进程"""
for proc in psutil.process_iter(['name']):
if proc.info['name'] in [info['exe'] for info in self.GAME_INFO.values()]:
reply = QMessageBox.question(
self.ui, "警告",
"检测到游戏正在运行,需要关闭才能继续安装",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
try:
proc.kill()
except:
QMessageBox.critical(
self.ui, "错误",
"无法关闭游戏进程,请手动关闭"
)
return False
else:
return False
return True
def _get_download_config(self):
"""获取下载配置"""
try:
headers = {"User-Agent": self.UA}
response = requests.get(self.CONFIG_URL, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
QMessageBox.critical(
self.ui, "错误",
f"获取下载配置失败: {str(e)}"
)
return None
def _download_and_install(self):
"""下载并安装文件"""
config = self._get_download_config()
if not config:
return
# 创建进度对话框
progress = QProgressDialog("下载补丁文件中...", "取消", 0, 100, self.ui)
progress.setWindowTitle("安装进度")
progress.setAutoClose(True)
# 为每个游戏版本下载补丁
for game_name, game_info in self.GAME_INFO.items():
vol_key = f"vol.{game_name.split()[-1]}.data"
if vol_key not in config:
continue
download_url = config[vol_key]["url"]
temp_file = os.path.join(self.plugin_dir, f"{vol_key}.7z")
# 启动下载线程
self.current_download_thread = DownloadThread(
download_url,
temp_file,
{"User-Agent": self.UA}
)
self.current_download_thread.progress_updated.connect(progress.setValue)
self.current_download_thread.download_finished.connect(
lambda success, err, f=temp_file, g=game_name, i=game_info:
self._handle_download_result(success, err, f, g, i)
)
self.current_download_thread.start()
progress.exec_()
def _handle_download_result(self, success, error, temp_file, game_name, game_info):
"""处理下载结果"""
if not success:
QMessageBox.critical(self.ui, "下载失败", f"错误: {error}")
return
try:
# 解压文件
with py7zr.SevenZipFile(temp_file, mode='r') as archive:
archive.extractall(path=self.plugin_dir)
# 复制到游戏目录
plugin_path = os.path.join(self.plugin_dir, game_info["plugin_path"])
install_path = os.path.join(self.install_dir, game_info["install_path"])
os.makedirs(os.path.dirname(install_path), exist_ok=True)
shutil.copy(plugin_path, install_path)
# 验证哈希
if self._verify_hash(install_path, game_info["hash"]):
QMessageBox.information(
self.ui, "安装完成",
f"{game_name} 补丁已成功安装!"
)
else:
QMessageBox.warning(
self.ui, "警告",
f"{game_name} 文件校验失败,安装可能不完整"
)
except Exception as e:
QMessageBox.critical(
self.ui, "安装失败",
f"安装过程中出错: {str(e)}"
)
finally:
# 清理临时文件
if os.path.exists(temp_file):
os.remove(temp_file)
def _verify_hash(self, file_path, expected_hash):
"""验证文件哈希"""
if not os.path.exists(file_path):
return False
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(8192), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest() == expected_hash
def shutdown_app(self):
"""关闭应用程序"""
reply = QMessageBox.question(
self.ui, "退出",
"确定要退出安装程序吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
# 清理临时目录
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
self.ui.close()