Update source->V4.8.6.17218

This commit is contained in:
Yanam1Anna 2025-02-05 21:08:56 +08:00
parent c88b83e11d
commit fb9ca25c3f
2 changed files with 500 additions and 309 deletions

View File

@ -8,15 +8,47 @@
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (
QMetaObject, QObject, QPoint, QRect, QSize, QTime, QCoreApplication,
QUrl, Qt) QDate,
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, QDateTime,
QFontDatabase, QGradient, QIcon, QImage, QLocale,
QKeySequence, QLinearGradient, QPainter, QPalette, QMetaObject,
QPixmap, QRadialGradient, QTransform) QObject,
from PySide6.QtWidgets import (QApplication, QLabel, QPushButton, QSizePolicy, QPoint,
QSplitter, QVBoxLayout, QWidget) QRect,
QSize,
QTime,
QUrl,
Qt,
)
from PySide6.QtGui import (
QBrush,
QColor,
QConicalGradient,
QCursor,
QFont,
QFontDatabase,
QGradient,
QIcon,
QImage,
QKeySequence,
QLinearGradient,
QPainter,
QPalette,
QPixmap,
QRadialGradient,
QTransform,
)
from PySide6.QtWidgets import (
QApplication,
QLabel,
QPushButton,
QSizePolicy,
QSplitter,
QVBoxLayout,
QWidget,
)
from pic_data import img_data from pic_data import img_data
from PySide6.QtCore import QByteArray from PySide6.QtCore import QByteArray
import base64 import base64
@ -26,7 +58,7 @@ class Ui_mainwin(object):
def setupUi(self, mainwin): def setupUi(self, mainwin):
if not mainwin.objectName(): if not mainwin.objectName():
mainwin.setObjectName(u"mainwin") mainwin.setObjectName("mainwin")
mainwin.setWindowModality(Qt.WindowModality.NonModal) mainwin.setWindowModality(Qt.WindowModality.NonModal)
mainwin.resize(1280, 720) mainwin.resize(1280, 720)
mainwin.setMinimumSize(QSize(1280, 720)) mainwin.setMinimumSize(QSize(1280, 720))
@ -35,99 +67,95 @@ class Ui_mainwin(object):
pixmap = QPixmap() pixmap = QPixmap()
icon = QIcon() icon = QIcon()
pixmap.loadFromData(QByteArray(base64.b64decode(img_data['icon']))) pixmap.loadFromData(QByteArray(base64.b64decode(img_data["icon"])))
icon.addPixmap(pixmap) icon.addPixmap(pixmap)
mainwin.setWindowIcon(icon) mainwin.setWindowIcon(icon)
self.mainbg = QLabel(mainwin) self.mainbg = QLabel(mainwin)
self.mainbg.setObjectName(u"mainbg") self.mainbg.setObjectName("mainbg")
self.mainbg.setEnabled(True) self.mainbg.setEnabled(True)
self.mainbg.setGeometry(QRect(0, 0, 1280, 720)) self.mainbg.setGeometry(QRect(0, 0, 1280, 720))
self.mainbg.setMinimumSize(QSize(1280, 720)) self.mainbg.setMinimumSize(QSize(1280, 720))
self.mainbg.setMaximumSize(QSize(1280, 720)) self.mainbg.setMaximumSize(QSize(1280, 720))
self.mainbg.setTextFormat(Qt.TextFormat.AutoText) self.mainbg.setTextFormat(Qt.TextFormat.AutoText)
pixmap.loadFromData(QByteArray(base64.b64decode(img_data['bg']))) pixmap.loadFromData(QByteArray(base64.b64decode(img_data["bg"])))
self.mainbg.setPixmap(pixmap) self.mainbg.setPixmap(pixmap)
self.mainbg.setScaledContents(False) self.mainbg.setScaledContents(False)
self.mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter) self.mainbg.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.cover = QSplitter(mainwin) self.cover = QSplitter(mainwin)
self.cover.setObjectName(u"cover") self.cover.setObjectName("cover")
self.cover.setGeometry(QRect(0, 320, 213, 400)) self.cover.setGeometry(QRect(0, 320, 213, 400))
self.cover.setOrientation(Qt.Orientation.Vertical) self.cover.setOrientation(Qt.Orientation.Vertical)
self.Cover_1 = QLabel(self.cover) self.Cover_1 = QLabel(self.cover)
self.Cover_1.setObjectName(u"Cover_1") self.Cover_1.setObjectName("Cover_1")
self.Cover_1.setMinimumSize(QSize(213, 100)) self.Cover_1.setMinimumSize(QSize(213, 100))
self.Cover_1.setMaximumSize(QSize(213, 100)) self.Cover_1.setMaximumSize(QSize(213, 100))
self.Cover_1.setTextFormat(Qt.TextFormat.AutoText) self.Cover_1.setTextFormat(Qt.TextFormat.AutoText)
pixmap.loadFromData( pixmap.loadFromData(QByteArray(base64.b64decode(img_data["vol_1_cover"])))
QByteArray(base64.b64decode(img_data['vol_1_cover'])))
self.Cover_1.setPixmap(pixmap) self.Cover_1.setPixmap(pixmap)
self.Cover_1.setAlignment(Qt.AlignmentFlag.AlignCenter) self.Cover_1.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.cover.addWidget(self.Cover_1) self.cover.addWidget(self.Cover_1)
self.Cover_2 = QLabel(self.cover) self.Cover_2 = QLabel(self.cover)
self.Cover_2.setObjectName(u"Cover_2") self.Cover_2.setObjectName("Cover_2")
self.Cover_2.setMinimumSize(QSize(213, 100)) self.Cover_2.setMinimumSize(QSize(213, 100))
self.Cover_2.setMaximumSize(QSize(213, 100)) self.Cover_2.setMaximumSize(QSize(213, 100))
self.Cover_2.setTextFormat(Qt.TextFormat.AutoText) self.Cover_2.setTextFormat(Qt.TextFormat.AutoText)
pixmap.loadFromData( pixmap.loadFromData(QByteArray(base64.b64decode(img_data["vol_2_cover"])))
QByteArray(base64.b64decode(img_data['vol_2_cover'])))
self.Cover_2.setPixmap(pixmap) self.Cover_2.setPixmap(pixmap)
self.Cover_2.setAlignment(Qt.AlignmentFlag.AlignCenter) self.Cover_2.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.cover.addWidget(self.Cover_2) self.cover.addWidget(self.Cover_2)
self.Cover_3 = QLabel(self.cover) self.Cover_3 = QLabel(self.cover)
self.Cover_3.setObjectName(u"Cover_3") self.Cover_3.setObjectName("Cover_3")
self.Cover_3.setMinimumSize(QSize(213, 100)) self.Cover_3.setMinimumSize(QSize(213, 100))
self.Cover_3.setMaximumSize(QSize(213, 100)) self.Cover_3.setMaximumSize(QSize(213, 100))
self.Cover_3.setTextFormat(Qt.TextFormat.AutoText) self.Cover_3.setTextFormat(Qt.TextFormat.AutoText)
pixmap.loadFromData( pixmap.loadFromData(QByteArray(base64.b64decode(img_data["vol_3_cover"])))
QByteArray(base64.b64decode(img_data['vol_3_cover'])))
self.Cover_3.setPixmap(pixmap) self.Cover_3.setPixmap(pixmap)
self.Cover_3.setAlignment(Qt.AlignmentFlag.AlignCenter) self.Cover_3.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.cover.addWidget(self.Cover_3) self.cover.addWidget(self.Cover_3)
self.Cover_4 = QLabel(self.cover) self.Cover_4 = QLabel(self.cover)
self.Cover_4.setObjectName(u"Cover_4") self.Cover_4.setObjectName("Cover_4")
self.Cover_4.setMinimumSize(QSize(213, 100)) self.Cover_4.setMinimumSize(QSize(213, 100))
self.Cover_4.setMaximumSize(QSize(213, 100)) self.Cover_4.setMaximumSize(QSize(213, 100))
self.Cover_4.setTextFormat(Qt.TextFormat.AutoText) self.Cover_4.setTextFormat(Qt.TextFormat.AutoText)
pixmap.loadFromData( pixmap.loadFromData(QByteArray(base64.b64decode(img_data["vol_4_cover"])))
QByteArray(base64.b64decode(img_data['vol_4_cover'])))
self.Cover_4.setPixmap(pixmap) self.Cover_4.setPixmap(pixmap)
self.Cover_4.setAlignment(Qt.AlignmentFlag.AlignCenter) self.Cover_4.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.cover.addWidget(self.Cover_4) self.cover.addWidget(self.Cover_4)
self.menubg = QLabel(mainwin) self.menubg = QLabel(mainwin)
self.menubg.setObjectName(u"menubg") self.menubg.setObjectName("menubg")
self.menubg.setGeometry(QRect(800, 0, 480, 720)) self.menubg.setGeometry(QRect(800, 0, 480, 720))
self.menubg.setMinimumSize(QSize(480, 720)) self.menubg.setMinimumSize(QSize(480, 720))
self.menubg.setMaximumSize(QSize(480, 720)) self.menubg.setMaximumSize(QSize(480, 720))
pixmap.loadFromData(QByteArray(base64.b64decode(img_data['menubg']))) pixmap.loadFromData(QByteArray(base64.b64decode(img_data["menubg"])))
self.menubg.setPixmap(pixmap) self.menubg.setPixmap(pixmap)
self.menubg.setAlignment(Qt.AlignmentFlag.AlignCenter) self.menubg.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layoutWidget = QWidget(mainwin) self.layoutWidget = QWidget(mainwin)
self.layoutWidget.setObjectName(u"layoutWidget") self.layoutWidget.setObjectName("layoutWidget")
self.layoutWidget.setGeometry(QRect(940, 340, 212, 204)) self.layoutWidget.setGeometry(QRect(940, 340, 212, 204))
self.button = QVBoxLayout(self.layoutWidget) self.button = QVBoxLayout(self.layoutWidget)
self.button.setObjectName(u"button") self.button.setObjectName("button")
self.button.setContentsMargins(0, 0, 0, 0) self.button.setContentsMargins(0, 0, 0, 0)
self.startbtn = QPushButton(self.layoutWidget) self.startbtn = QPushButton(self.layoutWidget)
self.startbtn.setObjectName(u"startbtn") self.startbtn.setObjectName("startbtn")
self.startbtn.setMinimumSize(QSize(210, 98)) self.startbtn.setMinimumSize(QSize(210, 98))
self.startbtn.setMaximumSize(QSize(210, 98)) self.startbtn.setMaximumSize(QSize(210, 98))
icon1 = QIcon() icon1 = QIcon()
pixmap.loadFromData(QByteArray(base64.b64decode(img_data['btn01bg']))) pixmap.loadFromData(QByteArray(base64.b64decode(img_data["btn01bg"])))
icon1.addPixmap(pixmap) icon1.addPixmap(pixmap)
self.startbtn.setIcon(icon1) self.startbtn.setIcon(icon1)
@ -137,12 +165,12 @@ class Ui_mainwin(object):
self.button.addWidget(self.startbtn) self.button.addWidget(self.startbtn)
self.exitbtn = QPushButton(self.layoutWidget) self.exitbtn = QPushButton(self.layoutWidget)
self.exitbtn.setObjectName(u"exitbtn") self.exitbtn.setObjectName("exitbtn")
self.exitbtn.setMinimumSize(QSize(210, 98)) self.exitbtn.setMinimumSize(QSize(210, 98))
self.exitbtn.setMaximumSize(QSize(210, 98)) self.exitbtn.setMaximumSize(QSize(210, 98))
icon2 = QIcon() icon2 = QIcon()
pixmap.loadFromData(QByteArray(base64.b64decode(img_data['btn02bg']))) pixmap.loadFromData(QByteArray(base64.b64decode(img_data["btn02bg"])))
icon2.addPixmap(pixmap) icon2.addPixmap(pixmap)
self.exitbtn.setIcon(icon2) self.exitbtn.setIcon(icon2)
@ -152,22 +180,21 @@ class Ui_mainwin(object):
self.button.addWidget(self.exitbtn) self.button.addWidget(self.exitbtn)
self.sup_info = QLabel(mainwin) self.sup_info = QLabel(mainwin)
self.sup_info.setObjectName(u"sup_info") self.sup_info.setObjectName("sup_info")
self.sup_info.setGeometry(QRect(0, 230, 213, 100)) self.sup_info.setGeometry(QRect(0, 230, 213, 100))
self.sup_info.setMinimumSize(QSize(213, 100)) self.sup_info.setMinimumSize(QSize(213, 100))
self.sup_info.setMaximumSize(QSize(213, 100)) self.sup_info.setMaximumSize(QSize(213, 100))
pixmap.loadFromData( pixmap.loadFromData(QByteArray(base64.b64decode(img_data["support_info"])))
QByteArray(base64.b64decode(img_data['support_info'])))
self.sup_info.setPixmap(pixmap) self.sup_info.setPixmap(pixmap)
self.future_info = QLabel(mainwin) self.future_info = QLabel(mainwin)
self.future_info.setObjectName(u"future_info") self.future_info.setObjectName("future_info")
self.future_info.setGeometry(QRect(860, 700, 370, 16)) self.future_info.setGeometry(QRect(860, 700, 370, 16))
self.future_info.setMinimumSize(QSize(370, 16)) self.future_info.setMinimumSize(QSize(370, 16))
self.future_info.setMaximumSize(QSize(370, 16)) self.future_info.setMaximumSize(QSize(370, 16))
font = QFont() font = QFont()
font.setFamilies([u"Consolas"]) font.setFamilies(["Consolas"])
font.setBold(True) font.setBold(True)
font.setItalic(True) font.setItalic(True)
self.future_info.setFont(font) self.future_info.setFont(font)
@ -188,7 +215,9 @@ class Ui_mainwin(object):
def retranslateUi(self, mainwin): def retranslateUi(self, mainwin):
mainwin.setWindowTitle( mainwin.setWindowTitle(
QCoreApplication.translate( QCoreApplication.translate(
"mainwin", u"FRAISEMOE Addons Installer V4.5.0.30988", None)) "mainwin", "FRAISEMOE Addons Installer V4.8.6.17218", None
)
)
self.mainbg.setText("") self.mainbg.setText("")
self.Cover_1.setText("") self.Cover_1.setText("")
self.Cover_2.setText("") self.Cover_2.setText("")
@ -200,7 +229,8 @@ class Ui_mainwin(object):
self.sup_info.setText("") self.sup_info.setText("")
self.future_info.setText( self.future_info.setText(
QCoreApplication.translate( QCoreApplication.translate(
"mainwin", "mainwin", "Nekopara After La Vraie Famille COMMING SOON IN 2025", None
"Nekopara After La Vraie Famille COMMING SOON IN 2025", None)) )
)
# retranslateUi # retranslateUi

