Files
FRAISEMOE2-Installer/core.py
2025-07-03 23:39:31 +08:00

254 lines
9.2 KiB
Python

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()