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