2023-07-31 02:02:53 -04:00
import os
import sys
2023-08-03 04:13:02 -04:00
import re
2023-07-31 02:02:53 -04:00
import subprocess
import configparser
import json
2023-07-31 14:45:46 -04:00
import shutil
2023-07-31 16:44:31 -04:00
import zipfile
2023-07-31 12:40:41 -04:00
import psutil
2023-07-31 02:02:53 -04:00
import requests
import time
import threading
from bs4 import BeautifulSoup
2023-08-03 04:13:02 -04:00
from PyQt5 . QtWidgets import QApplication , QWidget , QLabel , QLineEdit , QPushButton , QDialog , \
QVBoxLayout , QMessageBox , QHBoxLayout , QProgressBar , QSizePolicy , QFileDialog , QCheckBox , QSpacerItem
2023-07-31 02:02:53 -04:00
from PyQt5 . QtCore import Qt , QThread , pyqtSignal
2023-08-03 04:13:02 -04:00
from PyQt5 . QtCore import QCoreApplication , QSettings
from PyQt5 . QtGui import QIcon , QPixmap , QCloseEvent
2023-07-31 09:29:45 -04:00
import webbrowser
2023-07-31 02:02:53 -04:00
import qdarktheme
2023-08-03 04:13:02 -04:00
VERSION = " v0.1.3 "
GITHUB_REPO = " faroukbmiled/BOIIIWD "
LATEST_RELEASE_URL = " https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip "
UPDATER_FOLDER = " update "
2023-07-31 02:02:53 -04:00
CONFIG_FILE_PATH = " config.ini "
2023-08-03 04:13:02 -04:00
global stopped , steampid , console , up_cancelled
2023-07-31 12:40:41 -04:00
steampid = None
2023-07-31 02:02:53 -04:00
stopped = False
2023-08-03 04:13:02 -04:00
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
2023-07-31 02:02:53 -04:00
2023-07-31 10:39:56 -04:00
def cwd ( ) :
if getattr ( sys , ' frozen ' , False ) :
return os . path . dirname ( sys . executable )
else :
return os . path . dirname ( os . path . abspath ( __file__ ) )
2023-08-03 04:13:02 -04:00
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
2023-07-31 16:44:31 -04:00
def check_steamcmd ( ) :
steamcmd_path = get_steamcmd_path ( )
steamcmd_exe_path = os . path . join ( steamcmd_path , " steamcmd.exe " )
if not os . path . exists ( steamcmd_exe_path ) :
return False
return True
2023-08-03 04:13:02 -04:00
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. " )
2023-07-31 21:48:40 -04:00
def valid_id ( workshop_id ) :
url = f " https://steamcommunity.com/sharedfiles/filedetails/?id= { workshop_id } "
response = requests . get ( url )
response . raise_for_status ( )
content = response . text
soup = BeautifulSoup ( content , " html.parser " )
try :
soup . find ( " div " , class_ = " rightDetailsBlock " ) . text . strip ( )
soup . find ( " div " , class_ = " workshopItemTitle " ) . text . strip ( )
soup . find ( " div " , class_ = " detailsStatRight " ) . text . strip ( )
stars_div = soup . find ( " div " , class_ = " fileRatingDetails " )
stars_div . find ( " img " ) [ " src " ]
return True
except :
return False
2023-07-31 13:48:21 -04:00
def convert_speed ( speed_bytes ) :
if speed_bytes < 1024 :
return speed_bytes , " B/s "
elif speed_bytes < 1024 * 1024 :
return speed_bytes / 1024 , " KB/s "
elif speed_bytes < 1024 * 1024 * 1024 :
return speed_bytes / ( 1024 * 1024 ) , " MB/s "
else :
return speed_bytes / ( 1024 * 1024 * 1024 ) , " GB/s "
2023-07-31 02:02:53 -04:00
def create_default_config ( ) :
config = configparser . ConfigParser ( )
config [ " Settings " ] = {
2023-07-31 10:39:56 -04:00
" SteamCMDPath " : cwd ( ) ,
2023-08-03 04:13:02 -04:00
" DestinationFolder " : " " ,
" checkforupdtes " : " on " ,
" console " : " off "
2023-07-31 02:02:53 -04:00
}
with open ( CONFIG_FILE_PATH , " w " ) as config_file :
config . write ( config_file )
def run_steamcmd_command ( command ) :
steamcmd_path = get_steamcmd_path ( )
2023-08-03 04:13:02 -04:00
show_console = subprocess . CREATE_NO_WINDOW
if console :
show_console = subprocess . CREATE_NEW_CONSOLE
2023-07-31 16:52:22 -04:00
process = subprocess . Popen (
[ steamcmd_path + " \ steamcmd.exe " ] + command . split ( ) ,
2023-08-03 04:13:02 -04:00
stdout = None if console else subprocess . PIPE ,
stderr = None if console else subprocess . PIPE ,
2023-07-31 16:52:22 -04:00
text = True ,
bufsize = 1 ,
universal_newlines = True ,
2023-08-03 04:13:02 -04:00
creationflags = show_console
2023-07-31 16:52:22 -04:00
)
2023-07-31 12:40:41 -04:00
global steampid
steampid = process . pid
2023-07-31 02:02:53 -04:00
2023-07-31 14:45:46 -04:00
if process . poll ( ) is not None :
return process . returncode
process . communicate ( )
if process . returncode != 0 :
show_message ( " Warning " , " SteamCMD encountered an error while downloading, try again! " )
return process . returncode
2023-07-31 02:02:53 -04:00
def get_steamcmd_path ( ) :
config = configparser . ConfigParser ( )
config . read ( CONFIG_FILE_PATH )
2023-07-31 10:39:56 -04:00
return config . get ( " Settings " , " SteamCMDPath " , fallback = cwd ( ) )
2023-07-31 02:02:53 -04:00
2023-08-03 04:13:02 -04:00
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 " )
2023-07-31 02:02:53 -04:00
def extract_json_data ( json_path ) :
with open ( json_path , " r " ) as json_file :
data = json . load ( json_file )
return data [ " Type " ] , data [ " FolderName " ]
2023-08-01 09:32:32 -04:00
def convert_bytes_to_readable ( size_in_bytes ) :
for unit in [ ' B ' , ' KB ' , ' MB ' , ' GB ' , ' TB ' ] :
if size_in_bytes < 1024.0 :
return f " { size_in_bytes : .2f } { unit } "
size_in_bytes / = 1024.0
2023-07-31 16:44:31 -04:00
def get_workshop_file_size ( workshop_id , raw = None ) :
2023-07-31 02:02:53 -04:00
url = f " https://steamcommunity.com/sharedfiles/filedetails/?id= { workshop_id } &searchtext= "
response = requests . get ( url )
soup = BeautifulSoup ( response . text , " html.parser " )
file_size_element = soup . find ( " div " , class_ = " detailsStatRight " )
2023-07-31 16:44:31 -04:00
2023-07-31 21:48:40 -04:00
try :
if raw :
file_size_text = file_size_element . get_text ( strip = True )
file_size_text = file_size_text . replace ( " , " , " " )
2023-08-01 09:32:32 -04:00
file_size_in_mb = float ( file_size_text . replace ( " MB " , " " ) )
file_size_in_bytes = int ( file_size_in_mb * 1024 * 1024 )
return convert_bytes_to_readable ( file_size_in_bytes )
2023-07-31 21:48:40 -04:00
if file_size_element :
file_size_text = file_size_element . get_text ( strip = True )
file_size_text = file_size_text . replace ( " , " , " " )
file_size_in_mb = float ( file_size_text . replace ( " MB " , " " ) )
file_size_in_bytes = int ( file_size_in_mb * 1024 * 1024 )
return file_size_in_bytes
return None
except :
return None
2023-07-31 02:02:53 -04:00
def update_progress_bar ( current_size , file_size , progress_bar ) :
if file_size is not None :
progress = int ( current_size / file_size * 100 )
progress_bar . setValue ( progress )
2023-07-31 12:40:41 -04:00
def check_and_update_progress ( file_size , folder_name_path , progress_bar , speed_label ) :
previous_net_speed = 0
2023-07-31 02:02:53 -04:00
while not stopped :
current_size = sum ( os . path . getsize ( os . path . join ( folder_name_path , f ) ) for f in os . listdir ( folder_name_path ) )
update_progress_bar ( current_size , file_size , progress_bar )
2023-07-31 12:40:41 -04:00
current_net_speed = psutil . net_io_counters ( ) . bytes_recv
2023-07-31 13:48:21 -04:00
net_speed_bytes = current_net_speed - previous_net_speed
2023-07-31 12:40:41 -04:00
previous_net_speed = current_net_speed
2023-07-31 13:48:21 -04:00
net_speed , speed_unit = convert_speed ( net_speed_bytes )
speed_label . setText ( f " Network Speed: { net_speed : .2f } { speed_unit } " )
2023-07-31 12:40:41 -04:00
2023-07-31 02:02:53 -04:00
QCoreApplication . processEvents ( )
time . sleep ( 1 )
2023-07-31 12:40:41 -04:00
def download_workshop_map ( workshop_id , destination_folder , progress_bar , speed_label ) :
2023-07-31 02:02:53 -04:00
file_size = get_workshop_file_size ( workshop_id )
if file_size is None :
show_message ( " Error " , " Failed to retrieve file size. " )
return
2023-07-31 10:39:56 -04:00
download_folder = os . path . join ( get_steamcmd_path ( ) , " steamapps " , " workshop " , " downloads " , " 311210 " , workshop_id )
2023-07-31 02:02:53 -04:00
if not os . path . exists ( download_folder ) :
os . makedirs ( download_folder )
command = f " +login anonymous +workshop_download_item 311210 { workshop_id } +quit "
2023-07-31 12:40:41 -04:00
progress_thread = threading . Thread ( target = check_and_update_progress , args = ( file_size , download_folder , progress_bar , speed_label ) )
2023-07-31 02:02:53 -04:00
progress_thread . daemon = True
progress_thread . start ( )
run_steamcmd_command ( command )
global stopped
stopped = True
progress_bar . setValue ( 100 )
2023-07-31 10:39:56 -04:00
map_folder = os . path . join ( get_steamcmd_path ( ) , " steamapps " , " workshop " , " content " , " 311210 " , workshop_id )
2023-07-31 02:02:53 -04:00
json_file_path = os . path . join ( map_folder , " workshop.json " )
if os . path . exists ( json_file_path ) :
global mod_type
mod_type , folder_name = extract_json_data ( json_file_path )
if mod_type == " mod " :
mods_folder = os . path . join ( destination_folder , " mods " )
folder_name_path = os . path . join ( mods_folder , folder_name , " zone " )
elif mod_type == " map " :
usermaps_folder = os . path . join ( destination_folder , " usermaps " )
folder_name_path = os . path . join ( usermaps_folder , folder_name , " zone " )
else :
show_message ( " Error " , " Invalid map type in workshop.json. " )
return
os . makedirs ( folder_name_path , exist_ok = True )
2023-07-31 14:45:46 -04:00
try :
shutil . copytree ( map_folder , folder_name_path , dirs_exist_ok = True )
except Exception as E :
2023-08-03 04:13:02 -04:00
show_message ( " Error " , f " Error copying files: { E } " )
2023-07-31 02:02:53 -04:00
2023-07-31 21:48:40 -04:00
show_message ( " Download Complete " , f " { mod_type } files are downloaded at \n { folder_name_path } \n You can run the game now! " , icon = QMessageBox . Information )
2023-07-31 02:02:53 -04:00
2023-08-03 04:13:02 -04:00
def show_message ( title , message , icon = QMessageBox . Warning , exit_on_close = False ) :
2023-07-31 02:02:53 -04:00
msg = QMessageBox ( )
msg . setWindowTitle ( title )
2023-08-03 04:13:02 -04:00
msg . setWindowIcon ( QIcon ( ' ryuk.ico ' ) )
2023-07-31 02:02:53 -04:00
msg . setText ( message )
2023-07-31 16:44:31 -04:00
msg . setIcon ( icon )
2023-08-03 04:13:02 -04:00
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 )
2023-07-31 02:02:53 -04:00
class DownloadThread ( QThread ) :
finished = pyqtSignal ( )
2023-07-31 12:40:41 -04:00
def __init__ ( self , workshop_id , destination_folder , progress_bar , label_speed ) :
2023-07-31 02:02:53 -04:00
super ( ) . __init__ ( )
self . workshop_id = workshop_id
self . destination_folder = destination_folder
self . progress_bar = progress_bar
2023-07-31 12:40:41 -04:00
self . label_speed = label_speed
2023-07-31 02:02:53 -04:00
def run ( self ) :
2023-07-31 12:40:41 -04:00
download_workshop_map ( self . workshop_id , self . destination_folder , self . progress_bar , self . label_speed )
2023-07-31 02:02:53 -04:00
self . finished . emit ( )
class WorkshopDownloaderApp ( QWidget ) :
def __init__ ( self ) :
super ( ) . __init__ ( )
self . initUI ( )
2023-07-31 16:44:31 -04:00
if not check_steamcmd ( ) :
self . show_warning_message ( )
2023-07-31 02:02:53 -04:00
self . download_thread = None
self . button_download . setEnabled ( True )
self . button_stop . setEnabled ( False )
2023-07-31 16:44:31 -04:00
def show_warning_message ( self ) :
msg_box = QMessageBox ( self )
msg_box . setWindowTitle ( " Warning " )
msg_box . setWindowIcon ( QIcon ( ' ryuk.ico ' ) )
msg_box . setText ( " steamcmd.exe was not found in the specified directory. \n Press Download to get it or Press OK and select it from there!. " )
msg_box . setStandardButtons ( QMessageBox . Ok | QMessageBox . Cancel )
download_button = msg_box . addButton ( " Download " , QMessageBox . AcceptRole )
download_button . clicked . connect ( self . download_steamcmd )
2023-08-03 04:13:02 -04:00
msg_box . setDefaultButton ( download_button )
2023-07-31 16:44:31 -04:00
result = msg_box . exec_ ( )
if result == QMessageBox . Cancel :
sys . exit ( 0 )
def download_steamcmd ( self ) :
2023-08-03 04:13:02 -04:00
self . edit_steamcmd_path . setText ( cwd ( ) )
self . save_config ( self . edit_destination_folder . text ( ) , self . edit_steamcmd_path . text ( ) )
2023-07-31 16:44:31 -04:00
steamcmd_url = " https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip "
steamcmd_zip_path = os . path . join ( cwd ( ) , " steamcmd.zip " )
try :
response = requests . get ( steamcmd_url )
response . raise_for_status ( )
with open ( steamcmd_zip_path , " wb " ) as zip_file :
zip_file . write ( response . content )
with zipfile . ZipFile ( steamcmd_zip_path , " r " ) as zip_ref :
zip_ref . extractall ( cwd ( ) )
if check_steamcmd ( ) :
2023-08-03 04:13:02 -04:00
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 ( )
2023-07-31 16:44:31 -04:00
else :
2023-08-03 04:13:02 -04:00
show_message ( " Error " , " Failed to find steamcmd.exe after extraction. \n Make you sure to select the correct SteamCMD path (which is the current BOIIIWD path) " )
os . remove ( fr " { steamcmd_zip_path } " )
2023-07-31 16:44:31 -04:00
except requests . exceptions . RequestException as e :
show_message ( " Error " , f " Failed to download SteamCMD: { e } " )
2023-08-03 04:13:02 -04:00
os . remove ( fr " { steamcmd_zip_path } " )
2023-07-31 16:44:31 -04:00
except zipfile . BadZipFile :
show_message ( " Error " , " Failed to extract SteamCMD. The downloaded file might be corrupted. " )
2023-08-03 04:13:02 -04:00
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 \n Current Version: { current_version } \n Latest 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 } " )
2023-07-31 16:44:31 -04:00
2023-07-31 02:02:53 -04:00
def initUI ( self ) :
2023-08-03 04:13:02 -04:00
self . setWindowTitle ( f ' BOIII Workshop Downloader { VERSION } -beta ' )
2023-07-31 09:29:45 -04:00
self . setWindowIcon ( QIcon ( ' ryuk.ico ' ) )
2023-07-31 02:02:53 -04:00
self . setGeometry ( 100 , 100 , 400 , 200 )
2023-08-03 04:13:02 -04:00
self . settings = QSettings ( " MyApp " , " MyWindow " )
self . restore_geometry ( )
2023-07-31 02:02:53 -04:00
layout = QVBoxLayout ( )
2023-07-31 09:29:45 -04:00
browse_layout = QHBoxLayout ( )
2023-08-03 04:13:02 -04:00
self . label_workshop_id = QLabel ( " Enter the Workshop ID or Link of the map/mod you want to download: " )
2023-07-31 09:29:45 -04:00
browse_layout . addWidget ( self . label_workshop_id , 3 )
self . button_browse = QPushButton ( " Browse " )
self . button_browse . setSizePolicy ( QSizePolicy . Expanding , QSizePolicy . Expanding )
self . button_browse . clicked . connect ( self . open_browser )
browse_layout . addWidget ( self . button_browse , 1 )
layout . addLayout ( browse_layout )
2023-07-31 02:02:53 -04:00
2023-07-31 16:44:31 -04:00
info_workshop_layout = QHBoxLayout ( )
2023-07-31 02:02:53 -04:00
self . edit_workshop_id = QLineEdit ( )
2023-08-03 04:13:02 -04:00
self . edit_workshop_id . setPlaceholderText ( " Workshop ID/Link => Press info to see map/mod info " )
2023-08-01 11:07:44 -04:00
self . edit_workshop_id . textChanged . connect ( self . reset_file_size )
2023-07-31 16:44:31 -04:00
info_workshop_layout . addWidget ( self . edit_workshop_id , 3 )
layout . addLayout ( info_workshop_layout )
self . info_button = QPushButton ( " Info " )
self . info_button . setSizePolicy ( QSizePolicy . Expanding , QSizePolicy . Expanding )
self . info_button . clicked . connect ( self . show_map_info )
info_workshop_layout . addWidget ( self . info_button , 1 )
2023-07-31 02:02:53 -04:00
self . label_destination_folder = QLabel ( " Enter Your BOIII folder: " )
2023-07-31 16:44:31 -04:00
layout . addWidget ( self . label_destination_folder , 3 )
2023-07-31 02:02:53 -04:00
2023-07-31 16:44:31 -04:00
Boiii_Input = QHBoxLayout ( )
2023-07-31 02:02:53 -04:00
self . edit_destination_folder = QLineEdit ( )
2023-07-31 16:44:31 -04:00
self . edit_destination_folder . setPlaceholderText ( " Your BOIII Instalation folder " )
Boiii_Input . addWidget ( self . edit_destination_folder , 90 )
layout . addLayout ( Boiii_Input )
self . button_BOIII_browse = QPushButton ( " Select " )
self . button_BOIII_browse . setSizePolicy ( QSizePolicy . Expanding , QSizePolicy . Expanding )
self . button_BOIII_browse . clicked . connect ( self . open_BOIII_browser )
Boiii_Input . addWidget ( self . button_BOIII_browse , 10 )
2023-07-31 02:02:53 -04:00
self . label_steamcmd_path = QLabel ( " Enter SteamCMD path (default): " )
layout . addWidget ( self . label_steamcmd_path )
2023-07-31 16:44:31 -04:00
steamcmd_path = QHBoxLayout ( )
2023-07-31 02:02:53 -04:00
self . edit_steamcmd_path = QLineEdit ( )
2023-07-31 16:44:31 -04:00
steamcmd_path . addWidget ( self . edit_steamcmd_path , 90 )
self . button_steamcmd_browse = QPushButton ( " Select " )
self . button_steamcmd_browse . setSizePolicy ( QSizePolicy . Expanding , QSizePolicy . Expanding )
self . button_steamcmd_browse . clicked . connect ( self . open_steamcmd_path_browser )
steamcmd_path . addWidget ( self . button_steamcmd_browse , 10 )
layout . addLayout ( steamcmd_path )
layout . addSpacing ( 10 )
2023-07-31 02:02:53 -04:00
buttons_layout = QHBoxLayout ( )
self . button_download = QPushButton ( " Download " )
self . button_download . setSizePolicy ( QSizePolicy . Expanding , QSizePolicy . Expanding )
self . button_download . clicked . connect ( self . download_map )
2023-08-03 04:13:02 -04:00
buttons_layout . addWidget ( self . button_download , 70 )
2023-07-31 02:02:53 -04:00
self . button_stop = QPushButton ( " Stop " )
self . button_stop . setSizePolicy ( QSizePolicy . Expanding , QSizePolicy . Expanding )
self . button_stop . clicked . connect ( self . stop_download )
buttons_layout . addWidget ( self . button_stop , 25 )
layout . addLayout ( buttons_layout )
2023-07-31 16:44:31 -04:00
InfoBar = QHBoxLayout ( )
2023-07-31 12:40:41 -04:00
self . label_speed = QLabel ( " Network Speed: 0 KB/s " )
2023-07-31 16:44:31 -04:00
InfoBar . addWidget ( self . label_speed , 3 )
self . label_file_size = QLabel ( " File size: 0KB " )
InfoBar . addWidget ( self . label_file_size , 1 )
InfoWidget = QWidget ( )
InfoWidget . setLayout ( InfoBar )
layout . addWidget ( InfoWidget )
2023-07-31 12:40:41 -04:00
2023-07-31 02:02:53 -04:00
self . progress_bar = QProgressBar ( )
2023-07-31 16:44:31 -04:00
layout . addWidget ( self . progress_bar , 75 )
2023-07-31 02:02:53 -04:00
2023-08-03 04:13:02 -04:00
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 )
2023-07-31 02:02:53 -04:00
self . setLayout ( layout )
self . load_config ( )
2023-08-03 04:13:02 -04:00
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
2023-07-31 02:02:53 -04:00
def download_map ( self ) :
global stopped
stopped = False
2023-08-03 04:13:02 -04:00
self . save_config ( self . edit_destination_folder . text ( ) , self . edit_steamcmd_path . text ( ) )
2023-07-31 02:02:53 -04:00
2023-07-31 16:44:31 -04:00
if not check_steamcmd ( ) :
self . show_warning_message ( )
return
2023-08-03 04:13:02 -04:00
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 " , " SteamCMD is not initialized, Press OK to do so! \n Program may go unresponsive until SteamCMD is finished downloading. " , icon = QMessageBox . Warning , exit_on_close = True )
initialize_steam ( )
2023-07-31 21:48:40 -04:00
return
2023-08-03 04:13:02 -04:00
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
2023-07-31 21:48:40 -04:00
if not valid_id ( workshop_id ) :
QMessageBox . warning ( self , " Warning " , " Please enter a valid Workshop ID. " )
return
2023-07-31 02:02:53 -04:00
destination_folder = self . edit_destination_folder . text ( )
steamcmd_path = self . edit_steamcmd_path . text ( )
2023-07-31 16:44:31 -04:00
self . label_file_size . setText ( f " File size: { get_workshop_file_size ( workshop_id , raw = True ) } " )
2023-07-31 02:02:53 -04:00
if not destination_folder :
show_message ( " Error " , " Please select a destination folder. " )
return
if not steamcmd_path :
show_message ( " Error " , " Please enter the SteamCMD path. " )
return
self . button_stop . setEnabled ( True )
self . progress_bar . setValue ( 0 )
self . button_download . setEnabled ( False )
2023-07-31 12:40:41 -04:00
self . download_thread = DownloadThread ( workshop_id , destination_folder , self . progress_bar , self . label_speed )
2023-07-31 02:02:53 -04:00
self . download_thread . finished . connect ( self . on_download_finished )
self . download_thread . start ( )
def stop_download ( self ) :
global stopped
stopped = True
subprocess . run ( [ ' taskkill ' , ' /F ' , ' /IM ' , ' steamcmd.exe ' ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE )
if self . download_thread and self . download_thread . isRunning ( ) :
self . download_thread . terminate ( )
self . button_download . setEnabled ( True )
self . button_stop . setEnabled ( False )
2023-07-31 12:40:41 -04:00
self . progress_bar . setValue ( 0 )
self . label_speed . setText ( f " Network Speed: { 0 : .2f } KB/s " )
2023-07-31 16:44:31 -04:00
self . label_file_size . setText ( f " File size: 0KB " )
def open_BOIII_browser ( self ) :
selected_folder = QFileDialog . getExistingDirectory ( self , " Select BOIII Folder " , " " )
if selected_folder :
self . edit_destination_folder . setText ( selected_folder )
2023-08-03 04:13:02 -04:00
self . save_config ( self . edit_destination_folder . text ( ) , self . edit_steamcmd_path . text ( ) )
2023-07-31 16:44:31 -04:00
def open_steamcmd_path_browser ( self ) :
selected_folder = QFileDialog . getExistingDirectory ( self , " Select SteamCMD Folder " , " " )
if selected_folder :
self . edit_steamcmd_path . setText ( selected_folder )
2023-08-03 04:13:02 -04:00
self . save_config ( self . edit_destination_folder . text ( ) , self . edit_steamcmd_path . text ( ) )
2023-07-31 02:02:53 -04:00
def on_download_finished ( self ) :
self . button_download . setEnabled ( True )
2023-07-31 12:40:41 -04:00
self . progress_bar . setValue ( 0 )
self . label_speed . setText ( f " Network Speed: { 0 : .2f } KB/s " )
2023-07-31 16:44:31 -04:00
self . label_file_size . setText ( f " File size: 0KB " )
self . button_stop . setEnabled ( False )
2023-07-31 02:02:53 -04:00
self . save_config ( self . edit_destination_folder . text ( ) , self . edit_steamcmd_path . text ( ) )
2023-07-31 09:29:45 -04:00
def open_browser ( self ) :
link = " https://steamcommunity.com/app/311210/workshop/ "
webbrowser . open ( link )
2023-07-31 02:02:53 -04:00
def load_config ( self ) :
config = configparser . ConfigParser ( )
if os . path . exists ( CONFIG_FILE_PATH ) :
config . read ( CONFIG_FILE_PATH )
destination_folder = config . get ( " Settings " , " DestinationFolder " , fallback = " " )
2023-07-31 10:39:56 -04:00
steamcmd_path = config . get ( " Settings " , " SteamCMDPath " , fallback = cwd ( ) )
2023-07-31 02:02:53 -04:00
self . edit_destination_folder . setText ( destination_folder )
self . edit_steamcmd_path . setText ( steamcmd_path )
else :
create_default_config ( )
def save_config ( self , destination_folder , steamcmd_path ) :
config = configparser . ConfigParser ( )
config . read ( CONFIG_FILE_PATH )
config . set ( " Settings " , " DestinationFolder " , destination_folder )
config . set ( " Settings " , " SteamCMDPath " , steamcmd_path )
with open ( CONFIG_FILE_PATH , " w " ) as config_file :
config . write ( config_file )
2023-08-01 11:07:44 -04:00
def reset_file_size ( self ) :
self . label_file_size . setText ( f " File size: 0KB " )
2023-07-31 16:44:31 -04:00
def show_map_info ( self ) :
2023-08-03 04:13:02 -04:00
workshop_id = self . edit_workshop_id . text ( ) . strip ( )
2023-07-31 21:48:40 -04:00
2023-07-31 16:44:31 -04:00
if not workshop_id :
QMessageBox . warning ( self , " Warning " , " Please enter a Workshop ID first. " )
return
2023-07-31 21:48:40 -04:00
if not workshop_id . isdigit ( ) :
2023-08-03 04:13:02 -04:00
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
2023-07-31 21:48:40 -04:00
2023-07-31 16:44:31 -04:00
self . label_file_size . setText ( f " File size: { get_workshop_file_size ( workshop_id , raw = True ) } " )
try :
url = f " https://steamcommunity.com/sharedfiles/filedetails/?id= { workshop_id } "
response = requests . get ( url )
response . raise_for_status ( )
content = response . text
soup = BeautifulSoup ( content , " html.parser " )
2023-07-31 21:48:40 -04:00
try :
map_mod_type = soup . find ( " div " , class_ = " rightDetailsBlock " ) . text . strip ( )
map_name = soup . find ( " div " , class_ = " workshopItemTitle " ) . text . strip ( )
map_size = soup . find ( " div " , class_ = " detailsStatRight " ) . text . strip ( )
stars_div = soup . find ( " div " , class_ = " fileRatingDetails " )
stars = stars_div . find ( " img " ) [ " src " ]
except :
QMessageBox . warning ( self , " Warning " , " Please enter a valid Workshop ID. " )
return
2023-07-31 16:44:31 -04:00
try :
preview_image_element = soup . find ( " img " , id = " previewImage " )
workshop_item_image_url = preview_image_element [ " src " ]
except :
preview_image_element = soup . find ( " img " , id = " previewImageMain " )
workshop_item_image_url = preview_image_element [ " src " ]
image_response = requests . get ( workshop_item_image_url )
image_response . raise_for_status ( )
stars_response = requests . get ( stars )
stars_response . raise_for_status ( )
pixmap = QPixmap ( )
pixmap . loadFromData ( image_response . content )
pixmap_stars = QPixmap ( )
pixmap_stars . loadFromData ( stars_response . content )
label = QLabel ( self )
label . setPixmap ( pixmap )
label . setAlignment ( Qt . AlignCenter )
label_stars = QLabel ( self )
label_stars . setPixmap ( pixmap_stars )
label_stars . setAlignment ( Qt . AlignCenter )
msg_box = QMessageBox ( self )
msg_box . setWindowTitle ( " Map/Mod Information " )
2023-08-03 04:13:02 -04:00
msg_box . setWindowIcon ( QIcon ( ' ryuk.ico ' ) )
2023-07-31 16:44:31 -04:00
msg_box . setIconPixmap ( pixmap )
msg_box . setText ( f " Name: { map_name } \n Type: { map_mod_type } \n Size: { map_size } " )
layout = QVBoxLayout ( )
layout . addWidget ( label )
layout . addWidget ( label_stars )
msg_box . setLayout ( layout )
msg_box . setStandardButtons ( QMessageBox . Ok )
msg_box . setDetailedText ( f " Stars: { stars } \n Link: { url } " )
msg_box . exec_ ( )
except requests . exceptions . RequestException as e :
2023-08-03 04:13:02 -04:00
show_message ( " Error " , f " Failed to fetch map information. \n Error: { 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 \n Make 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 \n Please 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 )
2023-07-31 16:44:31 -04:00
2023-07-31 02:02:53 -04:00
if __name__ == " __main__ " :
app = QApplication ( sys . argv )
qdarktheme . setup_theme ( )
if not os . path . exists ( CONFIG_FILE_PATH ) :
create_default_config ( )
window = WorkshopDownloaderApp ( )
window . show ( )
sys . exit ( app . exec_ ( ) )