v0.2.3, added queue tab (beta)

This commit is contained in:
faroukbmiled 2023-08-27 20:37:29 +01:00
parent 359a0dfacf
commit 8c3471be9e

View File

@ -20,7 +20,7 @@ import io
import os import os
import re import re
VERSION = "v0.2.2" VERSION = "v0.2.3"
GITHUB_REPO = "faroukbmiled/BOIIIWD" GITHUB_REPO = "faroukbmiled/BOIIIWD"
LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip" LATEST_RELEASE_URL = "https://github.com/faroukbmiled/BOIIIWD/releases/latest/download/Release.zip"
UPDATER_FOLDER = "update" UPDATER_FOLDER = "update"
@ -29,17 +29,18 @@ RESOURCES_DIR = os.path.join(os.path.dirname(__file__), 'resources')
# fuck it we ball, ill get rid of globals when i finish everything cant be bothered rn # fuck it we ball, ill get rid of globals when i finish everything cant be bothered rn
global stopped, steampid, console, clean_on_finish, continuous, estimated_progress, steam_fail_counter, \ global stopped, steampid, console, clean_on_finish, continuous, estimated_progress, steam_fail_counter, \
steam_fail_counter_toggle, steam_fail_number, steamcmd_reset steam_fail_counter_toggle, steam_fail_number, steamcmd_reset, show_fails
steampid = None steampid = None
stopped = False stopped = False
console = False console = False
clean_on_finish = True clean_on_finish = True
continuous = True continuous = True
estimated_progress = True estimated_progress = True
steam_fail_counter_toggle = True steam_fail_counter_toggle = False
steam_fail_counter = 0 steam_fail_counter = 0
steam_fail_number = 10 steam_fail_number = 10
steamcmd_reset = False steamcmd_reset = False
show_fails = True
# Start Helper Functions # Start Helper Functions
def cwd(): def cwd():
@ -224,6 +225,19 @@ def run_steamcmd_command(command, self, map_folder):
if console: if console:
show_console = subprocess.CREATE_NEW_CONSOLE show_console = subprocess.CREATE_NEW_CONSOLE
if os.path.exists(map_folder):
try:
try:
os.remove(map_folder)
except:
timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
os.rename(map_folder, os.path.join(map_folder, os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", f"couldntremove_{timestamp}")))
except Exception as e:
stopped = True
self.queue_stop_button = True
show_message("Error", f"Couldn't remove {map_folder}, please do so manually\n{e}", icon="cancel")
return
if continuous: if continuous:
while not os.path.exists(map_folder) and not stopped: while not os.path.exists(map_folder) and not stopped:
process = subprocess.Popen( process = subprocess.Popen(
@ -265,7 +279,6 @@ def run_steamcmd_command(command, self, map_folder):
creationflags=show_console creationflags=show_console
) )
steampid = process.pid steampid = process.pid
if process.poll() is not None: if process.poll() is not None:
@ -405,6 +418,23 @@ def reset_steamcmd(no_warn=None):
if not no_warn: if not no_warn:
show_message("Warning!", "steamapps folder was not found, maybe already removed?", icon="warning") show_message("Warning!", "steamapps folder was not found, maybe already removed?", icon="warning")
def get_item_name(id):
try:
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={id}"
response = requests.get(url)
response.raise_for_status()
content = response.text
soup = BeautifulSoup(content, "html.parser")
try:
map_name = soup.find("div", class_="workshopItemTitle").text.strip()
return map_name
except:
return True
except:
return False
# End helper functions # End helper functions
class UpdateWindow(ctk.CTkToplevel): class UpdateWindow(ctk.CTkToplevel):
@ -850,15 +880,23 @@ class SettingsTab(ctk.CTkFrame):
\nThis is not accurate ,it's better than with it off which is calculating the downloaded folder size which steamcmd dumps the full size rigth mostly") \nThis is not accurate ,it's better than with it off which is calculating the downloaded folder size which steamcmd dumps the full size rigth mostly")
self.estimated_progress_var.set(self.load_settings("estimated_progress", "on")) self.estimated_progress_var.set(self.load_settings("estimated_progress", "on"))
# Show estimated_progress checkbox
self.show_fails_var = ctk.BooleanVar()
self.show_fails_var.trace_add("write", self.enable_save_button)
self.show_fails = ctk.CTkSwitch(left_frame, text="Show fails (on top of progress bar):", variable=self.show_fails_var)
self.show_fails.grid(row=5, column=1, padx=20, pady=(20, 0), sticky="nw")
self.show_fails_tooltip = CTkToolTip(self.show_fails, message="Display how many times steamcmd has failed/crashed\nIf the number is getting high quickly then try pressing Reset SteamCMD and try again, otherwise its fine")
self.estimated_progress_var.set(self.load_settings("show_fails", "on"))
# Resetr steam on many fails # Resetr steam on many fails
self.reset_steamcmd_on_fail_var = ctk.IntVar() self.reset_steamcmd_on_fail_var = ctk.IntVar()
self.reset_steamcmd_on_fail_var.trace_add("write", self.enable_save_button) self.reset_steamcmd_on_fail_var.trace_add("write", self.enable_save_button)
self.reset_steamcmd_on_fail_text = ctk.CTkLabel(left_frame, text=f"Reset steamcmd on % fails: (n of fails)", anchor="w") self.reset_steamcmd_on_fail_text = ctk.CTkLabel(left_frame, text=f"Reset steamcmd on % fails: (n of fails)", anchor="w")
self.reset_steamcmd_on_fail_text.grid(row=5, column=1, padx=20, pady=(10, 0), sticky="nw") self.reset_steamcmd_on_fail_text.grid(row=6, column=1, padx=20, pady=(10, 0), sticky="nw")
self.reset_steamcmd_on_fail = ctk.CTkOptionMenu(left_frame, values=["10", "15", "20", "Disable"], variable=self.reset_steamcmd_on_fail_var) self.reset_steamcmd_on_fail = ctk.CTkOptionMenu(left_frame, values=["20", "30", "40", "Custom", "Disable"], variable=self.reset_steamcmd_on_fail_var, command=self.reset_steamcmd_on_fail_func)
self.reset_steamcmd_on_fail.grid(row=6, column=1, padx=20, pady=(0, 0), sticky="nw") self.reset_steamcmd_on_fail.grid(row=7, column=1, padx=20, pady=(0, 0), sticky="nw")
self.reset_steamcmd_on_fail_tooltip = CTkToolTip(self.reset_steamcmd_on_fail, message="This actually fixes steamcmd when its crashing way too much") self.reset_steamcmd_on_fail_tooltip = CTkToolTip(self.reset_steamcmd_on_fail, message="This actually fixes steamcmd when its crashing way too much")
self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10")) self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "Disable"))
# Check for updates button n Launch boiii # Check for updates button n Launch boiii
self.check_for_updates = ctk.CTkButton(right_frame, text="Check for updates", command=self.settings_check_for_updates) self.check_for_updates = ctk.CTkButton(right_frame, text="Check for updates", command=self.settings_check_for_updates)
@ -901,7 +939,16 @@ class SettingsTab(ctk.CTkFrame):
self.version_info = ctk.CTkLabel(self, text=f"{VERSION}") self.version_info = ctk.CTkLabel(self, text=f"{VERSION}")
self.version_info.grid(row=3, column=1, padx=20, pady=(20, 20), sticky="e") self.version_info.grid(row=3, column=1, padx=20, pady=(20, 20), sticky="e")
def reset_steamcmd_on_fail_func(self, option: str):
if option == "Custom":
try:
save_config("reset_on_fail", self.reset_steamcmd_on_fail.get())
if show_message("config.ini" ,"change reset_on_fail value to whatever you want", exit_on_close=True):
os.system(f"notepad {os.path.join(cwd(), 'config.ini')}")
except:
show_message("Couldn't open config.ini" ,"you can do so by yourself and change reset_on_fail value to whatever you want")
else:
pass
def theme_options_func(self, option: str): def theme_options_func(self, option: str):
if option == "Default": if option == "Default":
self.boiiiwd_custom_theme(disable_only=True) self.boiiiwd_custom_theme(disable_only=True)
@ -927,7 +974,7 @@ class SettingsTab(ctk.CTkFrame):
def save_settings(self): def save_settings(self):
self.save_button.configure(state='disabled') self.save_button.configure(state='disabled')
global console, clean_on_finish, continuous, estimated_progress, steam_fail_number, steam_fail_counter_toggle global console, clean_on_finish, continuous, estimated_progress, steam_fail_number, steam_fail_counter_toggle, show_fails
if self.check_updates_checkbox.get(): if self.check_updates_checkbox.get():
save_config("checkforupdtes", "on") save_config("checkforupdtes", "on")
else: else:
@ -961,6 +1008,13 @@ class SettingsTab(ctk.CTkFrame):
save_config("estimated_progress", "off") save_config("estimated_progress", "off")
estimated_progress = False estimated_progress = False
if self.show_fails.get():
save_config("show_fails", "on")
show_fails = True
else:
save_config("show_fails", "off")
show_fails = False
if self.reset_steamcmd_on_fail.get(): if self.reset_steamcmd_on_fail.get():
value = self.reset_steamcmd_on_fail.get() value = self.reset_steamcmd_on_fail.get()
if value == "Disable": if value == "Disable":
@ -971,7 +1025,7 @@ class SettingsTab(ctk.CTkFrame):
save_config("reset_on_fail", value) save_config("reset_on_fail", value)
def load_settings(self, setting, fallback=None): def load_settings(self, setting, fallback=None):
global console, clean_on_finish, continuous, estimated_progress, steam_fail_counter_toggle, steam_fail_number global console, clean_on_finish, continuous, estimated_progress, steam_fail_counter_toggle, steam_fail_number, show_fails
if setting == "console": if setting == "console":
if check_config(setting, fallback) == "on": if check_config(setting, fallback) == "on":
console = True console = True
@ -1005,7 +1059,7 @@ class SettingsTab(ctk.CTkFrame):
if setting == "reset_on_fail": if setting == "reset_on_fail":
option = check_config(setting, fallback) option = check_config(setting, fallback)
if option == "Disable": if option == "Disable" or option == "Custom":
steam_fail_counter_toggle = False steam_fail_counter_toggle = False
return "Disable" return "Disable"
else: else:
@ -1013,8 +1067,20 @@ class SettingsTab(ctk.CTkFrame):
steam_fail_number = int(option) steam_fail_number = int(option)
return option return option
except: except:
steam_fail_number = 10 if steam_fail_counter_toggle:
return "10" steam_fail_number = 10
return "10"
else:
steam_fail_number = 10
return "Disable"
if setting == "show_fails":
if check_config(setting, fallback) == "on":
show_fails = True
return 1
else:
show_fails = False
return 0
if setting == "theme": if setting == "theme":
if os.path.exists(os.path.join(cwd(), "boiiiwd_theme.json")): if os.path.exists(os.path.join(cwd(), "boiiiwd_theme.json")):
@ -1055,10 +1121,11 @@ class SettingsTab(ctk.CTkFrame):
def load_on_switch_screen(self): def load_on_switch_screen(self):
self.check_updates_var.set(self.load_settings("checkforupdtes")) self.check_updates_var.set(self.load_settings("checkforupdtes"))
self.console_var.set(self.load_settings("console")) self.console_var.set(self.load_settings("console"))
self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "10")) self.reset_steamcmd_on_fail.set(value=self.load_settings("reset_on_fail", "Disable"))
self.estimated_progress_var.set(self.load_settings("estimated_progress", "on")) self.estimated_progress_var.set(self.load_settings("estimated_progress", "on"))
self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on")) self.clean_checkbox_var.set(self.load_settings("clean_on_finish", "on"))
self.continuous_var.set(self.load_settings("continuous_download")) self.continuous_var.set(self.load_settings("continuous_download"))
self.show_fails_var.set(self.load_settings("show_fails", "on"))
# keep last cuz of trace_add() # keep last cuz of trace_add()
self.save_button.configure(state='disabled') self.save_button.configure(state='disabled')
@ -1090,6 +1157,31 @@ class BOIIIWD(ctk.CTk):
self.wm_iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico")) self.wm_iconbitmap(os.path.join(RESOURCES_DIR, "ryuk.ico"))
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
# Qeue frame/tab, keep here or app will start shrinked eveytime
self.qeueuframe = ctk.CTkFrame(self)
self.qeueuframe.columnconfigure(1, weight=1)
self.qeueuframe.columnconfigure(2, weight=1)
self.qeueuframe.columnconfigure(3, weight=1)
self.qeueuframe.rowconfigure(1, weight=1)
self.qeueuframe.rowconfigure(2, weight=1)
self.qeueuframe.rowconfigure(3, weight=1)
self.qeueuframe.rowconfigure(4, weight=1)
self.workshop_queue_label = ctk.CTkLabel(self.qeueuframe, text="Workshop IDs/Links -> press help to see examples:")
self.workshop_queue_label.grid(row=0, column=0, padx=(20, 20), pady=(20, 20), sticky="wns")
self.help_button = ctk.CTkButton(master=self.qeueuframe, text="help", command=self.help_queue_text_func, width=10, height=10, fg_color="#585858")
self.help_button.grid(row=0, column=1, padx=(0, 20), pady=(20, 20), sticky="wns")
self.help_button_tooltip = CTkToolTip(self.help_button, message="This only works if the text area is empty (press twice if you had something in it)")
self.queuetextarea = ctk.CTkTextbox(master=self.qeueuframe, font=("", 15))
self.queuetextarea.grid(row=1, column=0, columnspan=2,rowspan=2, padx=(20, 20), pady=(0, 20), sticky="nwse")
self.status_text = ctk.CTkLabel(self.qeueuframe, text="Status: Not Downloading")
self.status_text.grid(row=3, column=0, padx=(20, 20), pady=(0, 20), sticky="ws")
self.qeueuframe.grid_remove()
# configure grid layout (4x4) # configure grid layout (4x4)
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure((2, 3), weight=0) self.grid_columnconfigure((2, 3), weight=0)
@ -1110,10 +1202,10 @@ class BOIIIWD(ctk.CTk):
self.txt_label.grid(row=1, column=0, padx=20, pady=(20, 10)) self.txt_label.grid(row=1, column=0, padx=20, pady=(20, 10))
self.sidebar_main = ctk.CTkButton(self.sidebar_frame) self.sidebar_main = ctk.CTkButton(self.sidebar_frame)
self.sidebar_main.grid(row=2, column=0, padx=20, pady=10) self.sidebar_main.grid(row=2, column=0, padx=20, pady=10)
self.sidebar_library = ctk.CTkButton(self.sidebar_frame)
self.sidebar_library.grid(row=3, column=0, padx=20, pady=10)
self.sidebar_queue = ctk.CTkButton(self.sidebar_frame) self.sidebar_queue = ctk.CTkButton(self.sidebar_frame)
self.sidebar_queue.grid(row=4, column=0, padx=20, pady=10, sticky="n") self.sidebar_queue.grid(row=3, column=0, padx=20, pady=10)
self.sidebar_library = ctk.CTkButton(self.sidebar_frame)
self.sidebar_library.grid(row=4, column=0, padx=20, pady=10, sticky="n")
self.sidebar_settings = ctk.CTkButton(self.sidebar_frame) self.sidebar_settings = ctk.CTkButton(self.sidebar_frame)
self.sidebar_settings.grid(row=5, column=0, padx=20, pady=10, sticky="n") self.sidebar_settings.grid(row=5, column=0, padx=20, pady=10, sticky="n")
@ -1157,7 +1249,7 @@ class BOIIIWD(ctk.CTk):
self.button_download.grid(row=4, column=0, padx=20, pady=(5, 20), columnspan=2, sticky="ew") self.button_download.grid(row=4, column=0, padx=20, pady=(5, 20), columnspan=2, sticky="ew")
self.button_stop = ctk.CTkButton(master=self.slider_progressbar_frame, text="Stop", command=self.stop_download) 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=(5, 20), columnspan=1, sticky="ew") self.button_stop.grid(row=4, column=2, padx=(0, 20), pady=(5, 20), columnspan=1, sticky="w")
# options frame # options frame
self.optionsframe.columnconfigure(1, weight=1) self.optionsframe.columnconfigure(1, weight=1)
@ -1201,6 +1293,7 @@ class BOIIIWD(ctk.CTk):
self.button_steamcmd_browse = ctk.CTkButton(master=self.optionsframe, text="Select", command=self.open_steamcmd_path_browser) 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, 30), sticky="ewn") self.button_steamcmd_browse.grid(row=6, column=5, padx=(0, 20), pady=(0, 30), sticky="ewn")
# set default values # set default values
self.active_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "button_active_state_color") self.active_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "button_active_state_color")
self.normal_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "button_normal_state_color") self.normal_color = get_button_state_colors(check_custom_theme(check_config("theme", fallback="boiiiwd_theme.json")), "button_normal_state_color")
@ -1211,17 +1304,19 @@ class BOIIIWD(ctk.CTk):
self.progress_bar.configure(progress_color=self.progress_color) self.progress_bar.configure(progress_color=self.progress_color)
self.hide_settings_widgets() self.hide_settings_widgets()
self.button_stop.configure(state="disabled") self.button_stop.configure(state="disabled")
self.is_downloading = False self.is_pressed = False
self.queue_enabled = False
self.queue_stop_button = False
# sidebar windows bouttons # sidebar windows bouttons
self.sidebar_main.configure(command=self.main_button_event, text="Main ⬇️", fg_color=(self.active_color), state="active") self.sidebar_main.configure(command=self.main_button_event, text="Main ⬇️", fg_color=(self.active_color), state="active")
self.sidebar_library.configure(text="Library 📙", command=self.library_button_event) self.sidebar_library.configure(text="Library 📙", command=self.library_button_event)
self.sidebar_queue.configure(state="disabled", text="Queue 🚧") self.sidebar_queue.configure(text="Queue 🚧", command=self.queue_button_event)
sidebar_settings_button_image = os.path.join(RESOURCES_DIR, "sett10.png") sidebar_settings_button_image = os.path.join(RESOURCES_DIR, "sett10.png")
self.sidebar_settings.configure(command=self.settings_button_event, text="", image=ctk.CTkImage(Image.open(sidebar_settings_button_image), size=(int(35), int(35))), fg_color="transparent", width=45, height=45) self.sidebar_settings.configure(command=self.settings_button_event, text="", image=ctk.CTkImage(Image.open(sidebar_settings_button_image), size=(int(35), int(35))), fg_color="transparent", width=45, height=45)
self.sidebar_settings_tooltip = CTkToolTip(self.sidebar_settings, message="Settings") self.sidebar_settings_tooltip = CTkToolTip(self.sidebar_settings, message="Settings")
self.sidebar_library_tooltip = CTkToolTip(self.sidebar_library, message="Experimental") self.sidebar_library_tooltip = CTkToolTip(self.sidebar_library, message="Experimental")
self.sidebar_queue_tooltip = CTkToolTip(self.sidebar_queue, message="Coming soon") self.sidebar_queue_tooltip = CTkToolTip(self.sidebar_queue, message="Experimental")
self.bind("<Configure>", self.save_window_size) self.bind("<Configure>", self.save_window_size)
# load ui configs # load ui configs
@ -1238,7 +1333,8 @@ class BOIIIWD(ctk.CTk):
self.settings_tab.load_settings("continuous_download", "on") self.settings_tab.load_settings("continuous_download", "on")
self.settings_tab.load_settings("console", "off") self.settings_tab.load_settings("console", "off")
self.settings_tab.load_settings("estimated_progress", "on") self.settings_tab.load_settings("estimated_progress", "on")
self.settings_tab.load_settings("reset_on_fail", "10") self.settings_tab.load_settings("reset_on_fail", "Disable")
self.settings_tab.load_settings("show_fails", "on")
except: except:
pass pass
@ -1295,30 +1391,56 @@ class BOIIIWD(ctk.CTk):
self.library_tab.load_items(self.edit_destination_folder.get()) self.library_tab.load_items(self.edit_destination_folder.get())
self.library_tab.grid(row=0, rowspan=3, column=1, padx=(0, 20), pady=(20, 20), sticky="nsew") self.library_tab.grid(row=0, rowspan=3, column=1, padx=(0, 20), pady=(20, 20), sticky="nsew")
def show_queue_widgets(self):
self.title("BOIII Workshop Downloader - Queue")
self.optionsframe.grid_forget()
self.queue_enabled = True
self.slider_progressbar_frame.grid(row=2, column=1, rowspan=1, padx=(0, 20), pady=(20, 20), sticky="nsew")
self.qeueuframe.grid(row=0, column=1, rowspan=2, padx=(0, 20), pady=(20, 0), sticky="nsew")
def hide_queue_widgets(self):
self.queue_enabled = False
self.qeueuframe.grid_forget()
def main_button_event(self): def main_button_event(self):
self.sidebar_main.configure(state="active", fg_color=(self.active_color)) self.sidebar_main.configure(state="active", fg_color=(self.active_color))
self.sidebar_settings.configure(state="normal", fg_color="transparent") self.sidebar_settings.configure(state="normal", fg_color="transparent")
self.sidebar_library.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_library.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color))
self.hide_settings_widgets() self.hide_settings_widgets()
self.hide_library_widgets() self.hide_library_widgets()
self.hide_queue_widgets()
self.show_main_widgets() self.show_main_widgets()
def settings_button_event(self): def settings_button_event(self):
self.sidebar_main.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_main.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_library.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_library.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_settings.configure(state="active", fg_color=(self.active_color)) self.sidebar_settings.configure(state="active", fg_color=(self.active_color))
self.hide_main_widgets() self.hide_main_widgets()
self.hide_library_widgets() self.hide_library_widgets()
self.hide_queue_widgets()
self.show_settings_widgets() self.show_settings_widgets()
def library_button_event(self): def library_button_event(self):
self.sidebar_main.configure(state="normal", fg_color=(self.normal_color)) self.sidebar_main.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_settings.configure(state="normal", fg_color="transparent") self.sidebar_settings.configure(state="normal", fg_color="transparent")
self.sidebar_queue.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_library.configure(state="active", fg_color=(self.active_color)) self.sidebar_library.configure(state="active", fg_color=(self.active_color))
self.hide_main_widgets() self.hide_main_widgets()
self.hide_settings_widgets() self.hide_settings_widgets()
self.hide_queue_widgets()
self.show_library_widgets() self.show_library_widgets()
def queue_button_event(self):
self.sidebar_main.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_settings.configure(state="normal", fg_color="transparent")
self.sidebar_library.configure(state="normal", fg_color=(self.normal_color))
self.sidebar_queue.configure(state="active", fg_color=(self.active_color))
self.hide_settings_widgets()
self.hide_library_widgets()
self.show_queue_widgets()
def load_configs(self): def load_configs(self):
if os.path.exists(CONFIG_FILE_PATH): if os.path.exists(CONFIG_FILE_PATH):
destination_folder = check_config("DestinationFolder", "") destination_folder = check_config("DestinationFolder", "")
@ -1348,6 +1470,17 @@ class BOIIIWD(ctk.CTk):
self.edit_steamcmd_path.insert(0, cwd()) self.edit_steamcmd_path.insert(0, cwd())
create_default_config() create_default_config()
def help_queue_text_func(self):
if any(char.isalpha() for char in self.queuetextarea.get("1.0", "end")):
self.workshop_queue_label.configure(text="Workshop IDs/Links => press help to see examples:")
self.queuetextarea.configure(state="normal")
self.queuetextarea.delete(1.0, "end")
self.queuetextarea.insert(1.0, "")
else:
self.workshop_queue_label.configure(text="Workshop IDs/Links => press help again to remove examples:")
self.queuetextarea.insert(1.0, "3010399939,2976006537,2118338989,...\nor:\n3010399939\n2976006537\n2113146805\n...")
self.queuetextarea.configure(state="disabled")
def open_BOIII_browser(self): def open_BOIII_browser(self):
selected_folder = ctk.filedialog.askdirectory(title="Select BOIII Folder") selected_folder = ctk.filedialog.askdirectory(title="Select BOIII Folder")
if selected_folder: if selected_folder:
@ -1568,13 +1701,286 @@ class BOIIIWD(ctk.CTk):
top.grid_columnconfigure(1, weight=1) top.grid_columnconfigure(1, weight=1)
def download_map(self): def download_map(self):
if not self.is_downloading: if not self.is_pressed:
self.is_downloading = True self.is_pressed = True
start_down_thread = threading.Thread(target=self.download_thread) if self.queue_enabled:
start_down_thread.start() start_down_thread = threading.Thread(target=self.queue_download_thread)
start_down_thread.start()
else:
start_down_thread = threading.Thread(target=self.download_thread)
start_down_thread.start()
else: else:
show_message("Warning", "Already pressed, Please wait.") show_message("Warning", "Already pressed, Please wait.")
def queue_download_thread(self):
global stopped
stopped = False
self.queue_stop_button = False
try:
save_config("DestinationFolder" ,self.edit_destination_folder.get())
save_config("SteamCMDPath" ,self.edit_steamcmd_path.get())
if not check_steamcmd():
self.show_warning_message()
return
steamcmd_path = get_steamcmd_path()
if not is_steamcmd_initialized():
if not show_message("Warning", "SteamCMD is not initialized, Press OK to do so!\nProgram 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()
return
text = self.queuetextarea.get("1.0", "end")
items = []
if "," in text:
items = [n.strip() for n in text.split(",")]
else:
items = [n.strip() for n in text.split("\n") if n.strip()]
if not items:
show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
self.stop_download
return
destination_folder = self.edit_destination_folder.get().strip()
if not destination_folder or not os.path.exists(destination_folder):
show_message("Error", "Please select a valid destination folder => in the main tab!.")
self.stop_download
return
if not steamcmd_path or not os.path.exists(steamcmd_path):
show_message("Error", "Please enter a valid SteamCMD path => in the main tab!.")
self.stop_download
return
self.total_queue_size = 0
for item in items:
item.strip()
workshop_id = item
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 valid Workshop IDs/Links.", icon="warning")
self.stop_download
return
except:
show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
self.stop_download
return
if not valid_id(workshop_id):
show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
self.stop_download
return
ws_file_size = get_workshop_file_size(workshop_id)
file_size = ws_file_size
self.total_queue_size += ws_file_size
if file_size is None:
show_message("Error", "Failed to retrieve file size.", icon="cancel")
self.stop_download
return
self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)}"))
start_time = time.time()
for index, item in enumerate(items):
if self.queue_stop_button:
self.stop_download
break
item.strip()
stopped = False
workshop_id = item
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 valid Workshop IDs/Links.", icon="warning")
self.stop_download
return
except:
show_message("Warning", "Please enter valid Workshop IDs/Links.", icon="warning")
self.stop_download
return
ws_file_size = get_workshop_file_size(workshop_id)
file_size = ws_file_size
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)
map_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", workshop_id)
if not os.path.exists(download_folder):
os.makedirs(download_folder)
def check_and_update_progress():
# delay untill steam boots up and starts downloading (ive a better idea ill implement it later)
time.sleep(8)
global stopped, steamcmd_reset
previous_net_speed = 0
est_downloaded_bytes = 0
file_size = ws_file_size
item_name = get_item_name(workshop_id) if get_item_name(workshop_id) else "Error getting name"
while not stopped:
if steamcmd_reset:
steamcmd_reset = False
previous_net_speed = 0
est_downloaded_bytes = 0
try:
current_size = sum(os.path.getsize(os.path.join(download_folder, f)) for f in os.listdir(download_folder))
except:
try:
current_size = sum(os.path.getsize(os.path.join(map_folder, f)) for f in os.listdir(map_folder))
except:
continue
progress = int(current_size / file_size * 100)
if progress > 100:
progress = int(current_size / current_size * 100)
self.total_queue_size -= file_size
file_size = current_size
self.total_queue_size += file_size
self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)} | Current Item: {workshop_id} | Name: {item_name}"))
self.after(1, lambda p=progress: self.label_file_size.configure(text=f"Wrong size reported\nFile size: ~{convert_bytes_to_readable(current_size)}"))
if estimated_progress:
time_elapsed = time.time() - start_time
raw_net_speed = psutil.net_io_counters().bytes_recv
current_net_speed_text = raw_net_speed
net_speed_bytes = current_net_speed_text - previous_net_speed
previous_net_speed = current_net_speed_text
current_net_speed = net_speed_bytes
down_cap = 150000000
if current_net_speed >= down_cap:
current_net_speed = 10
est_downloaded_bytes += current_net_speed
percentage_complete = (est_downloaded_bytes / file_size) * 100
progress = min(percentage_complete / 100, 0.99)
net_speed, speed_unit = convert_speed(net_speed_bytes)
elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
# print(f"raw_net {raw_net_speed}\ncurrent_net_speed: {current_net_speed}\nest_downloaded_bytes {est_downloaded_bytes}\npercentage_complete {percentage_complete}\nprogress {progress}")
self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)} | Current Item: {workshop_id} | Name: {item_name}"))
self.after(1, self.progress_bar.set(progress))
self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}"))
self.after(1, lambda p=min(percentage_complete ,99): self.progress_text.configure(text=f"{p:.2f}%"))
if show_fails:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d} - Fails: {steam_fail_counter}"))
else:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}"))
time.sleep(1)
else:
time_elapsed = time.time() - start_time
progress = int(current_size / file_size * 100)
self.after(1, lambda v=progress / 100.0: self.progress_bar.set(v))
current_net_speed = psutil.net_io_counters().bytes_recv
net_speed_bytes = current_net_speed - previous_net_speed
previous_net_speed = current_net_speed
net_speed, speed_unit = convert_speed(net_speed_bytes)
elapsed_hours, elapsed_minutes, elapsed_seconds = convert_seconds(time_elapsed)
self.after(1, self.status_text.configure(text=f"Status: Total size: ~{convert_bytes_to_readable(self.total_queue_size)} | Current Item: {workshop_id} | Name: {item_name}"))
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}%"))
if show_fails:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d} - Fails: {steam_fail_counter}"))
else:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}"))
time.sleep(1)
command = f"+login anonymous +@sSteamCmdForcePlatformBitness 64 +app_info_update 1 +app_info_print 311210 app_update 311210 +workshop_download_item 311210 {workshop_id} validate +quit"
steamcmd_thread = threading.Thread(target=lambda: run_steamcmd_command(command, self, map_folder))
steamcmd_thread.start()
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()
self.label_speed.configure(text="Network Speed: 0 KB/s")
self.progress_text.configure(text="0%")
self.progress_bar.set(0.0)
map_folder = os.path.join(get_steamcmd_path(), "steamapps", "workshop", "content", "311210", workshop_id)
json_file_path = os.path.join(map_folder, "workshop.json")
if os.path.exists(json_file_path):
mod_type = extract_json_data(json_file_path, "Type")
folder_name = extract_json_data(json_file_path, "FolderName")
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 workshop type in workshop.json, are you sure this is a map or a mod?.", icon="cancel")
self.stop_download
return
os.makedirs(folder_name_path, exist_ok=True)
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")
if clean_on_finish:
remove_tree(map_folder)
remove_tree(download_folder)
if index == len(items) - 1:
msg = CTkMessagebox(title="Downloads Complete", message=f"All files were downloaded\nYou can run the game now!", icon="info", option_1="Launch", option_2="Ok")
response = msg.get()
if response=="Launch":
launch_boiii_func(self.edit_destination_folder.get().strip())
if response=="Ok":
pass
self.button_download.configure(state="disabled")
self.button_stop.configure(state="normal")
update_wait_thread = threading.Thread(target=wait_for_threads)
update_wait_thread.start()
steamcmd_thread.join()
update_wait_thread.join()
if index == len(items) - 1:
self.button_download.configure(state="normal")
self.button_stop.configure(state="disabled")
self.after(1, self.status_text.configure(text=f"Status: Done"))
self.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
stopped = True
self.stop_download
return
finally:
global steam_fail_counter
steam_fail_counter = 0
self.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
self.stop_download
self.is_pressed = False
def download_thread(self): def download_thread(self):
try: try:
global stopped global stopped
@ -1599,6 +2005,7 @@ class BOIIIWD(ctk.CTk):
return return
workshop_id = self.edit_workshop_id.get().strip() workshop_id = self.edit_workshop_id.get().strip()
destination_folder = self.edit_destination_folder.get().strip() destination_folder = self.edit_destination_folder.get().strip()
if not destination_folder or not os.path.exists(destination_folder): if not destination_folder or not os.path.exists(destination_folder):
@ -1701,7 +2108,10 @@ class BOIIIWD(ctk.CTk):
self.after(1, self.progress_bar.set(progress)) self.after(1, self.progress_bar.set(progress))
self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}")) self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}"))
self.after(1, lambda p=min(percentage_complete ,99): self.progress_text.configure(text=f"{p:.2f}%")) self.after(1, lambda p=min(percentage_complete ,99): self.progress_text.configure(text=f"{p:.2f}%"))
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}")) if show_fails:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d} - Fails: {steam_fail_counter}"))
else:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}"))
time.sleep(1) time.sleep(1)
else: else:
@ -1719,7 +2129,10 @@ class BOIIIWD(ctk.CTk):
self.after(1, lambda v=net_speed: self.label_speed.configure(text=f"Network Speed: {v:.2f} {speed_unit}")) 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}%")) self.after(1, lambda p=progress: self.progress_text.configure(text=f"{p}%"))
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}")) if show_fails:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d} - Fails: {steam_fail_counter}"))
else:
self.after(1, lambda h=elapsed_hours, m=elapsed_minutes, s=elapsed_seconds: self.elapsed_time.configure(text=f"Elapsed Time: {int(h):02d}:{int(m):02d}:{int(s):02d}"))
time.sleep(1) time.sleep(1)
command = f"+login anonymous +@sSteamCmdForcePlatformBitness 64 +app_info_update 1 +app_info_print 311210 app_update 311210 +workshop_download_item 311210 {workshop_id} validate +quit" command = f"+login anonymous +@sSteamCmdForcePlatformBitness 64 +app_info_update 1 +app_info_print 311210 app_update 311210 +workshop_download_item 311210 {workshop_id} validate +quit"
@ -1786,12 +2199,18 @@ class BOIIIWD(ctk.CTk):
self.button_stop.configure(state="normal") self.button_stop.configure(state="normal")
finally: finally:
global steam_fail_counter
steam_fail_counter = 0
self.stop_download self.stop_download
self.is_downloading = False self.is_pressed = False
def stop_download(self, on_close=None): def stop_download(self, on_close=None):
global stopped global stopped, steam_fail_counter
stopped = True stopped = True
self.queue_stop_button = True
steam_fail_counter = 0
self.is_pressed = False
self.after(1, self.label_file_size.configure(text=f"File size: 0KB"))
if on_close: if on_close:
subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
@ -1801,6 +2220,7 @@ class BOIIIWD(ctk.CTk):
subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, subprocess.run(['taskkill', '/F', '/IM', 'steamcmd.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW) creationflags=subprocess.CREATE_NO_WINDOW)
self.status_text.configure(text=f"Status: Not Downloading")
self.button_download.configure(state="normal") self.button_download.configure(state="normal")
self.button_stop.configure(state="disabled") self.button_stop.configure(state="disabled")
self.label_speed.configure(text="Network Speed: 0 KB/s") self.label_speed.configure(text="Network Speed: 0 KB/s")