View File

@ -5,309 +5,467 @@ import shutil
import hashlib import hashlib
import sys import sys
import base64 import base64
import psutil
import ctypes
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import Qt, QByteArray from PySide6.QtCore import Qt, QByteArray, Signal
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox, QProgressBar, QVBoxLayout, QLabel from PySide6.QtWidgets import (
QApplication,
QWidget,
QMessageBox,
QProgressBar,
QVBoxLayout,
QLabel,
)
from PySide6.QtGui import QIcon, QPixmap from PySide6.QtGui import QIcon, QPixmap
from collections import deque
from pic_data import img_data from pic_data import img_data
from GUI import Ui_mainwin from GUI import Ui_mainwin
Msgboxtitle = "@FRAISEMOE Addons Installer V4.5.0.30988" APP_VERSION = "@FRAISEMOE Addons Installer V4.8.6.17218"
TEMP = os.getenv("TEMP")
Packfolder = "./addons" CACHE = os.path.join(TEMP, "FRAISEMOE")
PLUGIN = os.path.join(CACHE, "PLUGIN")
CONFIG_URL = "https://archive.ovofish.com/api/widget/nekopara/download_url.json"
UA = "Mozilla/5.0 (Linux debian12 Python-Accept) Gecko/20100101 Firefox/114.0"
SRC_HASHES = {
"NEKOPARA Vol.1": "04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e",
"NEKOPARA Vol.2": "b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42",
"NEKOPARA Vol.3": "2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5",
"NEKOPARA Vol.4": "4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a",
}
def calc_hash(dstfilepath): def admin_status():
msg_box = QMessageBox() try:
msg_box.setWindowTitle(f'通知 {Msgboxtitle}') return ctypes.windll.shell32.IsUserAnAdmin()
pixmap = QPixmap() except:
return False
icon = QIcon()
pixmap.loadFromData(QByteArray(base64.b64decode(img_data['icon'])))
icon.addPixmap(pixmap) def run_as_admin():
script = os.path.abspath(sys.argv[0])
msg_box.setWindowIcon(icon) params = " ".join([script] + sys.argv[1:])
msg_box.setText('\n正在检验文件完整性...\n') ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, None, 1)
msg_box.setStandardButtons(QMessageBox.StandardButton.NoButton)
msg_box.show()
class DownloadThread(QtCore.QThread):
# Force the message box to be painted immediately progress = Signal(int)
QApplication.processEvents() finished = Signal(bool, str)
sha256_hash = hashlib.sha256() def __init__(self, url, _7z_path, parent=None):
with open(dstfilepath, "rb") as f: super().__init__(parent)
for byte_block in iter(lambda: f.read(4096), b""): self.url = url
sha256_hash.update(byte_block) self._7z_path = _7z_path
# Close the message box after the operation is complete
msg_box.close() def run(self):
try:
return sha256_hash.hexdigest() headers = {"User-Agent": UA}
r = requests.get(self.url, headers=headers, stream=True, timeout=10)
r.raise_for_status()
block_size = 64 * 1024
total_size = int(r.headers.get("content-length", 0))
with open(self._7z_path, "wb") as f:
for chunk in r.iter_content(chunk_size=block_size):
f.write(chunk)
self.progress.emit(f.tell() * 100 // total_size)
self.finished.emit(True, "")
except requests.exceptions.RequestException as e:
self.finished.emit(False, f"\n网络请求错误\n\n【错误信息】: {e}\n")
except Exception as e:
self.finished.emit(False, f"\n未知错误\n\n【错误信息】: {e}\n")
def game_process_status(process_name):
for proc in psutil.process_iter(["pid", "name"]):
try:
if process_name.lower() in proc.info["name"].lower():
return proc.info["pid"]
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
return None
def kill_process(pid):
try:
process = psutil.Process(pid)
process.terminate()
process.wait(timeout=5)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
# UI
class MyWindow(QWidget, Ui_mainwin): class MyWindow(QWidget, Ui_mainwin):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setupUi(self) self.setupUi(self)
self.tgtfolder = '' self.selected_folder = ""
self.download_stat = { self.installed_status = {
'NEKOPARA Vol.1': False, "NEKOPARA Vol.1": False,
'NEKOPARA Vol.2': False, "NEKOPARA Vol.2": False,
'NEKOPARA Vol.3': False, "NEKOPARA Vol.3": False,
'NEKOPARA Vol.4': False "NEKOPARA Vol.4": False,
} }
# Create a folder to store the downloaded files self.download_queue = deque()
if not os.path.exists(Packfolder): self.current_download_thread = None
os.makedirs(Packfolder)
if not os.path.exists(Packfolder): game_process_info = {
QMessageBox.critical(self, f'错误 {Msgboxtitle}', "nekopara_vol1.exe": "NEKOPARA Vol.1",
'\n无法创建安装包存放位置,请检查文件读写权限后再试\n') "nekopara_vol2.exe": "NEKOPARA Vol.2",
"NEKOPARAvol3.exe": "NEKOPARA Vol.3",
"nekopara_vol4.exe": "NEKOPARA Vol.4",
}
for process_name, game_version in game_process_info.items():
pid = game_process_status(process_name)
if pid:
msg_box = QMessageBox()
msg_box.setWindowTitle(f"进程检测 {APP_VERSION}")
pixmap = QPixmap()
pixmap.loadFromData(QByteArray(base64.b64decode(img_data["icon"])))
icon = QIcon(pixmap)
msg_box.setWindowIcon(icon)
msg_box.setText(f"\n检测到 {game_version} 正在运行,是否关闭?\n")
yes_button = msg_box.addButton(
"确定", QMessageBox.ButtonRole.AcceptRole
)
no_button = msg_box.addButton("取消", QMessageBox.ButtonRole.RejectRole)
msg_box.setDefaultButton(no_button)
msg_box.exec()
if msg_box.clickedButton() == yes_button:
kill_process(pid)
else:
QMessageBox.warning(
self,
f"警告 {APP_VERSION}",
f"\n请关闭 {game_version} 后再运行本程序。\n",
)
self.close()
sys.exit()
if not os.path.exists(PLUGIN):
os.makedirs(PLUGIN)
if not os.path.exists(PLUGIN):
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
"\n无法创建缓存位置\n\n使用管理员身份运行或检查文件读写权限\n",
)
self.close() self.close()
sys.exit() sys.exit()
# Buttons self.startbtn.clicked.connect(self.file_dialog)
self.startbtn.clicked.connect(self.ChooseFileDialog) self.exitbtn.clicked.connect(self.shutdown_app)
self.exitbtn.clicked.connect(self.closeEvent)
def closeEvent(self, event): def get_install_paths(self):
self.confirm_quit = True return {
if self.confirm_quit: "NEKOPARA Vol.1": os.path.join(
reply = QMessageBox.question( self.selected_folder, "NEKOPARA Vol. 1", "adultsonly.xp3"
self, '退出程序', '\n是否确定退出?\n', ),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, "NEKOPARA Vol.2": os.path.join(
QMessageBox.StandardButton.No) self.selected_folder, "NEKOPARA Vol. 2", "adultsonly.xp3"
if reply == QMessageBox.StandardButton.Yes: ),
# event.accept() "NEKOPARA Vol.3": os.path.join(
shutil.rmtree(Packfolder) self.selected_folder, "NEKOPARA Vol. 3", "update00.int"
sys.exit() ),
else: "NEKOPARA Vol.4": os.path.join(
pass self.selected_folder, "NEKOPARA Vol. 4", "vol4adult.xp3"
# event.ignore() ),
else:
# event.accept()
shutil.rmtree(Packfolder)
sys.exit()
def download_config(self) -> dict:
config_url = 'https://cn-sy1.rains3.com/cloudfile/cloudfile/uploads/2025/01/25/xyXGNdvF_download_config.json'
try:
response = requests.get(config_url, timeout=10)
response = response.json()
url_v1 = response['vol.1.data']['url']
url_v2 = response['vol.2.data']['url']
url_v3 = response['vol.3.data']['url']
url_v4 = response['vol.4.data']['url']
except Exception:
QMessageBox.critical(self, f"错误 {Msgboxtitle}",f"\n获取下载配置失败\n检查网络状态,或服务器故障\n")
return {}
return {'vol1':url_v1,'vol2':url_v2,'vol3':url_v3,'vol4':url_v4}
def CheckFileStat(self, basedic):
pass
self.download_stat = {
'NEKOPARA Vol.1': False,
'NEKOPARA Vol.2': False,
'NEKOPARA Vol.3': False,
'NEKOPARA Vol.4': False
} }
if os.path.exists(f'{basedic}/NEKOPARA Vol. 1/adultsonly.xp3'):
if calc_hash(
f'{basedic}/NEKOPARA Vol. 1/adultsonly.xp3'
) == '04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e':
self.download_stat['NEKOPARA Vol.1'] = True
else:
QMessageBox.warning(self, f"警告 {Msgboxtitle}",
'\n补丁文件文件校验失败\n即将重新安装当前版本补丁\n')
os.remove(f'{basedic}/NEKOPARA Vol. 1/adultsonly.xp3')
if os.path.exists(f'{basedic}/NEKOPARA Vol. 2/adultsonly.xp3'): def file_dialog(self):
if calc_hash( self.selected_folder = QtWidgets.QFileDialog.getExistingDirectory(
f'{basedic}/NEKOPARA Vol. 2/adultsonly.xp3' self, f"选择游戏所在【上级目录】 {APP_VERSION}"
) == 'b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42': )
self.download_stat['NEKOPARA Vol.2'] = True if not self.selected_folder:
else: QMessageBox.warning(
QMessageBox.warning(self, f"警告 {Msgboxtitle}", self, f"通知 {APP_VERSION}", "\n未选择任何目录,请重新选择\n"
'\n补丁文件文件校验失败\n即将重新安装当前版本补丁\n') )
os.remove(f'{basedic}/NEKOPARA Vol. 2/adultsonly.xp3')
if os.path.exists(f'{basedic}/NEKOPARA Vol. 3/update00.int'):
if calc_hash(
f'{basedic}/NEKOPARA Vol. 3/update00.int'
) == '2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5':
self.download_stat['NEKOPARA Vol.3'] = True
else:
QMessageBox.warning(self, f"警告 {Msgboxtitle}",
'\n补丁文件文件校验失败\n即将重新安装当前版本补丁\n')
os.remove(f'{basedic}/NEKOPARA Vol. 3/update00.int')
if os.path.exists(f'{basedic}/NEKOPARA Vol. 4/vol4adult.xp3'):
if calc_hash(
f'{basedic}/NEKOPARA Vol. 4/vol4adult.xp3'
) == '4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a':
self.download_stat['NEKOPARA Vol.4'] = True
else:
QMessageBox.warning(self, f"警告 {Msgboxtitle}",
'\n补丁文件文件校验失败\n即将重新安装当前版本补丁\n')
os.remove(f'{basedic}/NEKOPARA Vol. 4/vol4adult.xp3')
def ChooseFileDialog(self):
self.tgtfolder = QtWidgets.QFileDialog.getExistingDirectory(
self, f"选择游戏所在上级目录 {Msgboxtitle}")
if not self.tgtfolder:
QMessageBox.warning(self, f"通知 {Msgboxtitle}",
"\n尚未选择任何目录,请重新选择目录\n")
return return
self.PackParameter() self.download_action()
def DownloadParameter(self, url, dstfilepath, GV, file_7z_route, def hash_calculate(self, file_path):
srcfileroute): sha256_hash = hashlib.sha256()
""" with open(file_path, "rb") as f:
install with params for byte_block in iter(lambda: f.read(4096), b""):
:param url: install url sha256_hash.update(byte_block)
:param dstfilepath: dest folder route return sha256_hash.hexdigest()
:param GV: GameVer
:param file_7z_route: temp addonfile route,eg: './addons/vol.1.7z' def hash_pop_window(self):
:param srcfileroute: src route,eg: './addons/vol.1/adultsonly.xp3' msg_box = QMessageBox()
:return: None msg_box.setWindowTitle(f"通知 {APP_VERSION}")
""" pixmap = QPixmap()
# TODO: check whether 7z file in assets and unzip, install pixmap.loadFromData(QByteArray(base64.b64decode(img_data["icon"])))
if not os.path.exists(dstfilepath): icon = QIcon(pixmap)
self.download_stat[GV] = False msg_box.setWindowIcon(icon)
elif self.download_stat[GV]: msg_box.setText("\n正在检验文件状态...\n")
QMessageBox.information(self, f"通知 {Msgboxtitle}", msg_box.setStandardButtons(QMessageBox.StandardButton.NoButton)
f"\n{GV} 补丁包已安装\n") msg_box.show()
else: QApplication.processEvents()
progress_window = ProgressWindow(self) return msg_box
progress_window.show()
try: def pre_hash_compare(self, install_path, game_version, SRC_HASHES):
r = requests.get(url, stream=True, timeout=10) if not os.path.exists(install_path):
r.raise_for_status() self.installed_status[game_version] = False
block_size = 64 * 1024 return
progress = 0
progress_window.setmaxvalue( msg_box = self.hash_pop_window()
int(r.headers.get('content-length', 0))) file_hash = self.hash_calculate(install_path)
with open(file_7z_route, 'wb') as f: msg_box.close()
for chunk in r.iter_content(chunk_size=block_size):
f.write(chunk) if file_hash != SRC_HASHES[game_version]:
progress += len(chunk) msg_box = QMessageBox(self)
progress_window.setprogressbarval(progress) msg_box.setWindowTitle(f"文件校验 {APP_VERSION}")
QApplication.processEvents() msg_box.setText(
except Exception as e: f"\n【 当前版本已安装旧版本补丁 -> {game_version}\n\n是否重新安装?\n----->取消安装前应确认补丁是否可用<-----\n"
QMessageBox.critical(self, f"错误 {Msgboxtitle}", )
f"\n网络超时,重启程序再试\n错误信息:{e}\n") yes_button = msg_box.addButton("确定", QMessageBox.ButtonRole.AcceptRole)
self.download_stat[GV] = False no_button = msg_box.addButton("取消", QMessageBox.ButtonRole.RejectRole)
progress_window.close() msg_box.setDefaultButton(no_button)
msg_box.exec()
if msg_box.clickedButton() == yes_button:
self.installed_status[game_version] = False
return return
# Extract the compressed file to the specified directory
archive = py7zr.SevenZipFile(file_7z_route, mode='r')
# Decompression directory of compressed files
archive.extractall(path=r'./addons')
archive.close()
shutil.copy(srcfileroute, dstfilepath)
self.download_stat[GV] = True
QMessageBox.information(self, f"通知 {Msgboxtitle}",
f"\n{GV} 补丁包安装完成\n")
def PackParameter(self):
# Get the installing stat
self.CheckFileStat(self.tgtfolder)
config = self.download_config()
if not config:
QMessageBox.critical(self, f'错误 {Msgboxtitle}',
'\n网络连接错误,重启应用再试\n')
return
# Download
self.DownloadParameter(
config['vol1'],
f'{self.tgtfolder}/NEKOPARA Vol. 1', 'NEKOPARA Vol.1',
'./addons/vol.1.7z', './addons/vol.1/adultsonly.xp3')
self.DownloadParameter(
config['vol2'],
f'{self.tgtfolder}/NEKOPARA Vol. 2', 'NEKOPARA Vol.2',
'./addons/vol.2.7z', './addons/vol.2/adultsonly.xp3')
self.DownloadParameter(
config['vol3'],
f'{self.tgtfolder}/NEKOPARA Vol. 3', 'NEKOPARA Vol.3',
'./addons/vol.3.7z', './addons/vol.3/update00.int')
self.DownloadParameter(
config['vol4'],
f'{self.tgtfolder}/NEKOPARA Vol. 4', 'NEKOPARA Vol.4',
'./addons/vol.4.7z', './addons/vol.4/vol4adult.xp3')
if not self.CompareHash():
QMessageBox.critical(self, f"错误 {Msgboxtitle}",
"\n文件损坏,无法通过文件校验\n请重新启动程序\n")
return
# Count the installation results
installver = ''
installnum = 0
failver = ''
failnum = 0
for i in list(self.download_stat.keys()):
if self.download_stat[i]:
installver += f"{i}\n"
installnum += 1
else: else:
failver += f"{i}\n" self.installed_status[game_version] = True
failnum += 1 return
else:
self.installed_status[game_version] = True
return
QMessageBox.information( def late_hash_compare(self, SRC_HASHES):
self, f"完成 {Msgboxtitle}", f"\n安装结果:\n" install_paths = self.get_install_paths()
f"安装成功数:{installnum} 安装失败数:{failnum}\n\n"
f"安装成功的版本:\n"
f"{installver}\n"
f"尚未持有的版本:\n"
f"{failver}\n")
def CompareHash(self):
# Src files hash
src_hash_v1 = '04b48b231a7f34431431e5027fcc7b27affaa951b8169c541709156acf754f3e'
src_hash_v2 = 'b9c00a2b113a1e768bf78400e4f9075ceb7b35349cdeca09be62eb014f0d4b42'
src_hash_v3 = '2ce7b223c84592e1ebc3b72079dee1e5e8d064ade15723328a64dee58833b9d5'
src_hash_v4 = '4a4a9ae5a75a18aacbe3ab0774d7f93f99c046afe3a777ee0363e8932b90f36a'
# Check hash value
passed = True passed = True
passlist = [] for game, hash_value in SRC_HASHES.items():
for i in list(self.download_stat.keys()): if self.installed_status.get(game):
if self.download_stat[i]: msg_box = self.hash_pop_window()
passlist.append(i) file_hash = self.hash_calculate(install_paths[game])
for i in passlist: msg_box.close()
if i == 'NEKOPARA Vol.1': if file_hash != hash_value:
if calc_hash(f'{self.tgtfolder}/NEKOPARA Vol. 1/adultsonly.xp3'
) != src_hash_v1:
passed = False
elif i == 'NEKOPARA Vol.2':
if calc_hash(f'{self.tgtfolder}/NEKOPARA Vol. 2/adultsonly.xp3'
) != src_hash_v2:
passed = False
elif i == 'NEKOPARA Vol.3':
if calc_hash(f'{self.tgtfolder}/NEKOPARA Vol. 3/update00.int'
) != src_hash_v3:
passed = False
elif i == 'NEKOPARA Vol.4':
if calc_hash(f'{self.tgtfolder}/NEKOPARA Vol. 4/vol4adult.xp3'
) != src_hash_v4:
passed = False passed = False
break
return passed return passed
def download_config(self) -> dict:
try:
headers = {"User-Agent": UA}
response = requests.get(CONFIG_URL, headers=headers, timeout=10)
response.raise_for_status()
response = response.json()
return {f"vol{i+1}": response[f"vol.{i+1}.data"]["url"] for i in range(4)}
except requests.exceptions.RequestException as e:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n下载配置获取失败\n\n网络状态异常或服务器故障\n\n【错误信息】:{e}\n",
)
except Exception as e:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n下载配置获取失败\n\n未知错误\n\n【错误信息】:{e}\n",
)
return {}
def download_setting(self, url, game_folder, game_version, _7z_path, plugin_path):
game_exe = {
"NEKOPARA Vol.1": os.path.join(
self.selected_folder, "NEKOPARA Vol. 1", "nekopara_vol1.exe"
),
"NEKOPARA Vol.2": os.path.join(
self.selected_folder, "NEKOPARA Vol. 2", "nekopara_vol2.exe"
),
"NEKOPARA Vol.3": os.path.join(
self.selected_folder, "NEKOPARA Vol. 3", "NEKOPARAvol3.exe"
),
"NEKOPARA Vol.4": os.path.join(
self.selected_folder, "NEKOPARA Vol. 4", "nekopara_vol4.exe"
),
}
if (
game_version not in game_exe
or not os.path.exists(game_exe[game_version])
or self.installed_status[game_version]
):
self.next_download_task()
return
progress_window = ProgressWindow(self)
progress_window.show()
self.current_download_thread = DownloadThread(url, _7z_path, self)
self.current_download_thread.progress.connect(progress_window.setprogressbarval)
self.current_download_thread.finished.connect(
lambda success, error: self.install_setting(
success,
error,
progress_window,
game_folder,
game_version,
_7z_path,
plugin_path,
)
)
self.current_download_thread.start()
def install_setting(
self,
success,
error,
progress_window,
game_folder,
game_version,
_7z_path,
plugin_path,
):
progress_window.close()
if success:
try:
msg_box = self.hash_pop_window()
QApplication.processEvents()
with py7zr.SevenZipFile(_7z_path, mode="r") as archive:
archive.extractall(path=PLUGIN)
shutil.copy(plugin_path, game_folder)
self.installed_status[game_version] = True
QMessageBox.information(
self, f"通知 {APP_VERSION}", f"\n{game_version} 补丁已安装\n"
)
except py7zr.Bad7zFile as e:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n文件损坏\n\n【错误信息】:{e}\n",
)
except FileNotFoundError as e:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n文件不存在\n\n【错误信息】:{e}\n",
)
except Exception as e:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n",
)
finally:
msg_box.close()
else:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n文件获取失败\n网络状态异常或服务器故障\n\n【错误信息】:{error}\n",
)
self.next_download_task()
def download_action(self):
install_paths = self.get_install_paths()
for game_version, install_path in install_paths.items():
self.pre_hash_compare(install_path, game_version, SRC_HASHES)
if self.late_hash_compare(SRC_HASHES):
config = self.download_config()
if not config:
QMessageBox.critical(
self, f"错误 {APP_VERSION}", "\n网络状态异常或服务器故障,请重试\n"
)
return
for i in range(1, 5):
game_version = f"NEKOPARA Vol.{i}"
if self.installed_status[game_version] == False:
url = config[f"vol{i}"]
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,
f"vol.{i}",
[
"adultsonly.xp3",
"adultsonly.xp3",
"update00.int",
"vol4adult.xp3",
][i - 1],
)
self.download_queue.append(
(url, game_folder, game_version, _7z_path, plugin_path)
)
self.next_download_task()
def next_download_task(self):
if not self.download_queue:
self.show_result()
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 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]]
)
QMessageBox.information(
self,
f"完成 {APP_VERSION}",
f"\n安装结果:\n"
f"安装成功数:{len(installed_version.splitlines())} 安装失败数:{len(failed_ver.splitlines())}\n\n"
f"安装成功的版本:\n"
f"{installed_version}\n"
f"尚未持有的版本:\n"
f"{failed_ver}\n",
)
def shutdown_app(self):
msg_box = QMessageBox(self)
msg_box.setWindowTitle("退出程序")
msg_box.setText("\n是否确定退出?\n")
yes_button = msg_box.addButton("确定", QMessageBox.ButtonRole.AcceptRole)
no_button = msg_box.addButton("取消", QMessageBox.ButtonRole.RejectRole)
msg_box.setDefaultButton(no_button)
msg_box.exec()
if msg_box.clickedButton() == yes_button:
if (
self.current_download_thread
and self.current_download_thread.isRunning()
):
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
"\n当前有下载任务正在进行,完成后再试。\n",
)
return
if os.path.exists(PLUGIN):
try:
shutil.rmtree(PLUGIN)
except Exception as e:
QMessageBox.critical(
self,
f"错误 {APP_VERSION}",
f"\n清理缓存失败\n\n【错误信息】:{e}\n",
)
sys.exit()
# Progress
class ProgressWindow(QtWidgets.QDialog): class ProgressWindow(QtWidgets.QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super(ProgressWindow, self).__init__(parent) super(ProgressWindow, self).__init__(parent)
self.setWindowTitle(f"下载进度 {Msgboxtitle}") self.setWindowTitle(f"下载进度 {APP_VERSION}")
self.resize(400, 100) self.resize(400, 100)
self.progress_bar_max = 100 self.progress_bar_max = 100
# Disable close button and system menu
self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowSystemMenuHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowSystemMenuHint)
@ -329,7 +487,10 @@ class ProgressWindow(QtWidgets.QDialog):
QtCore.QTimer.singleShot(2000, self.close) QtCore.QTimer.singleShot(2000, self.close)
if __name__ == '__main__': if __name__ == "__main__":
if not admin_status():
run_as_admin()
sys.exit()
app = QApplication([]) app = QApplication([])
window = MyWindow() window = MyWindow()
window.show() window.show()