2023-08-12 12:32:54 -04:00
from CTkMessagebox import CTkMessagebox
from bs4 import BeautifulSoup
import customtkinter as ctk
from pathlib import Path
from CTkToolTip import *
from PIL import Image
2023-07-31 02:02:53 -04:00
import configparser
2023-08-12 12:32:54 -04:00
import webbrowser
import subprocess
import threading
import requests
2023-07-31 16:44:31 -04:00
import zipfile
2023-08-12 12:32:54 -04:00
import shutil
2023-07-31 12:40:41 -04:00
import psutil
2023-08-12 12:32:54 -04:00
import json
import math
2023-07-31 02:02:53 -04:00
import time
2023-08-12 12:32:54 -04:00
import sys
import io
import os
import re
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
VERSION = " v0.2.0 "
2023-08-03 04:13:02 -04:00
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-12 12:32:54 -04:00
global stopped , steampid , console
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
2023-08-12 12:32:54 -04:00
ctk . set_appearance_mode ( " Dark " ) # Modes: "System" (standard), "Dark", "Light"
ctk . set_default_color_theme ( " dark-blue " ) # Themes: "blue" (standard), "green", "dark-blue"
# Start Helper Functions
2023-08-03 04:13:02 -04:00
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-12 12:32:54 -04:00
def initialize_steam ( master ) :
2023-08-03 04:13:02 -04:00
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 )
2023-08-12 12:32:54 -04:00
master . attributes ( ' -alpha ' , 0.0 )
2023-08-03 04:13:02 -04:00
process . wait ( )
2023-08-12 12:32:54 -04:00
show_message ( " SteamCMD has terminated! " , " BOIIIWD is ready for action. " , icon = " info " )
2023-08-03 04:13:02 -04:00
except :
2023-08-12 12:32:54 -04:00
show_message ( " Error! " , " An error occurred please check your paths and try again. " , icon = " cancel " )
master . attributes ( ' -alpha ' , 1.0 )
2023-08-03 04:13:02 -04:00
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 )
2023-08-12 12:32:54 -04:00
def run_steamcmd_command ( command , self ) :
2023-07-31 02:02:53 -04:00
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 ( )
2023-08-12 12:32:54 -04:00
show_message ( " SteamCMD has terminated " , " SteamCMD has been terminated \n Try again if it randomly stopped! " )
global stopped
stopped = True
self . button_download . configure ( state = " normal " )
self . button_stop . configure ( state = " disabled " )
2023-07-31 14:45:46 -04:00
return process . returncode
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
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-12 12:32:54 -04:00
def check_config ( name , fallback = None ) :
2023-08-03 04:13:02 -04:00
config = configparser . ConfigParser ( )
config . read ( CONFIG_FILE_PATH )
2023-08-12 12:32:54 -04:00
if fallback :
return config . get ( " Settings " , name , fallback = fallback )
return config . get ( " Settings " , name , fallback = " on " )
def save_config ( name , value ) :
2023-08-03 04:13:02 -04:00
config = configparser . ConfigParser ( )
config . read ( CONFIG_FILE_PATH )
2023-08-12 12:32:54 -04:00
if name and value :
config . set ( " Settings " , name , value )
with open ( CONFIG_FILE_PATH , " w " ) as config_file :
config . write ( config_file )
2023-08-03 04:13:02 -04:00
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
2023-08-12 12:32:54 -04:00
def show_message ( title , message , icon = " warning " , exit_on_close = False ) :
2023-08-03 04:13:02 -04:00
if exit_on_close :
2023-08-12 12:32:54 -04:00
msg = CTkMessagebox ( title = title , message = message , icon = icon , option_1 = " No " , option_2 = " Ok " )
response = msg . get ( )
if response == " No " :
return False
if response == " Ok " :
return True
else :
return False
2023-08-03 04:13:02 -04:00
else :
2023-08-12 12:32:54 -04:00
msg = CTkMessagebox ( title = title , message = message , icon = icon )
# End helper functions
class UpdateWindow ( ctk . CTkToplevel ) :
def __init__ ( self , master , update_url ) :
global master_win
master_win = master
super ( ) . __init__ ( master )
self . title ( " BOIIIWD Self-Updater " )
self . geometry ( " 400x150 " )
self . after ( 250 , lambda : self . iconbitmap ( ' ryuk.ico ' ) )
self . protocol ( " WM_DELETE_WINDOW " , self . cancel_update )
self . attributes ( ' -topmost ' , ' true ' )
self . columnconfigure ( 0 , weight = 1 )
self . columnconfigure ( 1 , weight = 1 )
self . rowconfigure ( 0 , weight = 1 )
self . rowconfigure ( 1 , weight = 1 )
self . label_download = ctk . CTkLabel ( self , text = " Starting... " )
self . label_download . grid ( row = 0 , column = 0 , padx = 30 , pady = ( 10 , 0 ) , sticky = " w " )
self . label_size = ctk . CTkLabel ( self , text = " Size: 0 " )
self . label_size . grid ( row = 0 , column = 1 , padx = 30 , pady = ( 10 , 0 ) , sticky = " e " )
self . progress_bar = ctk . CTkProgressBar ( self , mode = " determinate " , height = 20 , corner_radius = 7 )
self . progress_bar . grid ( row = 1 , column = 0 , columnspan = 4 , padx = 30 , pady = 10 , sticky = " ew " )
self . progress_bar . set ( 0 )
self . progress_label = ctk . CTkLabel ( self . progress_bar , text = " 0 % " , font = ( " Helvetica " , 12 ) , fg_color = " transparent " , height = 0 , width = 0 , corner_radius = 0 )
self . progress_label . place ( relx = 0.5 , rely = 0.5 , anchor = " center " )
self . cancel_button = ctk . CTkButton ( self , text = " Cancel " , command = self . cancel_update )
self . cancel_button . grid ( row = 2 , column = 0 , padx = 30 , pady = ( 0 , 10 ) , sticky = " w " )
self . update_url = update_url
self . total_size = None
self . up_cancelled = False
def update_progress_bar ( self ) :
2023-08-03 04:13:02 -04:00
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 )
2023-08-12 12:32:54 -04:00
self . progress_bar . set ( 0.0 )
self . total_size = int ( response . headers . get ( ' content-length ' , 0 ) )
self . label_size . configure ( text = f " Size: { convert_bytes_to_readable ( self . total_size ) } " )
2023-08-03 04:13:02 -04:00
zip_path = os . path . join ( update_dir , " latest_version.zip " )
2023-08-12 12:32:54 -04:00
with open ( zip_path , " wb " ) as file :
downloaded_size = 0
for chunk in response . iter_content ( chunk_size = 8192 ) :
if self . up_cancelled :
2023-08-03 04:13:02 -04:00
break
if chunk :
2023-08-12 12:32:54 -04:00
file . write ( chunk )
downloaded_size + = len ( chunk )
progress = int ( ( downloaded_size / self . total_size ) * 100 )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
self . after ( 1 , lambda p = progress : self . label_download . configure ( text = f " Downloading update... " ) )
self . after ( 1 , lambda v = progress / 100.0 : self . progress_bar . set ( v ) )
self . after ( 1 , lambda p = progress : self . progress_label . configure ( text = f " { p } % " ) )
if not self . up_cancelled :
self . progress_bar . set ( 1.0 )
2023-08-03 04:13:02 -04:00
with zipfile . ZipFile ( zip_path , " r " ) as zip_ref :
zip_ref . extractall ( update_dir )
2023-08-12 12:32:54 -04:00
self . label_download . configure ( text = " Update Downloaded successfully! " )
if not show_message ( " Success! " , " Update Downloaded successfully! \n Press ok to install it " , icon = " info " , exit_on_close = True ) :
return
2023-08-03 04:13:02 -04:00
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 } " )
2023-08-12 12:32:54 -04:00
self . label_download . configure ( text = " Update cancelled. " )
self . progress_bar . set ( 0.0 )
# there's a better solution ill implement it later
global master_win
master_win . attributes ( ' -alpha ' , 1.0 )
show_message ( " Cancelled! " , " Update cancelled by user " , icon = " warning " )
2023-08-03 04:13:02 -04:00
except Exception as e :
2023-08-12 12:32:54 -04:00
self . progress_bar . set ( 0.0 )
self . label_download . configure ( text = " Update failed " )
show_message ( " Error " , f " Error installing the update \n { e } " , icon = " cancel " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def start_update ( self ) :
self . thread = threading . Thread ( target = self . update_progress_bar )
self . thread . start ( )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def cancel_update ( self ) :
self . up_cancelled = True
self . withdraw ( )
class SettingsTab ( ctk . CTkFrame ) :
def __init__ ( self , master = None ) :
super ( ) . __init__ ( master )
# Check for updates checkbox
self . check_updates_var = ctk . BooleanVar ( )
self . check_updates_var . trace_add ( " write " , self . enable_save_button )
self . check_updates_checkbox = ctk . CTkSwitch ( self , text = " Check for updates on launch " , variable = self . check_updates_var )
self . check_updates_checkbox . grid ( row = 0 , column = 1 , padx = 20 , pady = ( 20 , 0 ) , sticky = " nsew " )
self . check_updates_var . set ( self . load_settings ( updates = True ) )
# Show console checkbox
self . console_var = ctk . BooleanVar ( )
self . console_var . trace_add ( " write " , self . enable_save_button )
self . checkbox_show_console = ctk . CTkSwitch ( self , text = " Console (On Download) " , variable = self . console_var )
self . checkbox_show_console . grid ( row = 1 , column = 1 , padx = 20 , pady = ( 20 , 0 ) , sticky = " nsew " )
self . checkbox_show_console_tooltip = CTkToolTip ( self . checkbox_show_console , message = " Toggle SteamCMD console \n Please don ' t close the Console If you want to stop press the Stop boutton " )
self . console_var . set ( self . load_settings ( console = True ) )
# Check for updates button
self . check_for_updates = ctk . CTkButton ( self , text = " Check for updates " , command = self . check_for_updates )
self . check_for_updates . grid ( row = 2 , column = 1 , padx = 20 , pady = ( 20 , 0 ) , sticky = " nsew " )
# Save button
self . save_button = ctk . CTkButton ( self , text = " Save " , command = self . save_settings , state = ' disabled ' )
self . save_button . grid ( row = 3 , column = 1 , padx = 20 , pady = ( 150 , 0 ) , sticky = " nsew " )
def enable_save_button ( self , * args ) :
self . save_button . configure ( state = ' normal ' )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def save_settings ( self ) :
self . save_button . configure ( state = ' disabled ' )
global console
if self . check_updates_checkbox . get ( ) :
save_config ( " checkforupdtes " , " on " )
else :
save_config ( " checkforupdtes " , " off " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if self . checkbox_show_console . get ( ) :
save_config ( " console " , " on " )
console = True
else :
save_config ( " console " , " off " )
console = False
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def load_settings ( self , console = None , updates = None ) :
if updates :
if check_config ( " checkforupdtes " ) == " on " :
return 1
else :
return 0
if console :
if check_config ( " console " ) == " on " :
console = True
return 1
else :
console = False
return 0
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def check_for_updates ( self , ignore_up_todate = False ) :
try :
latest_version = get_latest_release_version ( )
current_version = VERSION
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if latest_version and latest_version != current_version :
msg_box = CTkMessagebox ( title = " Update Available " , message = f " An update is available! \n \n Current Version: { current_version } \n Latest Version: { latest_version } " , option_1 = " View " , option_2 = " No " , option_3 = " Yes " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
result = msg_box . get ( )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if result == " View " :
webbrowser . open ( f " https://github.com/ { GITHUB_REPO } /releases/latest " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if result == " Yes " :
update_window = UpdateWindow ( self , LATEST_RELEASE_URL )
update_window . start_update ( )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if result == " No " :
return
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
elif latest_version == current_version :
if ignore_up_todate :
return
msg_box = CTkMessagebox ( title = " Up to Date! " , message = " No Updates Available! " , option_1 = " Ok " )
result = msg_box . get ( )
except Exception as e :
show_message ( " Error " , f " Error while checking for updates: \n { e } " , icon = " cancel " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def load_on_switch_screen ( self ) :
self . check_updates_var . set ( self . load_settings ( updates = True ) )
self . console_var . set ( self . load_settings ( console = True ) )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
# keep last cuz of trace_add()
self . save_button . configure ( state = ' disabled ' )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
class BOIIIWD ( ctk . CTk ) :
def __init__ ( self ) :
2023-07-31 02:02:53 -04:00
super ( ) . __init__ ( )
2023-08-12 12:32:54 -04:00
# self.app_instance = BOIIIWD()
# configure window
self . title ( " BOIII Workshop Downloader - Main " )
self . geometry ( f " { 910 } x { 560 } " )
self . wm_iconbitmap ( ' ryuk.ico ' )
# configure grid layout (4x4)
self . grid_columnconfigure ( 1 , weight = 1 )
self . grid_columnconfigure ( ( 2 , 3 ) , weight = 0 )
self . grid_rowconfigure ( ( 0 , 1 , 2 ) , weight = 1 )
self . settings_tab = SettingsTab ( self )
# create sidebar frame with widgets
self . sidebar_icon = ctk . CTkImage ( light_image = Image . open ( " ryuk.png " ) , dark_image = Image . open ( " ryuk.png " ) , size = ( 40 , 40 ) )
self . sidebar_frame = ctk . CTkFrame ( self , width = 140 , corner_radius = 10 )
self . sidebar_frame . grid ( row = 0 , column = 0 , rowspan = 4 , padx = ( 10 , 10 ) , pady = ( 10 , 10 ) , sticky = " nsew " )
self . sidebar_frame . grid_rowconfigure ( 4 , weight = 1 )
self . logo_label = ctk . CTkLabel ( self . sidebar_frame , text = ' ' , image = self . sidebar_icon )
self . logo_label . grid ( row = 0 , column = 0 , padx = 20 , pady = ( 20 , 10 ) )
self . txt_label = ctk . CTkLabel ( self . sidebar_frame , text = " - Sidebar - " )
self . txt_label . grid ( row = 1 , column = 0 , padx = 20 , pady = ( 20 , 10 ) )
self . sidebar_button_1 = ctk . CTkButton ( self . sidebar_frame )
self . sidebar_button_1 . grid ( row = 2 , column = 0 , padx = 20 , pady = 10 )
self . sidebar_button_2 = ctk . CTkButton ( self . sidebar_frame )
self . sidebar_button_2 . grid ( row = 3 , column = 0 , padx = 20 , pady = 10 )
self . sidebar_button_3 = ctk . CTkButton ( self . sidebar_frame )
self . sidebar_button_3 . grid ( row = 5 , column = 0 , padx = 20 , pady = 10 )
self . appearance_mode_label = ctk . CTkLabel ( self . sidebar_frame , text = " Appearance Mode: " , anchor = " w " )
self . appearance_mode_label . grid ( row = 6 , column = 0 , padx = 20 , pady = ( 10 , 0 ) )
self . appearance_mode_optionemenu = ctk . CTkOptionMenu ( self . sidebar_frame , values = [ " Light " , " Dark " , " System " ] ,
command = self . change_appearance_mode_event )
self . appearance_mode_optionemenu . grid ( row = 7 , column = 0 , padx = 20 , pady = ( 10 , 10 ) )
self . scaling_label = ctk . CTkLabel ( self . sidebar_frame , text = " UI Scaling: " , anchor = " w " )
self . scaling_label . grid ( row = 8 , column = 0 , padx = 20 , pady = ( 10 , 0 ) )
self . scaling_optionemenu = ctk . CTkOptionMenu ( self . sidebar_frame , values = [ " 80 % " , " 90 % " , " 100 % " , " 110 % " , " 120 % " ] ,
command = self . change_scaling_event )
self . scaling_optionemenu . grid ( row = 9 , column = 0 , padx = 20 , pady = ( 10 , 20 ) )
# create optionsframe
self . optionsframe = ctk . CTkFrame ( self )
self . optionsframe . grid ( row = 0 , column = 1 , padx = ( 20 , 20 ) , pady = ( 20 , 0 ) , sticky = " nsew " )
# create slider and progressbar frame
self . slider_progressbar_frame = ctk . CTkFrame ( self )
self . slider_progressbar_frame . grid ( row = 1 , column = 1 , padx = ( 20 , 20 ) , pady = ( 20 , 0 ) , sticky = " nsew " )
self . slider_progressbar_frame . columnconfigure ( 0 , weight = 0 )
self . slider_progressbar_frame . columnconfigure ( 1 , weight = 1 )
self . slider_progressbar_frame . columnconfigure ( 2 , weight = 0 )
self . slider_progressbar_frame . rowconfigure ( 0 , weight = 1 )
self . slider_progressbar_frame . rowconfigure ( 1 , weight = 1 )
self . slider_progressbar_frame . rowconfigure ( 2 , weight = 1 )
self . slider_progressbar_frame . rowconfigure ( 3 , weight = 1 )
self . spacer = ctk . CTkLabel ( master = self . slider_progressbar_frame , text = " " )
self . spacer . grid ( row = 0 , column = 0 , columnspan = 1 )
self . label_speed = ctk . CTkLabel ( master = self . slider_progressbar_frame , text = " Network Speed: 0 KB/s " )
self . label_speed . grid ( row = 1 , column = 0 , padx = 20 , pady = ( 0 , 10 ) , sticky = " w " )
self . label_file_size = ctk . CTkLabel ( master = self . slider_progressbar_frame , text = " File size: 0KB " )
self . label_file_size . grid ( row = 1 , column = 2 , padx = ( 0 , 20 ) , pady = ( 0 , 10 ) , sticky = " e " )
self . progress_bar = ctk . CTkProgressBar ( master = self . slider_progressbar_frame , mode = " determinate " , height = 20 , corner_radius = 7 )
self . progress_bar . grid ( row = 2 , column = 0 , padx = 20 , pady = ( 0 , 10 ) , columnspan = 3 , sticky = " ew " )
self . progress_text = ctk . CTkLabel ( self . progress_bar , text = " 0 % " , font = ( " Helvetica " , 12 ) , fg_color = " transparent " , height = 0 , width = 0 , corner_radius = 0 )
self . progress_text . place ( relx = 0.5 , rely = 0.5 , anchor = " center " )
self . button_download = ctk . CTkButton ( master = self . slider_progressbar_frame , text = " Download " , command = self . download_map )
self . button_download . grid ( row = 4 , column = 0 , padx = 20 , pady = ( 10 , 20 ) , columnspan = 2 , sticky = " ew " )
self . button_stop = ctk . CTkButton ( master = self . slider_progressbar_frame , text = " Stop " , command = self . stop_download )
self . button_stop . grid ( row = 4 , column = 2 , padx = ( 0 , 20 ) , pady = ( 10 , 20 ) , columnspan = 1 , sticky = " ew " )
# options frame
self . optionsframe . columnconfigure ( 1 , weight = 1 )
self . optionsframe . columnconfigure ( 2 , weight = 1 )
self . optionsframe . columnconfigure ( 3 , weight = 1 )
self . label_workshop_id = ctk . CTkLabel ( master = self . optionsframe , text = " Enter the Workshop ID or Link of the map/mod you want to download: " )
self . label_workshop_id . grid ( row = 0 , column = 1 , padx = 20 , pady = ( 20 , 0 ) , columnspan = 3 , sticky = " w " )
self . check_if_changed = ctk . StringVar ( )
self . check_if_changed . trace_add ( " write " , self . id_chnaged_handler )
self . edit_workshop_id = ctk . CTkEntry ( master = self . optionsframe , textvariable = self . check_if_changed )
self . edit_workshop_id . grid ( row = 1 , column = 1 , padx = 20 , pady = ( 0 , 10 ) , columnspan = 4 , sticky = " ew " )
self . button_browse = ctk . CTkButton ( master = self . optionsframe , text = " Browse " , command = self . open_browser )
self . button_browse . grid ( row = 1 , column = 5 , padx = ( 0 , 20 ) , pady = ( 0 , 10 ) , sticky = " ew " )
self . info_button = ctk . CTkButton ( master = self . optionsframe , text = " Info " , command = self . show_map_info )
self . info_button . grid ( row = 2 , column = 5 , padx = ( 0 , 20 ) , pady = ( 0 , 10 ) , sticky = " ew " )
self . label_destination_folder = ctk . CTkLabel ( master = self . optionsframe , text = " Enter Your BOIII folder: " )
self . label_destination_folder . grid ( row = 3 , column = 1 , padx = 20 , pady = ( 0 , 10 ) , columnspan = 4 , sticky = " w " )
self . edit_destination_folder = ctk . CTkEntry ( master = self . optionsframe , placeholder_text = " Your BOIII Instalation folder " )
self . edit_destination_folder . grid ( row = 4 , column = 1 , padx = 20 , pady = ( 0 , 10 ) , columnspan = 4 , sticky = " ew " )
self . button_BOIII_browse = ctk . CTkButton ( master = self . optionsframe , text = " Select " , command = self . open_BOIII_browser )
self . button_BOIII_browse . grid ( row = 4 , column = 5 , padx = ( 0 , 20 ) , pady = ( 0 , 10 ) , sticky = " ew " )
self . label_steamcmd_path = ctk . CTkLabel ( master = self . optionsframe , text = " Enter SteamCMD path: " )
self . label_steamcmd_path . grid ( row = 5 , column = 1 , padx = 20 , pady = ( 20 , 0 ) , columnspan = 3 , sticky = " w " )
self . edit_steamcmd_path = ctk . CTkEntry ( master = self . optionsframe , placeholder_text = " Enter your SteamCMD path " )
self . edit_steamcmd_path . grid ( row = 6 , column = 1 , padx = 20 , pady = ( 0 , 10 ) , columnspan = 4 , sticky = " ew " )
self . button_steamcmd_browse = ctk . CTkButton ( master = self . optionsframe , text = " Select " , command = self . open_steamcmd_path_browser )
self . button_steamcmd_browse . grid ( row = 6 , column = 5 , padx = ( 0 , 20 ) , pady = ( 0 , 10 ) , sticky = " ew " )
# set default values
self . appearance_mode_optionemenu . set ( " Dark " )
self . scaling_optionemenu . set ( " 100 % " )
2023-08-12 13:03:45 -04:00
self . progress_bar . set ( 0.0 )
2023-08-12 12:32:54 -04:00
self . hide_settings_widgets ( )
self . button_stop . configure ( state = " disabled " )
# sidebar windows bouttons
self . sidebar_button_1 . configure ( command = self . main_button_event , text = " Main " , fg_color = ( " #3d3d3d " ) )
self . sidebar_button_2 . configure ( state = " disabled " , text = " Library " )
self . sidebar_button_3 . configure ( command = self . settings_button_event , text = " Settings " )
# load ui configs
self . load_configs ( )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
if check_config ( " checkforupdtes " ) == " on " :
self . withdraw ( )
self . check_for_updates ( ignore_up_todate = True )
self . update ( )
self . deiconify ( )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
try :
global console
if check_config ( " console " ) == " on " :
console = True
else :
console = False
except :
pass
2023-07-31 16:44:31 -04:00
if not check_steamcmd ( ) :
self . show_warning_message ( )
2023-08-12 12:32:54 -04:00
def id_chnaged_handler ( self , some = None , other = None , shit = None ) :
self . after ( 1 , self . label_file_size . configure ( text = f " File size: 0KB " ) )
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 = CTkMessagebox ( title = " Update Available " , message = f " An update is available! \n \n Current Version: { current_version } \n Latest Version: { latest_version } " , option_1 = " View " , option_2 = " No " , option_3 = " Yes " )
result = msg_box . get ( )
if result == " View " :
webbrowser . open ( f " https://github.com/ { GITHUB_REPO } /releases/latest " )
if result == " Yes " :
self . attributes ( ' -alpha ' , 0 )
update_window = UpdateWindow ( self , LATEST_RELEASE_URL )
update_window . start_update ( )
if result == " No " :
return
elif latest_version == current_version :
if ignore_up_todate :
return
msg_box = CTkMessagebox ( title = " Up to Date! " , message = " No Updates Available! " , option_1 = " Ok " )
result = msg_box . get ( )
except Exception as e :
show_message ( " Error " , f " Error while checking for updates: \n { e } " , icon = " cancel " )
def change_appearance_mode_event ( self , new_appearance_mode : str ) :
ctk . set_appearance_mode ( new_appearance_mode )
save_config ( " appearance " , new_appearance_mode )
def change_scaling_event ( self , new_scaling : str ) :
new_scaling_float = int ( new_scaling . replace ( " % " , " " ) ) / 100
ctk . set_widget_scaling ( new_scaling_float )
save_config ( " scaling " , str ( new_scaling_float ) )
def sidebar_button_event ( self ) :
print ( " sidebar_button click " )
def hide_main_widgets ( self ) :
self . optionsframe . grid_remove ( )
self . slider_progressbar_frame . grid_remove ( )
def show_main_widgets ( self ) :
self . title ( " BOIII Workshop Downloader - Main " )
self . optionsframe . grid ( row = 0 , column = 1 , padx = ( 20 , 20 ) , pady = ( 20 , 0 ) , sticky = " nsew " )
self . slider_progressbar_frame . grid ( row = 1 , column = 1 , padx = ( 20 , 20 ) , pady = ( 20 , 0 ) , sticky = " nsew " )
def hide_settings_widgets ( self ) :
self . settings_tab . pack_forget ( )
def show_settings_widgets ( self ) :
self . title ( " BOIII Workshop Downloader - Settings " )
self . settings_tab . grid ( row = 0 , column = 1 , padx = ( 20 , 20 ) , pady = ( 20 , 0 ) , sticky = " nsew " )
self . settings_tab . load_on_switch_screen ( )
def main_button_event ( self ) :
self . sidebar_button_1 . configure ( state = " active " , fg_color = ( " #3d3d3d " ) )
self . sidebar_button_3 . configure ( state = " normal " , fg_color = ( " #1f538d " ) )
self . hide_settings_widgets ( )
self . show_main_widgets ( )
def settings_button_event ( self ) :
self . sidebar_button_1 . configure ( state = " normal " , fg_color = ( " #1f538d " ) )
self . sidebar_button_3 . configure ( state = " active " , fg_color = ( " #3d3d3d " ) )
self . hide_main_widgets ( )
self . show_settings_widgets ( )
def load_configs ( self ) :
if os . path . exists ( CONFIG_FILE_PATH ) :
destination_folder = check_config ( " DestinationFolder " , " " )
steamcmd_path = check_config ( " SteamCMDPath " , os . getcwd ( ) )
new_appearance_mode = check_config ( " appearance " , " Dark " )
new_scaling = check_config ( " scaling " , 1.0 )
self . edit_destination_folder . delete ( 0 , " end " )
self . edit_destination_folder . insert ( 0 , destination_folder )
self . edit_steamcmd_path . delete ( 0 , " end " )
self . edit_steamcmd_path . insert ( 0 , steamcmd_path )
ctk . set_appearance_mode ( new_appearance_mode )
ctk . set_widget_scaling ( float ( new_scaling ) )
self . appearance_mode_optionemenu . set ( new_appearance_mode )
scaling_float = float ( new_scaling ) * 100
scaling_int = math . trunc ( scaling_float )
self . scaling_optionemenu . set ( f " { scaling_int } % " )
else :
new_appearance_mode = check_config ( " appearance " , " Dark " )
new_scaling = check_config ( " scaling " , 1.0 )
ctk . set_appearance_mode ( new_appearance_mode )
ctk . set_widget_scaling ( float ( new_scaling ) )
self . appearance_mode_optionemenu . set ( new_appearance_mode )
scaling_float = float ( new_scaling ) * 100
scaling_int = math . trunc ( scaling_float )
self . scaling_optionemenu . set ( f " { scaling_int } % " )
create_default_config ( )
def open_BOIII_browser ( self ) :
selected_folder = ctk . filedialog . askdirectory ( title = " Select BOIII Folder " )
if selected_folder :
self . edit_destination_folder . delete ( 0 , " end " )
self . edit_destination_folder . insert ( 0 , selected_folder )
save_config ( " DestinationFolder " , self . edit_destination_folder . get ( ) )
save_config ( " SteamCMDPath " , self . edit_steamcmd_path . get ( ) )
def open_steamcmd_path_browser ( self ) :
selected_folder = ctk . filedialog . askdirectory ( title = " Select SteamCMD Folder " )
if selected_folder :
self . edit_steamcmd_path . delete ( 0 , " end " )
self . edit_steamcmd_path . insert ( 0 , selected_folder )
save_config ( " DestinationFolder " , self . edit_destination_folder . get ( ) )
save_config ( " SteamCMDPath " , self . edit_steamcmd_path . get ( ) )
2023-07-31 02:02:53 -04:00
2023-07-31 16:44:31 -04:00
def show_warning_message ( self ) :
2023-08-12 12:32:54 -04:00
msg = CTkMessagebox ( title = " Warning " , message = " steamcmd.exe was not found in the specified directory. \n Press Download to get it or Press Cancel and select it from there!. " ,
icon = " warning " , option_1 = " Cancel " , option_2 = " Download " )
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
response = msg . get ( )
if response == " Cancel " :
return
elif response == " Download " :
self . download_steamcmd ( )
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
def open_browser ( self ) :
link = " https://steamcommunity.com/app/311210/workshop/ "
webbrowser . open ( link )
2023-07-31 16:44:31 -04:00
def download_steamcmd ( self ) :
2023-08-12 12:32:54 -04:00
self . edit_steamcmd_path . delete ( 0 , " end " )
self . edit_steamcmd_path . insert ( 0 , cwd ( ) )
save_config ( " DestinationFolder " , self . edit_destination_folder . get ( ) )
save_config ( " SteamCMDPath " , self . edit_steamcmd_path . get ( ) )
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 } " )
2023-08-12 12:32:54 -04:00
if not show_message ( " Success " , " SteamCMD has been downloaded ,Press ok to initialize it. " , icon = " info " , exit_on_close = True ) :
pass
else :
initialize_steam_thread = threading . Thread ( target = lambda : initialize_steam ( self ) )
initialize_steam_thread . start ( )
2023-07-31 16:44:31 -04:00
else :
2023-08-12 12:32:54 -04:00
show_message ( " Error " , " Failed to find steamcmd.exe after extraction. \n Make you sure to select the correct SteamCMD path (by default current BOIIIWD path) " , icon = " cancel " )
2023-08-03 04:13:02 -04:00
os . remove ( fr " { steamcmd_zip_path } " )
2023-07-31 16:44:31 -04:00
except requests . exceptions . RequestException as e :
2023-08-12 12:32:54 -04:00
show_message ( " Error " , f " Failed to download SteamCMD: { e } " , icon = " cancel " )
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 :
2023-08-12 12:32:54 -04:00
show_message ( " Error " , " Failed to extract SteamCMD. The downloaded file might be corrupted. " , icon = " cancel " )
2023-08-03 04:13:02 -04:00
os . remove ( fr " { steamcmd_zip_path } " )
2023-08-12 12:32:54 -04:00
def show_map_info ( self ) :
def show_map_thread ( ) :
workshop_id = self . edit_workshop_id . get ( ) . strip ( )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if not workshop_id :
show_message ( " Warning " , " Please enter a Workshop ID first. " )
return
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
if not workshop_id . isdigit ( ) :
try :
if extract_workshop_id ( workshop_id ) . strip ( ) . isdigit ( ) :
workshop_id = extract_workshop_id ( workshop_id ) . strip ( )
else :
show_message ( " Warning " , " Please enter a valid Workshop ID. " )
except :
show_message ( " Warning " , " Please enter a valid Workshop ID. " )
2023-08-03 04:13:02 -04:00
return
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
self . after ( 1 , lambda mid = workshop_id : self . label_file_size . configure ( text = f " File size: { get_workshop_file_size ( mid , raw = True ) } " ) )
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
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 " )
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 ( )
except :
show_message ( " Warning " , " Please enter a valid Workshop ID. " )
return
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
try :
preview_image_element = soup . find ( " img " , id = " previewImage " )
workshop_item_image_url = preview_image_element [ " src " ]
except :
try :
preview_image_element = soup . find ( " img " , id = " previewImageMain " )
workshop_item_image_url = preview_image_element [ " src " ]
except Exception as e :
show_message ( " Warning " , f " Failed to get preview image ,probably wrong link/id if not please open an issue on github. \n { e } " )
return
image_response = requests . get ( workshop_item_image_url )
image_response . raise_for_status ( )
image = Image . open ( io . BytesIO ( image_response . content ) )
image = image . resize ( ( 200 , 200 ) , Image . Resampling . LANCZOS )
self . toplevel_info_window ( map_name , map_mod_type , map_size , image )
except requests . exceptions . RequestException as e :
show_message ( " Error " , f " Failed to fetch map information. \n Error: { e } " , icon = " cancel " )
info_thread = threading . Thread ( target = show_map_thread )
info_thread . start ( )
def toplevel_info_window ( self , map_name , map_mod_type , map_size , image ) :
top = ctk . CTkToplevel ( self )
top . after ( 210 , lambda : top . iconbitmap ( " ryuk.ico " ) )
top . geometry ( " 340x430 " )
top . title ( " Map/Mod Information " )
top . attributes ( ' -topmost ' , ' true ' )
label = ctk . CTkLabel ( top , text = " " )
image = ctk . CTkImage ( image , size = ( 260 , 200 ) )
label . configure ( image = image )
label . pack ( )
info = (
f " Name: { map_name } \n "
f " Type: { map_mod_type } \n "
f " Size: { map_size } \n "
)
text = ctk . CTkLabel ( top )
text . configure ( text = info )
text . pack ( )
2023-08-03 04:13:02 -04:00
2023-07-31 02:02:53 -04:00
def download_map ( self ) :
global stopped
stopped = False
2023-08-12 12:32:54 -04:00
save_config ( " DestinationFolder " , self . edit_destination_folder . get ( ) )
save_config ( " SteamCMDPath " , self . edit_steamcmd_path . get ( ) )
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 :
2023-08-12 12:32:54 -04:00
if not show_message ( " Warning " , " SteamCMD is not initialized, Press OK to do so! \n Program may go unresponsive until SteamCMD is finished downloading. " ,
icon = " warning " , exit_on_close = True ) :
pass
else :
initialize_steam_thread = threading . Thread ( target = lambda : initialize_steam ( self ) )
initialize_steam_thread . start ( )
2023-07-31 21:48:40 -04:00
return
2023-08-12 12:32:54 -04:00
workshop_id = self . edit_workshop_id . get ( ) . strip ( )
2023-08-12 13:03:45 -04:00
destination_folder = self . edit_destination_folder . get ( ) . strip ( )
2023-08-12 12:32:54 -04:00
2023-08-03 04:13:02 -04:00
if not workshop_id . isdigit ( ) :
try :
if extract_workshop_id ( workshop_id ) . strip ( ) . isdigit ( ) :
workshop_id = extract_workshop_id ( workshop_id ) . strip ( )
else :
2023-08-12 12:32:54 -04:00
show_message ( " Warning " , " Please enter a valid Workshop ID. " , icon = " warning " )
2023-08-03 04:13:02 -04:00
return
except :
2023-08-12 12:32:54 -04:00
show_message ( " Warning " , " Please enter a valid Workshop ID. " , icon = " warning " )
2023-08-03 04:13:02 -04:00
return
2023-08-12 12:32:54 -04:00
file_size = get_workshop_file_size ( workshop_id )
2023-07-31 21:48:40 -04:00
if not valid_id ( workshop_id ) :
2023-08-12 12:32:54 -04:00
show_message ( " Warning " , " Please enter a valid Workshop ID. " , icon = " warning " )
2023-07-31 21:48:40 -04:00
return
2023-08-12 12:32:54 -04:00
if file_size is None :
show_message ( " Error " , " Failed to retrieve file size. " , icon = " cancel " )
return
2023-07-31 02:02:53 -04:00
2023-08-12 13:03:45 -04:00
if not Path ( destination_folder ) . exists ( ) and not destination_folder :
2023-08-12 12:32:54 -04:00
show_message ( " Error " , " Please select a valid destination folder. " )
2023-07-31 02:02:53 -04:00
return
2023-08-12 13:03:45 -04:00
if not Path ( steamcmd_path ) . exists ( ) and not steamcmd_path . strip ( ) :
2023-08-12 12:32:54 -04:00
show_message ( " Error " , " Please enter a valid SteamCMD path. " )
2023-07-31 02:02:53 -04:00
return
2023-08-12 12:32:54 -04:00
self . after ( 1 , lambda mid = workshop_id : self . label_file_size . configure ( text = f " File size: { get_workshop_file_size ( mid , raw = True ) } " ) )
download_folder = os . path . join ( get_steamcmd_path ( ) , " steamapps " , " workshop " , " downloads " , " 311210 " , workshop_id )
2023-08-12 13:03:45 -04:00
map_folder = os . path . join ( get_steamcmd_path ( ) , " steamapps " , " workshop " , " content " , " 311210 " , workshop_id )
2023-08-12 12:32:54 -04:00
if not os . path . exists ( download_folder ) :
os . makedirs ( download_folder )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
def check_and_update_progress ( ) :
global stopped
previous_net_speed = 0
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
while not stopped :
2023-08-12 13:03:45 -04:00
try :
current_size = sum ( os . path . getsize ( os . path . join ( download_folder , f ) ) for f in os . listdir ( download_folder ) )
except :
current_size = sum ( os . path . getsize ( os . path . join ( map_folder , f ) ) for f in os . listdir ( map_folder ) )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
progress = int ( current_size / file_size * 100 )
self . after ( 1 , lambda v = progress / 100.0 : self . progress_bar . set ( v ) )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
current_net_speed = psutil . net_io_counters ( ) . bytes_recv
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
net_speed_bytes = current_net_speed - previous_net_speed
previous_net_speed = current_net_speed
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
net_speed , speed_unit = convert_speed ( net_speed_bytes )
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
self . after ( 1 , lambda v = net_speed : self . label_speed . configure ( text = f " Network Speed: { v : .2f } { speed_unit } " ) )
self . after ( 1 , lambda p = progress : self . progress_text . configure ( text = f " { p } % " ) )
time . sleep ( 1 )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
command = f " +login anonymous +workshop_download_item 311210 { workshop_id } +quit "
steamcmd_thread = threading . Thread ( target = lambda : run_steamcmd_command ( command , self ) )
steamcmd_thread . start ( )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
def wait_for_threads ( ) :
update_ui_thread = threading . Thread ( target = check_and_update_progress )
update_ui_thread . daemon = True
update_ui_thread . start ( )
update_ui_thread . join ( )
2023-07-31 09:29:45 -04:00
2023-08-12 12:32:54 -04:00
global stopped
stopped = True
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
self . label_speed . configure ( text = " Network Speed: 0 KB/s " )
self . progress_text . configure ( text = " 0 % " )
self . progress_bar . set ( 0.0 )
2023-07-31 02:02:53 -04:00
2023-08-12 12:32:54 -04:00
map_folder = os . path . join ( get_steamcmd_path ( ) , " steamapps " , " workshop " , " content " , " 311210 " , workshop_id )
2023-08-01 11:07:44 -04:00
2023-08-12 12:32:54 -04:00
json_file_path = os . path . join ( map_folder , " workshop.json " )
2023-07-31 21:48:40 -04:00
2023-08-12 12:32:54 -04:00
if os . path . exists ( json_file_path ) :
mod_type , folder_name = extract_json_data ( json_file_path )
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
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 " )
2023-08-03 04:13:02 -04:00
else :
2023-08-12 12:32:54 -04:00
show_message ( " Error " , " Invalid map type in workshop.json. " , icon = " cancel " )
2023-08-03 04:13:02 -04:00
return
2023-07-31 21:48:40 -04:00
2023-08-12 12:32:54 -04:00
os . makedirs ( folder_name_path , exist_ok = True )
2023-07-31 16:44:31 -04:00
2023-08-12 12:32:54 -04:00
try :
shutil . copytree ( map_folder , folder_name_path , dirs_exist_ok = True )
except Exception as E :
show_message ( " Error " , f " Error copying files: { E } " , icon = " cancel " )
2023-07-31 16:44:31 -04:00
2023-08-12 13:03:45 -04:00
msg = CTkMessagebox ( title = " Download Complete " , message = f " { mod_type . capitalize ( ) } files were downloaded \n You can run the game now! " , icon = " info " , option_1 = " Launch " , option_2 = " Ok " )
response = msg . get ( )
if response == " Launch " :
try :
boiii_path = os . path . join ( self . edit_destination_folder . get ( ) . strip ( ) , " boiii.exe " )
subprocess . Popen ( [ boiii_path ] , cwd = self . edit_destination_folder . get ( ) . strip ( ) )
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 } " )
if response == " Ok " :
pass
self . button_download . configure ( state = " normal " )
self . button_stop . configure ( state = " disabled " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
update_wait_thread = threading . Thread ( target = wait_for_threads )
update_wait_thread . start ( )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
self . button_download . configure ( state = " disabled " )
self . button_stop . configure ( state = " normal " )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
def stop_download ( self ) :
global stopped
stopped = True
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
subprocess . run ( [ ' taskkill ' , ' /F ' , ' /IM ' , ' steamcmd.exe ' ] , stdout = subprocess . PIPE , stderr = subprocess . PIPE ,
creationflags = subprocess . CREATE_NO_WINDOW )
2023-08-03 04:13:02 -04:00
2023-08-12 12:32:54 -04:00
self . button_download . configure ( state = " normal " )
self . button_stop . configure ( state = " disabled " )
self . label_speed . configure ( text = " Network Speed: 0 KB/s " )
self . progress_text . configure ( text = " 0 % " )
self . progress_bar . set ( 0.0 )
2023-08-03 04:13:02 -04:00
2023-07-31 16:44:31 -04:00
2023-07-31 02:02:53 -04:00
if __name__ == " __main__ " :
2023-08-12 12:32:54 -04:00
app = BOIIIWD ( )
app . mainloop ( )