import os import shutil import py7zr import tempfile import traceback from PySide6.QtCore import QThread, Signal from config.config import PLUGIN, GAME_INFO import time # 用于时间计算 import threading import queue from concurrent.futures import TimeoutError class ExtractionThread(QThread): finished = Signal(bool, str, str) # success, error_message, game_version progress = Signal(int, str) # 添加进度信号,传递进度百分比和状态信息 def __init__(self, _7z_path, game_folder, plugin_path, game_version, parent=None, extracted_path=None): super().__init__(parent) self._7z_path = _7z_path self.game_folder = game_folder self.plugin_path = plugin_path self.game_version = game_version self.extracted_path = extracted_path # 添加已解压文件路径参数 def run(self): try: # 确保游戏目录存在 os.makedirs(self.game_folder, exist_ok=True) def update_progress(percent: int, message: str): try: self.progress.emit(percent, message) except Exception: pass # 记录调试信息 from utils.logger import setup_logger debug_logger = setup_logger("extraction_thread") debug_logger.info(f"====== 开始处理 {self.game_version} 补丁文件 ======") debug_logger.info(f"压缩包路径: {self._7z_path}") debug_logger.info(f"游戏目录: {self.game_folder}") debug_logger.info(f"插件路径: {self.plugin_path}") update_progress(0, f"开始处理 {self.game_version} 的补丁文件...") # 支持外部请求中断 if self.isInterruptionRequested(): self.finished.emit(False, "操作已取消", self.game_version) return # 如果提供了已解压文件路径,直接使用它 if self.extracted_path and os.path.exists(self.extracted_path): update_progress(20, f"正在复制 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)") # 直接复制已解压的文件到游戏目录 target_file = os.path.join(self.game_folder, os.path.basename(self.plugin_path)) shutil.copy(self.extracted_path, target_file) update_progress(60, f"正在完成 {self.game_version} 的补丁安装...") # 对于NEKOPARA After,还需要复制签名文件 if self.game_version == "NEKOPARA After": try: update_progress(70, f"正在处理 {self.game_version} 的签名文件...") # 从已解压文件的目录中获取签名文件 extracted_dir = os.path.dirname(self.extracted_path) sig_filename = os.path.basename(GAME_INFO[self.game_version]["sig_path"]) sig_path = os.path.join(extracted_dir, sig_filename) # 尝试多种可能的签名文件路径 if not os.path.exists(sig_path): # 尝试在同级目录查找 sig_path = os.path.join(os.path.dirname(extracted_dir), sig_filename) # 如果签名文件存在,则复制它 if os.path.exists(sig_path): target_sig = os.path.join(self.game_folder, sig_filename) shutil.copy(sig_path, target_sig) update_progress(80, f"签名文件复制完成") else: # 如果签名文件不存在,则使用原始路径 sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"]) if os.path.exists(sig_path): target_sig = os.path.join(self.game_folder, os.path.basename(sig_path)) shutil.copy(sig_path, target_sig) update_progress(80, f"使用内置签名文件完成") else: update_progress(80, f"未找到签名文件,继续安装主补丁文件") except Exception as sig_err: # 签名文件处理失败时记录错误但不中断主流程 update_progress(80, f"签名文件处理失败: {str(sig_err)}") update_progress(100, f"{self.game_version} 补丁文件处理完成") self.finished.emit(True, "", self.game_version) return # 否则解压源压缩包到临时目录,再复制目标文件 update_progress(10, f"正在打开 {self.game_version} 的补丁压缩包...") with py7zr.SevenZipFile(self._7z_path, mode="r") as archive: # 获取压缩包内的文件列表 file_list = archive.getnames() # 详细记录压缩包中的所有文件 debug_logger.debug(f"压缩包内容分析:") debug_logger.debug(f"- 文件总数: {len(file_list)}") for i, f in enumerate(file_list): debug_logger.debug(f" {i+1}. {f} - 类型: {'文件夹' if f.endswith('/') or f.endswith('\\') else '文件'}") update_progress(20, f"正在分析 {self.game_version} 的补丁文件...") update_progress(30, f"正在解压 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)") with tempfile.TemporaryDirectory() as temp_dir: # 查找主补丁文件和签名文件 target_filename = os.path.basename(self.plugin_path) # 只有NEKOPARA After版本才需要查找签名文件 if self.game_version == "NEKOPARA After": sig_filename = target_filename + ".sig" # 签名文件名 debug_logger.debug(f"查找主补丁文件: {target_filename}") debug_logger.debug(f"查找签名文件: {sig_filename}") else: sig_filename = None debug_logger.debug(f"查找主补丁文件: {target_filename}") debug_logger.debug(f"{self.game_version} 不需要签名文件") target_file_in_archive = None sig_file_in_archive = None # 对于NEKOPARA After,增加特殊处理 if self.game_version == "NEKOPARA After": # 增加专门的检查,同时识别主补丁和签名文件 debug_logger.debug("执行NEKOPARA After特殊补丁文件识别") # 查找主补丁和签名文件 for file_path in file_list: basename = os.path.basename(file_path) # 查找主补丁文件 if basename == "afteradult.xp3" and not basename.endswith('.sig'): target_file_in_archive = file_path debug_logger.debug(f"找到精确匹配的After主补丁文件: {target_file_in_archive}") # 查找签名文件 elif basename == "afteradult.xp3.sig" or basename.endswith('.sig'): sig_file_in_archive = file_path debug_logger.debug(f"找到After签名文件: {sig_file_in_archive}") # 如果没找到主补丁文件,寻找可能的替代文件 if not target_file_in_archive: for file_path in file_list: if "afteradult.xp3" in file_path and not file_path.endswith('.sig'): target_file_in_archive = file_path debug_logger.debug(f"找到备选After主补丁文件: {target_file_in_archive}") break else: # 标准处理逻辑 for file_path in file_list: basename = os.path.basename(file_path) # 查找主补丁文件 if basename == target_filename and not basename.endswith('.sig'): target_file_in_archive = file_path debug_logger.debug(f"在压缩包中找到主补丁文件: {target_file_in_archive}") # 查找签名文件 elif basename == sig_filename: sig_file_in_archive = file_path debug_logger.debug(f"在压缩包中找到签名文件: {sig_file_in_archive}") # 如果没有找到精确匹配的主补丁文件,使用更宽松的搜索 if not target_file_in_archive: debug_logger.warning(f"没有找到精确匹配的主补丁文件,尝试更宽松的搜索") for file_path in file_list: if target_filename in file_path and not file_path.endswith('.sig'): target_file_in_archive = file_path debug_logger.info(f"在压缩包中找到可能的主补丁文件: {target_file_in_archive}") break # 如果找不到主补丁文件,使用回退方案:提取全部内容 if not target_file_in_archive: debug_logger.warning(f"未能识别正确的主补丁文件,将提取所有文件并尝试查找") # 提取所有文件到临时目录 update_progress(30, f"正在解压所有文件...") archive.extractall(path=temp_dir) debug_logger.debug(f"已提取所有文件到临时目录") # 在提取的文件中查找主补丁文件和签名文件 found_main = False found_sig = False for root, dirs, files in os.walk(temp_dir): for file in files: # 查找主补丁文件 if file == target_filename and not file.endswith('.sig'): extracted_file_path = os.path.join(root, file) file_size = os.path.getsize(extracted_file_path) debug_logger.debug(f"在提取的文件中找到主补丁文件: {extracted_file_path}, 大小: {file_size} 字节") # 复制到目标位置 target_path = os.path.join(self.game_folder, target_filename) shutil.copy2(extracted_file_path, target_path) debug_logger.debug(f"已复制主补丁文件到: {target_path}") found_main = True # 查找签名文件 elif file == sig_filename or file.endswith('.sig'): extracted_sig_path = os.path.join(root, file) sig_size = os.path.getsize(extracted_sig_path) debug_logger.debug(f"在提取的文件中找到签名文件: {extracted_sig_path}, 大小: {sig_size} 字节") # 复制到目标位置 sig_target = os.path.join(self.game_folder, sig_filename) shutil.copy2(extracted_sig_path, sig_target) debug_logger.debug(f"已复制签名文件到: {sig_target}") found_sig = True # 如果两个文件都找到,可以停止遍历 if found_main and found_sig: debug_logger.debug("已找到所有需要的文件,停止遍历") break if found_main and found_sig: break if not found_main: debug_logger.error(f"无法找到主补丁文件,安装失败") raise FileNotFoundError(f"在压缩包中未找到主补丁文件 {target_filename}") # 只有NEKOPARA After版本才需要处理签名文件 if self.game_version == "NEKOPARA After": # 签名文件没找到不影响主流程,但记录警告 if not found_sig: debug_logger.warning(f"未找到签名文件 {sig_filename},但继续安装主补丁文件") else: debug_logger.info(f"{self.game_version} 不需要签名文件,跳过签名文件处理") else: # 准备要解压的文件列表 files_to_extract = [target_file_in_archive] # 只有NEKOPARA After版本才需要解压签名文件 if self.game_version == "NEKOPARA After" and sig_file_in_archive: files_to_extract.append(sig_file_in_archive) debug_logger.debug(f"将同时解压主补丁文件和签名文件: {files_to_extract}") else: debug_logger.debug(f"将仅解压主补丁文件: {files_to_extract}") # 解压选定的文件到临时目录 debug_logger.debug(f"开始解压选定文件到临时目录: {temp_dir}") # 设置解压超时时间(秒) extract_timeout = 180 # 3分钟超时 debug_logger.debug(f"设置解压超时: {extract_timeout}秒") # 创建子线程执行解压 import threading import queue extract_result = queue.Queue() def extract_files(): try: archive.extract(path=temp_dir, targets=files_to_extract) extract_result.put(("success", None)) except Exception as e: extract_result.put(("error", e)) extract_thread = threading.Thread(target=extract_files) extract_thread.daemon = True extract_thread.start() # 每5秒更新一次进度,最多等待设定的超时时间 total_waited = 0 while extract_thread.is_alive() and total_waited < extract_timeout: update_progress(30 + int(30 * total_waited / extract_timeout), f"正在解压文件...已等待{total_waited}秒") extract_thread.join(5) # 等待5秒 total_waited += 5 # 检查是否超时 if extract_thread.is_alive(): debug_logger.error(f"解压超时(超过{extract_timeout}秒)") raise TimeoutError(f"解压超时(超过{extract_timeout}秒),请检查补丁文件是否完整") # 检查解压结果 if not extract_result.empty(): status, error = extract_result.get() if status == "error": debug_logger.error(f"解压错误: {error}") raise error debug_logger.debug(f"文件解压完成") update_progress(60, f"正在复制 {self.game_version} 的补丁文件...") # 复制主补丁文件到游戏目录 extracted_file_path = os.path.join(temp_dir, target_file_in_archive) # 检查解压后的文件是否存在及其大小 if os.path.exists(extracted_file_path): file_size = os.path.getsize(extracted_file_path) debug_logger.debug(f"解压后的主补丁文件存在: {extracted_file_path}, 大小: {file_size} 字节") else: debug_logger.error(f"解压后的主补丁文件不存在: {extracted_file_path}") raise FileNotFoundError(f"解压后的文件不存在: {extracted_file_path}") # 构建目标路径并复制 target_path = os.path.join(self.game_folder, target_filename) debug_logger.debug(f"复制主补丁文件: {extracted_file_path} 到 {target_path}") shutil.copy2(extracted_file_path, target_path) # 验证主补丁文件是否成功复制 if os.path.exists(target_path): target_size = os.path.getsize(target_path) debug_logger.debug(f"主补丁文件成功复制: {target_path}, 大小: {target_size} 字节") else: debug_logger.error(f"主补丁文件复制失败: {target_path}") raise FileNotFoundError(f"目标文件复制失败: {target_path}") # 只有NEKOPARA After版本才需要处理签名文件 if self.game_version == "NEKOPARA After": # 如果有找到签名文件,也复制它 if sig_file_in_archive: update_progress(80, f"正在复制签名文件...") extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive) if os.path.exists(extracted_sig_path): sig_size = os.path.getsize(extracted_sig_path) debug_logger.debug(f"解压后的签名文件存在: {extracted_sig_path}, 大小: {sig_size} 字节") # 复制签名文件到游戏目录 sig_target = os.path.join(self.game_folder, sig_filename) shutil.copy2(extracted_sig_path, sig_target) debug_logger.debug(f"签名文件成功复制: {sig_target}") else: debug_logger.warning(f"解压后的签名文件不存在: {extracted_sig_path}") else: debug_logger.warning(f"压缩包中没有找到签名文件,但继续安装主补丁文件") else: debug_logger.info(f"{self.game_version} 不需要签名文件,跳过签名文件处理") update_progress(100, f"{self.game_version} 补丁文件解压完成") self.finished.emit(True, "", self.game_version) except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e: try: self.progress.emit(100, f"处理 {self.game_version} 的补丁文件失败") except Exception: pass self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)