self-updater ,settings, improvments + fixes

This commit is contained in:
faroukbmiled 2023-08-03 09:13:02 +01:00
parent 30c66410f2
commit e83d6417c2

View File

@ -1,5 +1,6 @@
import os
import sys
import re
import subprocess
import configparser
import json
@ -10,17 +11,61 @@ import requests
import time
import threading
from bs4 import BeautifulSoup
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QMessageBox, QHBoxLayout, QProgressBar, QSizePolicy, QFileDialog
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QDialog, \
QVBoxLayout, QMessageBox, QHBoxLayout, QProgressBar, QSizePolicy, QFileDialog, QCheckBox, QSpacerItem
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtCore import QCoreApplication, QSettings
from PyQt5.QtGui import QIcon, QPixmap, QCloseEvent
import webbrowser
import qdarktheme
VERSION = "v0.1.3"
GITHUB_REPO = "faroukbmiled/BOIIIWD"
LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip"
UPDATER_FOLDER = "update"
CONFIG_FILE_PATH = "config.ini"
global stopped, steampid
global stopped, steampid, console, up_cancelled
steampid = None
stopped = False
console = False
up_cancelled = False
def get_latest_release_version():
try:
release_api_url = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
response = requests.get(release_api_url)
response.raise_for_status()
data = response.json()
return data["tag_name"]
except requests.exceptions.RequestException as e:
show_message("Warning", f"Error while checking for updates: \n{e}")
return None
def create_update_script(current_exe, new_exe, updater_folder, program_name):
script_content = f"""
@echo off
echo Terminating BOIIIWD.exe...
taskkill /im "{program_name}" /t /f
echo Replacing BOIIIWD.exe...
cd "{updater_folder}"
taskkill /im "{program_name}" /t /f
move /y "{new_exe}" "../"{program_name}""
echo Starting BOIIIWD.exe...
cd ..
start "" "{current_exe}"
echo Exiting!
exit
"""
script_path = os.path.join(updater_folder, "boiiiwd_updater.bat")
with open(script_path, "w") as script_file:
script_file.write(script_content)
return script_path
def cwd():
if getattr(sys, 'frozen', False):
@ -28,6 +73,18 @@ def cwd():
else:
return os.path.dirname(os.path.abspath(__file__))
def extract_workshop_id(link):
try:
pattern = r'(?<=id=)(\d+)'
match = re.search(pattern, link)
if match:
return match.group(0)
else:
return None
except:
return None
def check_steamcmd():
steamcmd_path = get_steamcmd_path()
steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe")
@ -37,6 +94,17 @@ def check_steamcmd():
return True
def initialize_steam():
try:
steamcmd_path = get_steamcmd_path()
steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe")
process = subprocess.Popen([steamcmd_exe_path, "+quit"], creationflags=subprocess.CREATE_NEW_CONSOLE)
process.wait()
show_message("Done!", "BOIIIWD is ready for action.", icon=QMessageBox.Information)
except:
show_message("Done!", "An error occurred please check your paths and try again.")
def valid_id(workshop_id):
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}"
response = requests.get(url)
@ -68,22 +136,27 @@ def create_default_config():
config = configparser.ConfigParser()
config["Settings"] = {
"SteamCMDPath": cwd(),
"DestinationFolder": ""
"DestinationFolder": "",
"checkforupdtes": "on",
"console": "off"
}
with open(CONFIG_FILE_PATH, "w") as config_file:
config.write(config_file)
def run_steamcmd_command(command):
steamcmd_path = get_steamcmd_path()
show_console = subprocess.CREATE_NO_WINDOW
if console:
show_console = subprocess.CREATE_NEW_CONSOLE
process = subprocess.Popen(
[steamcmd_path + "\steamcmd.exe"] + command.split(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdout=None if console else subprocess.PIPE,
stderr=None if console else subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
creationflags=subprocess.CREATE_NO_WINDOW
creationflags=show_console
)
global steampid
@ -104,6 +177,30 @@ def get_steamcmd_path():
config.read(CONFIG_FILE_PATH)
return config.get("Settings", "SteamCMDPath", fallback=cwd())
def config_check_for_updates(state=None):
if state:
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
config["Settings"]["checkforupdtes"] = state
with open(CONFIG_FILE_PATH, "w") as config_file:
config.write(config_file)
return
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
return config.get("Settings", "checkforupdtes", fallback="on")
def config_console_state(state=None):
if state:
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
config["Settings"]["console"] = state
with open(CONFIG_FILE_PATH, "w") as config_file:
config.write(config_file)
return
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
return config.get("Settings", "console", fallback="off")
def extract_json_data(json_path):
with open(json_path, "r") as json_file:
data = json.load(json_file)
@ -207,16 +304,148 @@ def download_workshop_map(workshop_id, destination_folder, progress_bar, speed_l
try:
shutil.copytree(map_folder, folder_name_path, dirs_exist_ok=True)
except Exception as E:
print(f"Error copying files: {E}")
show_message("Error", f"Error copying files: {E}")
show_message("Download Complete", f"{mod_type} files are downloaded at \n{folder_name_path}\nYou can run the game now!", icon=QMessageBox.Information)
def show_message(title, message, icon=QMessageBox.Warning):
def show_message(title, message, icon=QMessageBox.Warning, exit_on_close=False):
msg = QMessageBox()
msg.setWindowTitle(title)
msg.setWindowIcon(QIcon('ryuk.ico'))
msg.setText(message)
msg.setIcon(icon)
msg.exec_()
if exit_on_close:
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.No)
msg.setDefaultButton(QMessageBox.Ok)
result = msg.exec_()
if result == QMessageBox.No:
sys.exit(0)
else:
msg.exec_()
class UpdatePorgressThread(QThread):
global up_cancelled
progress_update = pyqtSignal(int)
def __init__(self, label_progress, progress_bar, label_size):
super().__init__()
self.label_progress = label_progress
self.progress_bar = progress_bar
self.label_size = label_size
self.cancelled = False
def run(self):
try:
update_dir = os.path.join(os.getcwd(), UPDATER_FOLDER)
response = requests.get(LATEST_RELEASE_URL, stream=True)
response.raise_for_status()
current_exe = sys.argv[0]
program_name = os.path.basename(current_exe)
new_exe = os.path.join(update_dir, "BOIIIWD.exe")
if not os.path.exists(update_dir):
os.makedirs(update_dir)
zip_path = os.path.join(update_dir, "latest_version.zip")
total_size = int(response.headers.get('content-length', 0))
size = convert_bytes_to_readable(total_size)
self.label_size.setText(f"Size: {size}")
with open(zip_path, "wb") as zip_file:
chunk_size = 8192
current_size = 0
for chunk in response.iter_content(chunk_size=chunk_size):
if up_cancelled:
break
if chunk:
zip_file.write(chunk)
current_size += len(chunk)
progress = int(current_size / total_size * 100)
self.progress_update.emit(progress)
QCoreApplication.processEvents()
if not up_cancelled:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(update_dir)
self.label_progress.setText("Update installed successfully!")
time.sleep(1)
script_path = create_update_script(current_exe, new_exe, update_dir, program_name)
subprocess.run(('cmd', '/C', 'start', '', fr'{script_path}'))
sys.exit(0)
else:
if os.path.exists(zip_path):
os.remove(fr"{zip_path}")
self.label_progress.setText("Update cancelled.")
except Exception as e:
self.label_progress.setText("Error installing the update.")
show_message("Warning", f"Error installing the update: {e}")
class UpdateProgressWindow(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Updating...")
self.setWindowIcon(QIcon('ryuk.ico'))
layout = QVBoxLayout()
info_layout = QHBoxLayout()
self.label_progress = QLabel("Downloading latest update from Github...")
info_layout.addWidget(self.label_progress, 3)
self.label_size = QLabel("File size: 0KB")
info_layout.addWidget(self.label_size, 1)
layout.addLayout(info_layout)
self.progress_bar = QProgressBar()
layout.addWidget(self.progress_bar)
spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
layout.addSpacerItem(spacer)
button_layout = QHBoxLayout()
self.cancel_button = QPushButton("Cancel")
self.cancel_button.clicked.connect(self.cancel_update)
button_layout.addItem(QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
global up_cancelled
self.thread = None
up_cancelled = False
def update_progress(self, value):
self.progress_bar.setValue(value)
def start_update(self):
self.thread = UpdatePorgressThread(self.label_progress, self.progress_bar, self.label_size)
self.thread.progress_update.connect(self.update_progress)
self.thread.finished.connect(self.on_update_finished)
self.thread.start()
def on_update_finished(self):
"""code"""
# self.accept()
def cancel_update(self):
global up_cancelled
up_cancelled = True
self.label_progress.setText("Update cancelled.")
def closeEvent(self, event: QCloseEvent):
global up_cancelled
if not up_cancelled:
self.cancel_update()
super().closeEvent(event)
class DownloadThread(QThread):
finished = pyqtSignal()
@ -253,12 +482,15 @@ class WorkshopDownloaderApp(QWidget):
download_button = msg_box.addButton("Download", QMessageBox.AcceptRole)
download_button.clicked.connect(self.download_steamcmd)
msg_box.setDefaultButton(download_button)
result = msg_box.exec_()
if result == QMessageBox.Cancel:
sys.exit(0)
def download_steamcmd(self):
self.edit_steamcmd_path.setText(cwd())
self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
steamcmd_url = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"
steamcmd_zip_path = os.path.join(cwd(), "steamcmd.zip")
@ -273,28 +505,68 @@ class WorkshopDownloaderApp(QWidget):
zip_ref.extractall(cwd())
if check_steamcmd():
show_message("Success", "SteamCMD has been downloaded and extracted.", icon=QMessageBox.Information)
os.remove(steamcmd_zip_path)
os.remove(fr"{steamcmd_zip_path}")
show_message("Success", "SteamCMD has been downloaded ,Press ok to initialize it.", icon=QMessageBox.Information, exit_on_close=True)
initialize_steam()
else:
show_message("Error", "Failed to find steamcmd.exe after extraction.")
os.remove(steamcmd_zip_path)
show_message("Error", "Failed to find steamcmd.exe after extraction.\nMake you sure to select the correct SteamCMD path (which is the current BOIIIWD path)")
os.remove(fr"{steamcmd_zip_path}")
except requests.exceptions.RequestException as e:
show_message("Error", f"Failed to download SteamCMD: {e}")
os.remove(steamcmd_zip_path)
os.remove(fr"{steamcmd_zip_path}")
except zipfile.BadZipFile:
show_message("Error", "Failed to extract SteamCMD. The downloaded file might be corrupted.")
os.remove(steamcmd_zip_path)
os.remove(fr"{steamcmd_zip_path}")
def check_for_updates(self, ignore_up_todate=False):
try:
latest_version = get_latest_release_version()
current_version = VERSION
if latest_version and latest_version != current_version:
msg_box = QMessageBox()
msg_box.setWindowTitle("Update Available")
msg_box.setWindowIcon(QIcon('ryuk.ico'))
msg_box.setText(f"An update is available!, Do you want to install it?\n\nCurrent Version: {current_version}\nLatest Version: {latest_version}")
msg_box.setIcon(QMessageBox.Information)
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Open)
msg_box.setDefaultButton(QMessageBox.Yes)
result = msg_box.exec_()
if result == QMessageBox.Open:
webbrowser.open(f"https://github.com/{GITHUB_REPO}/releases/latest")
if result == QMessageBox.Yes:
update_progress_window = UpdateProgressWindow()
update_progress_window.start_update()
update_progress_window.exec_()
elif latest_version == current_version:
if ignore_up_todate:
return
msg_box = QMessageBox()
msg_box.setWindowTitle("Up to Date!")
msg_box.setWindowIcon(QIcon('ryuk.ico'))
msg_box.setText(f"No Updates Available!")
msg_box.setIcon(QMessageBox.Information)
msg_box.setStandardButtons(QMessageBox.Ok)
msg_box.setDefaultButton(QMessageBox.Ok)
result = msg_box.exec_()
except Exception as e:
show_message("Error", f"Error while checking for updates: \n{e}")
def initUI(self):
self.setWindowTitle('BOIII Workshop Downloader v0.1.2-beta')
self.setWindowTitle(f'BOIII Workshop Downloader {VERSION}-beta')
self.setWindowIcon(QIcon('ryuk.ico'))
self.setGeometry(100, 100, 400, 200)
self.settings = QSettings("MyApp", "MyWindow")
self.restore_geometry()
layout = QVBoxLayout()
browse_layout = QHBoxLayout()
self.label_workshop_id = QLabel("Enter the Workshop ID of the map/mod you want to download:")
self.label_workshop_id = QLabel("Enter the Workshop ID or Link of the map/mod you want to download:")
browse_layout.addWidget(self.label_workshop_id, 3)
self.button_browse = QPushButton("Browse")
@ -307,7 +579,7 @@ class WorkshopDownloaderApp(QWidget):
info_workshop_layout = QHBoxLayout()
self.edit_workshop_id = QLineEdit()
self.edit_workshop_id.setPlaceholderText("Workshop ID => Press info to see map/mod info")
self.edit_workshop_id.setPlaceholderText("Workshop ID/Link => Press info to see map/mod info")
self.edit_workshop_id.textChanged.connect(self.reset_file_size)
info_workshop_layout.addWidget(self.edit_workshop_id, 3)
@ -352,7 +624,7 @@ class WorkshopDownloaderApp(QWidget):
self.button_download = QPushButton("Download")
self.button_download.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.button_download.clicked.connect(self.download_map)
buttons_layout.addWidget(self.button_download, 75)
buttons_layout.addWidget(self.button_download, 70)
self.button_stop = QPushButton("Stop")
self.button_stop.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@ -377,32 +649,86 @@ class WorkshopDownloaderApp(QWidget):
self.progress_bar = QProgressBar()
layout.addWidget(self.progress_bar, 75)
spacer = QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)
layout.addSpacerItem(spacer)
check_for_update_layout = QHBoxLayout()
check_update_button = QPushButton("Check for Updates")
check_update_button.clicked.connect(self.check_for_updates)
check_update_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.check_for_update_layout = QVBoxLayout()
self.check_for_update_layout.addWidget(check_update_button)
self.show_more_button = QPushButton("Launch boiii")
self.show_more_button.clicked.connect(self.launch_boiii)
check_for_update_layout = QHBoxLayout()
check_for_update_layout.addWidget(check_update_button)
self.check_for_updates_checkbox = QPushButton("Settings")
self.check_for_updates_checkbox.clicked.connect(self.open_settings_dialog)
check_for_update_layout = QHBoxLayout()
check_for_update_layout.addWidget(check_update_button)
check_for_update_layout.addWidget(self.check_for_updates_checkbox)
check_for_update_layout.addWidget(self.show_more_button)
layout.addLayout(check_for_update_layout)
layout.addLayout(check_for_update_layout)
self.setLayout(layout)
self.load_config()
if config_check_for_updates() == "on":
self.check_for_updates(ignore_up_todate=True)
try:
global console
if config_console_state() == "on":
console = True
return 1
else:
console = False
return 0
except:
pass
def download_map(self):
global stopped
stopped = False
self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
if not check_steamcmd():
self.show_warning_message()
return
workshop_id = self.edit_workshop_id.text()
if not workshop_id.isdigit():
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
if not valid_id(workshop_id):
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
steamcmd_path = get_steamcmd_path()
steamcmd_exe_path = os.path.join(steamcmd_path, "steamcmd.exe")
steamcmd_size = os.path.getsize(steamcmd_exe_path)
if steamcmd_size < 3 * 1024 * 1024:
show_message("Warning", "Please wait a bit until SteamCMD downloads and initializes. It might take some time, but it will only happen once.", icon=QMessageBox.Warning)
show_message("Warning", "SteamCMD is not initialized, Press OK to do so!\nProgram may go unresponsive until SteamCMD is finished downloading.", icon=QMessageBox.Warning, exit_on_close=True)
initialize_steam()
return
workshop_id = self.edit_workshop_id.text().strip()
if not workshop_id.isdigit():
try:
if extract_workshop_id(workshop_id).strip().isdigit():
workshop_id = extract_workshop_id(workshop_id).strip()
else:
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
except:
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
if not valid_id(workshop_id):
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
destination_folder = self.edit_destination_folder.text()
steamcmd_path = self.edit_steamcmd_path.text()
@ -443,11 +769,13 @@ class WorkshopDownloaderApp(QWidget):
selected_folder = QFileDialog.getExistingDirectory(self, "Select BOIII Folder", "")
if selected_folder:
self.edit_destination_folder.setText(selected_folder)
self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
def open_steamcmd_path_browser(self):
selected_folder = QFileDialog.getExistingDirectory(self, "Select SteamCMD Folder", "")
if selected_folder:
self.edit_steamcmd_path.setText(selected_folder)
self.save_config(self.edit_destination_folder.text(), self.edit_steamcmd_path.text())
def on_download_finished(self):
self.button_download.setEnabled(True)
@ -484,15 +812,22 @@ class WorkshopDownloaderApp(QWidget):
self.label_file_size.setText(f"File size: 0KB")
def show_map_info(self):
workshop_id = self.edit_workshop_id.text()
workshop_id = self.edit_workshop_id.text().strip()
if not workshop_id:
QMessageBox.warning(self, "Warning", "Please enter a Workshop ID first.")
return
if not workshop_id.isdigit():
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
try:
if extract_workshop_id(workshop_id).strip().isdigit():
workshop_id = extract_workshop_id(workshop_id).strip()
else:
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
except:
QMessageBox.warning(self, "Warning", "Please enter a valid Workshop ID.")
return
self.label_file_size.setText(f"File size: {get_workshop_file_size(workshop_id, raw=True)}")
try:
@ -542,6 +877,7 @@ class WorkshopDownloaderApp(QWidget):
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Map/Mod Information")
msg_box.setWindowIcon(QIcon('ryuk.ico'))
msg_box.setIconPixmap(pixmap)
msg_box.setText(f"Name: {map_name}\nType: {map_mod_type}\nSize: {map_size}")
@ -556,7 +892,100 @@ class WorkshopDownloaderApp(QWidget):
msg_box.exec_()
except requests.exceptions.RequestException as e:
QMessageBox.warning(self, "Error", f"Failed to fetch map information.\nError: {e}")
show_message("Error", f"Failed to fetch map information.\nError: {e}")
def launch_boiii(self):
try:
boiii_path = os.path.join(self.edit_destination_folder.text(), "boiii.exe")
subprocess.Popen([boiii_path], cwd=self.edit_destination_folder.text())
except Exception as e:
show_message("Error: Failed to launch BOIII", f"Failed to launch boiii.exe\nMake sure to put in your correct boiii path\n{e}")
def open_settings_dialog(self):
settings_dialog = SettingsDialog()
settings_dialog.exec_()
def closeEvent(self, event):
self.settings.setValue("geometry", self.saveGeometry())
super().closeEvent(event)
def restore_geometry(self):
geometry = self.settings.value("geometry", None)
if geometry is not None:
self.restoreGeometry(geometry)
class SettingsDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Settings")
self.setWindowIcon(QIcon('ryuk.ico'))
self.setGeometry(50, 50, 250, 120)
self.settings = QSettings("MyApp2", "MyWindow2")
self.restore_geometry()
self.initUI()
def initUI(self):
layout = QVBoxLayout()
self.check_updates_checkbox = QCheckBox("Check for updates on launch")
self.check_updates_checkbox.setChecked(self.load_settings(updates=True))
layout.addWidget(self.check_updates_checkbox)
buttons_layout = QHBoxLayout()
self.checkbox_show_console = QCheckBox("Console (On Download)", self)
self.checkbox_show_console.setChecked(self.load_settings(console=True))
tooltip_text = "<font color='black'>Toggle SteamCMD console\nPlease don't close the Console If you want to stop press the Stop boutton.</font>"
self.checkbox_show_console.setToolTip(tooltip_text)
buttons_layout.addWidget(self.checkbox_show_console, 5)
layout.addLayout(buttons_layout)
save_button = QPushButton("Save")
save_button.setFixedWidth(60)
save_button.clicked.connect(self.save_settings)
layout.addWidget(save_button, alignment=Qt.AlignLeft)
self.setLayout(layout)
def save_settings(self):
global console
if self.check_updates_checkbox.isChecked():
config_check_for_updates(state="on")
else:
config_check_for_updates(state="off")
if self.checkbox_show_console.isChecked():
config_console_state(state="on")
console = True
else:
config_console_state(state="off")
console = False
self.accept()
def load_settings(self, console=None, updates=None):
if updates:
if config_check_for_updates() == "on":
return 1
else:
return 0
if console:
if config_console_state() == "on":
console = True
return 1
else:
console = False
return 0
def closeEvent(self, event):
self.settings.setValue("geometry", self.saveGeometry())
super().closeEvent(event)
def restore_geometry(self):
geometry = self.settings.value("geometry", None)
if geometry is not None:
self.restoreGeometry(geometry)
if __name__ == "__main__":
app = QApplication(sys.argv)