Files
ProgressionMods/steam_workshop_gui.py

776 lines
34 KiB
Python

import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog, messagebox, font
import requests
import re
from urllib.parse import urlparse, parse_qs
import threading
import os
from pathlib import Path
import xml.etree.ElementTree as ET
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Try to import pyglet for font loading
try:
import pyglet
PYGLET_AVAILABLE = True
except ImportError:
PYGLET_AVAILABLE = False
print("pyglet not available - custom fonts may not work properly")
class SteamWorkshopGUI:
def __init__(self, root):
self.root = root
self.root.title("Steam Workshop Collection Manager")
self.root.geometry("1200x800")
self.root.configure(bg='#2b2b2b')
# Load custom font
self.load_custom_font()
# Configure dark theme
self.setup_dark_theme()
# Initialize paths
self.workshop_folder = None
self.modsconfig_path = None
# Create main frame
main_frame = tk.Frame(root, bg='#2b2b2b')
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Left panel for inputs
left_frame = tk.Frame(main_frame, bg='#2b2b2b', width=400)
left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
left_frame.pack_propagate(False)
# Right panel for output
right_frame = tk.Frame(main_frame, bg='#2b2b2b')
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.create_input_section(left_frame)
self.create_output_section(right_frame)
def load_custom_font(self):
"""Load the RimWorld font using pyglet"""
try:
font_path = os.path.join("art", "RimWordFont4.ttf")
if os.path.exists(font_path):
abs_font_path = os.path.abspath(font_path)
print(f"Font file found: {abs_font_path}")
if PYGLET_AVAILABLE:
try:
# Use pyglet to add the font
pyglet.font.add_file(abs_font_path)
# Try to get the font family name from pyglet
# Load the font to get its actual family name
pyglet_font = pyglet.font.load(None, 12, bold=False, italic=False)
# Common RimWorld font family names to try
possible_names = [
"RimWorld",
"RimWorldFont",
"RimWordFont4",
"Calibri",
"Arial"
]
# Test each possible font name
for font_name in possible_names:
try:
test_font = font.Font(family=font_name, size=10)
# If we get here, the font worked
self.custom_font_family = font_name
self.custom_font_available = True
print(f"Successfully loaded font with pyglet: {font_name}")
return
except Exception as e:
continue
print("pyglet loaded font but couldn't find working family name")
self.custom_font_available = False
except Exception as e:
print(f"pyglet font loading failed: {e}")
self.custom_font_available = False
else:
print("pyglet not available, trying Windows registration...")
# Fall back to Windows registration method
try:
import ctypes
from ctypes import wintypes
# Add font resource to Windows
gdi32 = ctypes.windll.gdi32
result = gdi32.AddFontResourceW(abs_font_path)
if result > 0:
print(f"Successfully registered font with Windows")
# Notify Windows that fonts have changed
try:
HWND_BROADCAST = 0xFFFF
WM_FONTCHANGE = 0x001D
ctypes.windll.user32.SendMessageW(HWND_BROADCAST, WM_FONTCHANGE, 0, 0)
print("Sent font change notification to Windows")
except Exception as notify_error:
print(f"Could not send font change notification: {notify_error}")
# For Windows registration, let's try using the actual font file name
# Sometimes the family name is different from what we expect
self.custom_font_family = "RimWorld" # Default assumption
self.custom_font_available = True
print(f"Using Windows-registered font: {self.custom_font_family}")
else:
print("Failed to register font with Windows")
self.custom_font_available = False
except Exception as e:
print(f"Windows font registration error: {e}")
self.custom_font_available = False
else:
print(f"Font file not found: {font_path}")
self.custom_font_available = False
except Exception as e:
print(f"Error loading custom font: {e}")
self.custom_font_available = False
def get_font(self, size=10, weight='normal'):
"""Get font tuple for UI elements"""
if self.custom_font_available:
try:
# Use the registered font family name
font_obj = font.Font(family=self.custom_font_family, size=size, weight=weight)
print(f"Created font: {self.custom_font_family}, size={size}, weight={weight}")
return font_obj
except Exception as e:
print(f"Error using custom font: {e}")
# Fall back to a distinctive font so we can see the difference
return font.Font(family='Courier New', size=size, weight=weight)
else:
print(f"Using fallback font: Courier New, size={size}")
return font.Font(family='Courier New', size=size, weight=weight)
def setup_dark_theme(self):
"""Configure dark theme colors"""
style = ttk.Style()
style.theme_use('clam')
# Configure colors for dark theme
style.configure('TLabel', background='#2b2b2b', foreground='#ffffff')
style.configure('TButton', background='#404040', foreground='#ffffff')
style.map('TButton', background=[('active', '#505050')])
style.configure('TEntry', background='#404040', foreground='#ffffff', fieldbackground='#404040')
def create_input_section(self, parent):
"""Create the left input section"""
title_label = tk.Label(parent, text="Steam Workshop Collections",
font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff')
title_label.pack(pady=(0, 10))
# RimWorld game folder input
rimworld_frame = tk.Frame(parent, bg='#2b2b2b')
rimworld_frame.pack(fill='x', pady=(0, 10))
rimworld_label = tk.Label(rimworld_frame, text="RimWorld Game Folder:",
font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff')
rimworld_label.pack(anchor='w')
rimworld_help = tk.Label(rimworld_frame,
text="Right-click RimWorld in Steam > Manage > Browse local files, copy that path",
font=self.get_font(8), bg='#2b2b2b', fg='#888888', wraplength=380)
rimworld_help.pack(anchor='w', pady=(2, 5))
rimworld_input_frame = tk.Frame(rimworld_frame, bg='#2b2b2b')
rimworld_input_frame.pack(fill='x')
self.rimworld_var = tk.StringVar()
self.rimworld_entry = tk.Entry(rimworld_input_frame, textvariable=self.rimworld_var,
font=self.get_font(9), bg='#404040', fg='#ffffff',
insertbackground='#ffffff', relief='flat', bd=5)
self.rimworld_entry.pack(side='left', fill='x', expand=True)
self.rimworld_entry.bind('<KeyRelease>', self.on_rimworld_path_change)
browse_game_btn = tk.Button(rimworld_input_frame, text="Browse",
command=self.browse_rimworld_folder,
bg='#505050', fg='white', font=self.get_font(8),
relief='flat', padx=10)
browse_game_btn.pack(side='right', padx=(5, 0))
# Workshop folder display (derived from RimWorld path)
workshop_frame = tk.Frame(parent, bg='#2b2b2b')
workshop_frame.pack(fill='x', pady=(0, 10))
workshop_label = tk.Label(workshop_frame, text="Workshop Folder (Auto-derived):",
font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff')
workshop_label.pack(anchor='w')
self.workshop_var = tk.StringVar(value="Enter RimWorld path above")
self.workshop_display = tk.Entry(workshop_frame, textvariable=self.workshop_var,
font=self.get_font(8), bg='#404040', fg='#ffffff',
insertbackground='#ffffff', relief='flat', bd=5)
self.workshop_display.pack(fill='x', pady=(5, 0))
self.workshop_display.bind('<KeyRelease>', self.on_workshop_path_change)
# ModsConfig.xml folder display
modsconfig_frame = tk.Frame(parent, bg='#2b2b2b')
modsconfig_frame.pack(fill='x', pady=(0, 15))
modsconfig_label = tk.Label(modsconfig_frame, text="ModsConfig.xml Path:",
font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff')
modsconfig_label.pack(anchor='w')
self.modsconfig_var = tk.StringVar()
self.modsconfig_display = tk.Entry(modsconfig_frame, textvariable=self.modsconfig_var,
font=self.get_font(8), bg='#404040', fg='#ffffff',
insertbackground='#ffffff', relief='flat', bd=5)
self.modsconfig_display.pack(fill='x', pady=(5, 0))
self.modsconfig_display.bind('<KeyRelease>', self.on_modsconfig_path_change)
# Initialize ModsConfig path
self.find_modsconfig_path()
# Core collection
core_url = os.getenv('CORE_COLLECTION_URL', 'https://steamcommunity.com/workshop/filedetails/?id=3521297585')
self.create_url_input(parent, "Core Collection:", core_url)
# Content collection
content_url = os.getenv('CONTENT_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712')
self.create_url_input(parent, "Content Collection:", content_url)
# Cosmetics collection
cosmetics_url = os.getenv('COSMETICS_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646')
self.create_url_input(parent, "Cosmetics Collection:", cosmetics_url)
# Load Progression Pack button
load_btn = tk.Button(parent, text="Load Progression Pack Complete",
command=self.load_progression_pack,
bg='#0078d4', fg='white', font=self.get_font(12, 'bold'),
relief='flat', padx=30, pady=12)
load_btn.pack(pady=30)
def create_url_input(self, parent, label_text, default_url):
"""Create a labeled URL input field"""
label = tk.Label(parent, text=label_text, font=self.get_font(10, 'bold'),
bg='#2b2b2b', fg='#ffffff')
label.pack(anchor='w', pady=(10, 5))
entry = tk.Entry(parent, font=self.get_font(9), bg='#404040', fg='#ffffff',
insertbackground='#ffffff', relief='flat', bd=5)
entry.pack(fill='x', pady=(0, 10))
entry.insert(0, default_url)
# Store reference to entry widgets
if not hasattr(self, 'url_entries'):
self.url_entries = {}
collection_name = label_text.replace(" Collection:", "").lower()
self.url_entries[collection_name] = entry
def create_output_section(self, parent):
"""Create the right output section"""
title_label = tk.Label(parent, text="Workshop IDs and Package Names",
font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff')
title_label.pack(pady=(0, 10))
# Output text area with scrollbar
self.output_text = scrolledtext.ScrolledText(parent,
font=self.get_font(10),
bg='#1e1e1e', fg='#ffffff',
insertbackground='#ffffff',
selectbackground='#404040',
relief='flat', bd=5)
self.output_text.pack(fill=tk.BOTH, expand=True)
def on_workshop_path_change(self, event=None):
"""Called when workshop path is manually changed"""
self.workshop_folder = self.workshop_var.get().strip()
def on_modsconfig_path_change(self, event=None):
"""Called when ModsConfig path is manually changed"""
self.modsconfig_path = self.modsconfig_var.get().strip()
def on_rimworld_path_change(self, event=None):
"""Called when RimWorld path is changed"""
rimworld_path = self.rimworld_var.get().strip()
if rimworld_path:
# Derive workshop path from RimWorld path
# From: D:\SteamLibrary\steamapps\common\RimWorld
# To: D:\SteamLibrary\steamapps\workshop\content\294100
if "steamapps" in rimworld_path.lower():
# Find the steamapps part and replace common\RimWorld with workshop\content\294100
parts = rimworld_path.split(os.sep)
try:
steamapps_index = next(i for i, part in enumerate(parts) if part.lower() == 'steamapps')
# Build new path: everything up to steamapps + steamapps + workshop + content + 294100
workshop_parts = parts[:steamapps_index + 1] + ['workshop', 'content', '294100']
workshop_path = os.sep.join(workshop_parts)
self.workshop_folder = workshop_path
self.workshop_var.set(workshop_path)
except (StopIteration, IndexError):
self.workshop_var.set("Invalid RimWorld path - should contain 'steamapps'")
else:
self.workshop_var.set("Invalid RimWorld path - should contain 'steamapps'")
else:
self.workshop_var.set("Enter RimWorld path above")
def browse_rimworld_folder(self):
"""Allow user to browse for RimWorld game folder"""
folder = filedialog.askdirectory(
title="Select RimWorld Game Folder (Right-click RimWorld in Steam > Manage > Browse local files)",
initialdir="C:\\"
)
if folder:
self.rimworld_var.set(folder)
self.on_rimworld_path_change() # Update workshop path
def find_modsconfig_path(self):
"""Find the ModsConfig.xml file in AppData"""
try:
# Get path template from .env
path_template = os.getenv('MODSCONFIG_PATH_TEMPLATE',
r'%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml')
# Expand environment variables
modsconfig_path = os.path.expandvars(path_template)
if os.path.exists(modsconfig_path):
self.modsconfig_path = modsconfig_path
self.modsconfig_var.set(modsconfig_path)
return modsconfig_path
else:
self.modsconfig_var.set("ModsConfig.xml not found")
return None
except Exception as e:
self.modsconfig_var.set(f"Error finding ModsConfig.xml: {str(e)}")
return None
def extract_workshop_id(self, url):
"""Extract workshop ID from Steam URL"""
# Clean up steam:// protocol URLs
if url.startswith('steam://openurl/'):
url = url.replace('steam://openurl/', '')
# Extract ID from URL parameters
try:
parsed_url = urlparse(url)
if 'id=' in parsed_url.query:
query_params = parse_qs(parsed_url.query)
return query_params.get('id', [None])[0]
elif '/filedetails/?id=' in url:
# Direct extraction for simple cases
match = re.search(r'id=(\d+)', url)
if match:
return match.group(1)
except Exception as e:
print(f"Error extracting ID from {url}: {e}")
return None
def load_progression_pack(self):
"""Load all progression pack collections and extract package names"""
if not self.workshop_folder or not os.path.exists(self.workshop_folder):
self.output_text.delete(1.0, tk.END)
self.output_text.insert(tk.END, "Workshop folder not found!\nPlease enter the correct RimWorld path above.")
return
self.output_text.delete(1.0, tk.END)
self.output_text.insert(tk.END, "Loading Progression Pack Complete...\n\n")
threading.Thread(target=self._load_progression_pack_thread, daemon=True).start()
def _load_progression_pack_thread(self):
"""Thread function to load progression pack"""
try:
all_workshop_ids = []
# Get collection URLs from entries
collections = {
"Core": self.url_entries['core'].get().strip(),
"Content": self.url_entries['content'].get().strip(),
"Cosmetics": self.url_entries['cosmetics'].get().strip()
}
# Fetch all workshop IDs from collections
for collection_name, url in collections.items():
if url:
self._safe_update_output(f"Fetching {collection_name} collection...\n")
workshop_id = self.extract_workshop_id(url)
if workshop_id:
workshop_ids = self.fetch_collection_items(workshop_id)
all_workshop_ids.extend(workshop_ids)
self._safe_update_output(f"Found {len(workshop_ids)} items in {collection_name}\n")
# Remove duplicates
unique_ids = list(set(all_workshop_ids))
self._safe_update_output(f"\nTotal unique workshop items: {len(unique_ids)}\n")
self._safe_update_output("Extracting package names from About.xml files...\n\n")
# Extract package names from About.xml files
results = []
for workshop_id in sorted(unique_ids):
package_name = self.get_package_name_from_about_xml(workshop_id)
results.append((workshop_id, package_name))
# Display results
self._safe_update_output("=== PROGRESSION PACK COMPLETE ===\n\n")
self._safe_update_output(f"{'Workshop ID':<15} | Package Name\n")
self._safe_update_output("-" * 80 + "\n")
for workshop_id, package_name in results:
self._safe_update_output(f"{workshop_id:<15} | {package_name}\n")
self._safe_update_output(f"\nTotal mods processed: {len(results)}\n")
# Create ProgressionVanilla.rml file
self.create_rml_file(results)
except Exception as e:
self._safe_update_output(f"Error loading progression pack: {str(e)}\n")
def create_rml_file(self, mod_data):
"""Create ProgressionVanilla.rml file with workshop IDs and names"""
try:
# Get ModLists directory path
modlists_path = os.path.expandvars(r"%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\ModLists")
# Create directory if it doesn't exist
os.makedirs(modlists_path, exist_ok=True)
rml_file_path = os.path.join(modlists_path, "ProgressionVanilla.rml")
# Create XML content
xml_content = self.generate_rml_xml(mod_data)
# Write to file
with open(rml_file_path, 'w', encoding='utf-8') as f:
f.write(xml_content)
self._safe_update_output(f"\nProgressionVanilla.rml created successfully at:\n{rml_file_path}\n")
except Exception as e:
self._safe_update_output(f"Error creating RML file: {str(e)}\n")
def generate_rml_xml(self, mod_data):
"""Generate the XML content for the .rml file"""
# Get core mods from ModsConfig.xml and RimWorld Data folder
core_mods = self.get_core_mods_from_config()
# Extract mod names from About.xml files for workshop mods
mod_names = []
for workshop_id, package_id in mod_data:
mod_name = self.get_mod_name_from_about_xml(workshop_id)
mod_names.append(mod_name)
xml_lines = [
'<?xml version="1.0" encoding="utf-8"?>',
'<savedModList>',
'\t<meta>',
'\t\t<gameVersion>1.6.4633 rev1261</gameVersion>',
'\t\t<modIds>'
]
# Add core mods first
for package_id, mod_name in core_mods:
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
# Add workshop mod package IDs to meta section
for workshop_id, package_id in mod_data:
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
xml_lines.extend([
'\t\t</modIds>',
'\t\t<modSteamIds>'
])
# Add core mods (they don't have Steam IDs, so use 0)
for package_id, mod_name in core_mods:
xml_lines.append(f'\t\t\t<li>0</li>')
# Add workshop IDs to meta section
for workshop_id, package_id in mod_data:
xml_lines.append(f'\t\t\t<li>{workshop_id}</li>')
xml_lines.extend([
'\t\t</modSteamIds>',
'\t\t<modNames>'
])
# Add core mod names first
for package_id, mod_name in core_mods:
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
# Add workshop mod names to meta section
for mod_name in mod_names:
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
xml_lines.extend([
'\t\t</modNames>',
'\t</meta>',
'\t<modList>',
'\t\t<ids>'
])
# Add core mods first to modList section
for package_id, mod_name in core_mods:
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
# Add workshop mod package IDs to modList section
for workshop_id, package_id in mod_data:
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
xml_lines.extend([
'\t\t</ids>',
'\t\t<names>'
])
# Add core mod names first to modList section
for package_id, mod_name in core_mods:
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
# Add workshop mod names to modList section
for mod_name in mod_names:
xml_lines.append(f'\t\t\t<li>{mod_name}</li>')
xml_lines.extend([
'\t\t</names>',
'\t</modList>',
'</savedModList>'
])
return '\n'.join(xml_lines)
def get_core_mods_from_config(self):
"""Get core mods from ModsConfig.xml knownExpansions section and their info from RimWorld Data folder"""
core_mods = []
try:
if not self.modsconfig_path or not os.path.exists(self.modsconfig_path):
self._safe_update_output("ModsConfig.xml not found, using default core mods\n")
return [('ludeon.rimworld', 'RimWorld')]
# Parse ModsConfig.xml
tree = ET.parse(self.modsconfig_path)
root = tree.getroot()
# Get RimWorld Data folder path from the RimWorld game path
rimworld_data_path = None
if self.rimworld_var.get().strip():
rimworld_game_path = self.rimworld_var.get().strip()
rimworld_data_path = os.path.join(rimworld_game_path, "Data")
# Find knownExpansions section
known_expansions_element = root.find('knownExpansions')
if known_expansions_element is not None:
for li in known_expansions_element.findall('li'):
expansion_id = li.text
if expansion_id:
mod_name = self.get_core_mod_name(expansion_id.strip(), rimworld_data_path)
core_mods.append((expansion_id.strip(), mod_name))
if not core_mods:
# Fallback if no expansions found
core_mods = [('ludeon.rimworld', 'RimWorld')]
self._safe_update_output(f"Found {len(core_mods)} core expansions from ModsConfig.xml\n")
except Exception as e:
self._safe_update_output(f"Error reading core mods: {str(e)}\n")
core_mods = [('ludeon.rimworld', 'RimWorld')]
return core_mods
def get_core_mod_name(self, mod_id, rimworld_data_path):
"""Get mod name from RimWorld Data folder About.xml"""
try:
if not rimworld_data_path or not os.path.exists(rimworld_data_path):
return self.get_default_mod_name(mod_id)
# Look for mod folder in Data directory
# Core mods are usually in subfolders like Core, Royalty, etc.
possible_folders = [
mod_id.replace('ludeon.rimworld.', '').replace('ludeon.rimworld', 'Core'),
mod_id.replace('ludeon.rimworld.', '').capitalize(),
mod_id.split('.')[-1].capitalize() if '.' in mod_id else mod_id
]
for folder_name in possible_folders:
about_xml_path = os.path.join(rimworld_data_path, folder_name, "About", "About.xml")
if os.path.exists(about_xml_path):
try:
tree = ET.parse(about_xml_path)
root = tree.getroot()
name_element = root.find('name')
if name_element is not None and name_element.text:
return name_element.text.strip()
except ET.ParseError:
continue
# If not found in Data folder, use default name
return self.get_default_mod_name(mod_id)
except Exception as e:
return self.get_default_mod_name(mod_id)
def get_default_mod_name(self, mod_id):
"""Get default display name for core mods"""
default_names = {
'ludeon.rimworld': 'RimWorld',
'ludeon.rimworld.royalty': 'Royalty',
'ludeon.rimworld.ideology': 'Ideology',
'ludeon.rimworld.biotech': 'Biotech',
'ludeon.rimworld.anomaly': 'Anomaly',
'ludeon.rimworld.odyssey': 'Odyssey'
}
return default_names.get(mod_id, mod_id)
def get_mod_name_from_about_xml(self, workshop_id):
"""Extract mod name from About/About.xml file"""
try:
about_xml_path = os.path.join(self.workshop_folder, workshop_id, "About", "About.xml")
if not os.path.exists(about_xml_path):
return f"Workshop ID {workshop_id}"
# Parse the XML file
tree = ET.parse(about_xml_path)
root = tree.getroot()
# Look for name element
name_element = root.find('name')
if name_element is not None and name_element.text:
return name_element.text.strip()
else:
return f"Workshop ID {workshop_id}"
except ET.ParseError:
return f"Workshop ID {workshop_id}"
except Exception as e:
return f"Workshop ID {workshop_id}"
def get_package_name_from_about_xml(self, workshop_id):
"""Extract package name from About/About.xml file"""
try:
about_xml_path = os.path.join(self.workshop_folder, workshop_id, "About", "About.xml")
if not os.path.exists(about_xml_path):
return "About.xml not found"
# Parse the XML file
tree = ET.parse(about_xml_path)
root = tree.getroot()
# Look for packageId element
package_id_element = root.find('packageId')
if package_id_element is not None and package_id_element.text:
return package_id_element.text.strip()
else:
return "packageId not found"
except ET.ParseError:
return "XML parse error"
except Exception as e:
return f"Error: {str(e)}"
def _safe_update_output(self, text):
"""Safely update output text without recursion"""
try:
self.output_text.insert(tk.END, text)
self.output_text.see(tk.END)
except Exception:
pass # Ignore update errors to prevent recursion
def fetch_collection_items(self, collection_id):
"""Fetch workshop IDs from a Steam Workshop collection"""
try:
# Steam Workshop collection URL
url = f"https://steamcommunity.com/sharedfiles/filedetails/?id={collection_id}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
self._safe_update_output(f"Fetching collection page...\n")
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
# Extract workshop IDs from collection items only
html_content = response.text
workshop_ids = []
# Find the collectionChildren section
collection_start = html_content.find('<div class="collectionChildren">')
if collection_start != -1:
# Find the proper end of the collectionChildren section
# Look for the closing </div> that matches the opening collectionChildren div
# We need to count div tags to find the matching closing tag
search_pos = collection_start + len('<div class="collectionChildren">')
div_count = 1 # We've opened one div
collection_end = -1
while search_pos < len(html_content) and div_count > 0:
next_open = html_content.find('<div', search_pos)
next_close = html_content.find('</div>', search_pos)
if next_close == -1:
break
if next_open != -1 and next_open < next_close:
div_count += 1
search_pos = next_open + 4
else:
div_count -= 1
if div_count == 0:
collection_end = next_close + 6
break
search_pos = next_close + 6
if collection_end == -1:
collection_end = len(html_content)
collection_section = html_content[collection_start:collection_end]
# Extract IDs from sharedfile_ elements (these are the actual collection items)
sharedfile_ids = re.findall(r'id="sharedfile_(\d+)"', collection_section)
workshop_ids.extend(sharedfile_ids)
self._safe_update_output(f"Found {len(sharedfile_ids)} collection items\n")
else:
# Fallback: search the entire page but be more selective
self._safe_update_output("Collection section not found, using fallback method\n")
sharedfile_ids = re.findall(r'id="sharedfile_(\d+)"', html_content)
workshop_ids.extend(sharedfile_ids)
# Remove duplicates and the collection ID itself
unique_ids = list(set(workshop_ids))
if collection_id in unique_ids:
unique_ids.remove(collection_id)
return sorted(unique_ids) # Sort for consistent output
except requests.RequestException as e:
self._safe_update_output(f"Network error: {str(e)}\n")
return []
except Exception as e:
self._safe_update_output(f"Error fetching collection: {str(e)}\n")
return []
def main():
root = tk.Tk()
app = SteamWorkshopGUI(root)
root.mainloop()
if __name__ == "__main__":
main()