feat: support multiple cache sources (Standard + Steam)

This commit is contained in:
LemonNexus 2026-02-12 14:49:40 +00:00
parent 52e48ebbb8
commit aec841f9f3
1 changed files with 187 additions and 75 deletions

View File

@ -102,25 +102,21 @@ def parse_library_folders_vdf(vdf_path: Path) -> List[Path]:
return libraries return libraries
def find_entropia_cache_path() -> Optional[Path]: def find_all_cache_paths() -> List[Path]:
""" """
Find Entropia Universe cache folder. Find all Entropia Universe cache folders.
Checks multiple locations based on platform. Returns a list of paths (standard install, Steam, etc.)
""" """
found_paths = []
# Check standard installation first (platform-specific) # Check standard installation first (platform-specific)
if sys.platform == 'win32': if sys.platform == 'win32':
standard_paths = [ standard_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon")
Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon"),
]
else: else:
# Linux standard paths standard_path = Path.home() / ".local" / "share" / "Entropia Universe" / "public_users_data" / "cache" / "icon"
standard_paths = [
Path.home() / ".local" / "share" / "Entropia Universe" / "public_users_data" / "cache" / "icon",
]
for path in standard_paths: if standard_path.exists() and list(standard_path.rglob("*.tga")):
if path.exists() and list(path.rglob("*.tga")): found_paths.append(("Standard Install", standard_path))
return path
# Check Steam installations # Check Steam installations
steam_paths = get_steam_paths() steam_paths = get_steam_paths()
@ -129,7 +125,7 @@ def find_entropia_cache_path() -> Optional[Path]:
# Check default Steam library # Check default Steam library
eu_path = steam_path / "steamapps" / "common" / "Entropia Universe" / "public_users_data" / "cache" / "icon" eu_path = steam_path / "steamapps" / "common" / "Entropia Universe" / "public_users_data" / "cache" / "icon"
if eu_path.exists() and list(eu_path.rglob("*.tga")): if eu_path.exists() and list(eu_path.rglob("*.tga")):
return eu_path found_paths.append(("Steam", eu_path))
# Check other Steam libraries # Check other Steam libraries
library_folders = steam_path / "steamapps" / "libraryfolders.vdf" library_folders = steam_path / "steamapps" / "libraryfolders.vdf"
@ -138,9 +134,19 @@ def find_entropia_cache_path() -> Optional[Path]:
for library in libraries: for library in libraries:
eu_path = library / "steamapps" / "common" / "Entropia Universe" / "public_users_data" / "cache" / "icon" eu_path = library / "steamapps" / "common" / "Entropia Universe" / "public_users_data" / "cache" / "icon"
if eu_path.exists() and list(eu_path.rglob("*.tga")): if eu_path.exists() and list(eu_path.rglob("*.tga")):
return eu_path # Check if we already have this path
if not any(str(p[1]) == str(eu_path) for p in found_paths):
found_paths.append(("Steam", eu_path))
return None return found_paths
def find_entropia_cache_path() -> Optional[Path]:
"""
Find first Entropia Universe cache folder (for backward compatibility).
"""
paths = find_all_cache_paths()
return paths[0][1] if paths else None
class TGAHeader: class TGAHeader:
@ -338,8 +344,9 @@ class IconExtractorWindow(QMainWindow):
self.worker: Optional[ConversionWorker] = None self.worker: Optional[ConversionWorker] = None
self.found_files: List[Path] = [] self.found_files: List[Path] = []
# Auto-detect cache path # Find all cache sources (standard, steam, etc.)
self.base_cache_path = find_entropia_cache_path() self.cache_sources = find_all_cache_paths() # List of (name, path) tuples
self.selected_source_index = 0 # 0 = All Sources, 1+ = specific source
self.cache_path_manually_set = False self.cache_path_manually_set = False
self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor") self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor")
@ -348,8 +355,9 @@ class IconExtractorWindow(QMainWindow):
self._load_icon() self._load_icon()
self._load_settings() self._load_settings()
# Detect subfolders if path was found # Detect subfolders if any sources were found
if self.base_cache_path: if self.cache_sources:
self._populate_source_combo()
self._detect_subfolders() self._detect_subfolders()
else: else:
self._show_cache_not_found() self._show_cache_not_found()
@ -361,10 +369,40 @@ class IconExtractorWindow(QMainWindow):
"font-family: Consolas; font-size: 10px; color: #f44336; " "font-family: Consolas; font-size: 10px; color: #f44336; "
"padding: 6px 8px; background: #3e2723; border-radius: 3px;" "padding: 6px 8px; background: #3e2723; border-radius: 3px;"
) )
self.source_combo.clear()
self.source_combo.addItem("❌ No sources found")
self.source_combo.setEnabled(False)
self.status_label.setText("Click 'Browse...' to select the cache folder manually") self.status_label.setText("Click 'Browse...' to select the cache folder manually")
self.files_count_label.setText("No cache folder selected") self.files_count_label.setText("No cache folder selected")
self.convert_btn.setEnabled(False) self.convert_btn.setEnabled(False)
def _populate_source_combo(self):
"""Populate the source selection dropdown with found cache sources."""
self.source_combo.clear()
self.source_combo.setEnabled(True)
# Count total icons across all sources
total_icons = 0
for name, path in self.cache_sources:
total_icons += len(list(path.rglob("*.tga")))
# Add "All Sources" option
self.source_combo.addItem(f"🌐 All Sources ({total_icons} icons)", "all")
# Add individual sources
for name, path in self.cache_sources:
icon_count = len(list(path.rglob("*.tga")))
display_name = f"📁 {name}: {path.parent.parent.parent.name if path.parent.parent.parent != path else 'Entropia Universe'} ({icon_count} icons)"
self.source_combo.addItem(display_name, str(path))
# Connect source change handler
self.source_combo.currentIndexChanged.connect(self._on_source_changed)
def _on_source_changed(self):
"""Handle source selection change."""
self.selected_source_index = self.source_combo.currentIndex()
self._detect_subfolders()
def _browse_cache_folder(self): def _browse_cache_folder(self):
"""Browse for cache folder manually.""" """Browse for cache folder manually."""
folder = QFileDialog.getExistingDirectory( folder = QFileDialog.getExistingDirectory(
@ -380,13 +418,12 @@ class IconExtractorWindow(QMainWindow):
tga_files = list(selected_path.rglob("*.tga")) tga_files = list(selected_path.rglob("*.tga"))
if tga_files: if tga_files:
self.base_cache_path = selected_path # Add as a manual source
self.cache_sources.append(("Manual", selected_path))
self.cache_path_manually_set = True self.cache_path_manually_set = True
self.cache_label.setText(str(selected_path)) self._populate_source_combo()
self.cache_label.setStyleSheet( # Select the newly added source (last index)
"font-family: Consolas; font-size: 10px; color: #aaa; " self.source_combo.setCurrentIndex(len(self.cache_sources))
"padding: 6px 8px; background: #252525; border-radius: 3px;"
)
self.status_label.setText(f"Found {len(tga_files)} TGA files") self.status_label.setText(f"Found {len(tga_files)} TGA files")
self._detect_subfolders() self._detect_subfolders()
else: else:
@ -468,15 +505,23 @@ class IconExtractorWindow(QMainWindow):
cache_layout.setContentsMargins(12, 18, 12, 12) cache_layout.setContentsMargins(12, 18, 12, 12)
cache_layout.setSpacing(10) cache_layout.setSpacing(10)
# Display detected or manual path # Source selection dropdown
if self.base_cache_path: source_layout = QHBoxLayout()
path_display = str(self.base_cache_path).replace("/", "\\") source_layout.setSpacing(8)
self.cache_path_full = path_display
else:
path_display = "Not found - click Browse to select"
self.cache_path_full = ""
self.cache_label = QLabel(path_display) source_label = QLabel("🌐 Source:")
source_label.setStyleSheet("font-size: 12px;")
source_layout.addWidget(source_label)
self.source_combo = QComboBox()
self.source_combo.setMinimumWidth(250)
self.source_combo.setStyleSheet("font-size: 12px; padding: 3px;")
source_layout.addWidget(self.source_combo, 1)
cache_layout.addLayout(source_layout)
# Path display label
self.cache_label = QLabel("Scanning for cache folders...")
self.cache_label.setStyleSheet( self.cache_label.setStyleSheet(
"font-family: Consolas; font-size: 10px; color: #aaa; " "font-family: Consolas; font-size: 10px; color: #aaa; "
"padding: 6px 8px; background: #252525; border-radius: 3px;" "padding: 6px 8px; background: #252525; border-radius: 3px;"
@ -484,30 +529,30 @@ class IconExtractorWindow(QMainWindow):
self.cache_label.setWordWrap(True) self.cache_label.setWordWrap(True)
cache_layout.addWidget(self.cache_label) cache_layout.addWidget(self.cache_label)
# Subfolder selector and browse button # Version selector
subfolder_layout = QHBoxLayout() version_layout = QHBoxLayout()
subfolder_layout.setSpacing(8) version_layout.setSpacing(8)
subfolder_label = QLabel("📁 Version:") version_label = QLabel("📁 Version:")
subfolder_label.setStyleSheet("font-size: 12px;") version_label.setStyleSheet("font-size: 12px;")
subfolder_layout.addWidget(subfolder_label) version_layout.addWidget(version_label)
self.subfolder_combo = QComboBox() self.version_combo = QComboBox()
self.subfolder_combo.setMinimumWidth(180) self.version_combo.setMinimumWidth(180)
self.subfolder_combo.setStyleSheet("font-size: 12px; padding: 3px;") self.version_combo.setStyleSheet("font-size: 12px; padding: 3px;")
self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed) self.version_combo.currentIndexChanged.connect(self._on_version_changed)
subfolder_layout.addWidget(self.subfolder_combo, 1) version_layout.addWidget(self.version_combo, 1)
refresh_btn = QPushButton("🔄 Refresh") refresh_btn = QPushButton("🔄 Refresh")
refresh_btn.setMaximumWidth(80) refresh_btn.setMaximumWidth(80)
refresh_btn.setStyleSheet("font-size: 11px; padding: 4px;") refresh_btn.setStyleSheet("font-size: 11px; padding: 4px;")
refresh_btn.clicked.connect(self._detect_subfolders) refresh_btn.clicked.connect(self._detect_subfolders)
subfolder_layout.addWidget(refresh_btn) version_layout.addWidget(refresh_btn)
cache_layout.addLayout(subfolder_layout) cache_layout.addLayout(version_layout)
# Browse button for manual selection # Browse button for manual selection
browse_btn = QPushButton("📂 Browse...") browse_btn = QPushButton("📂 Browse for cache folder...")
browse_btn.setStyleSheet("font-size: 11px; padding: 5px;") browse_btn.setStyleSheet("font-size: 11px; padding: 5px;")
browse_btn.clicked.connect(self._browse_cache_folder) browse_btn.clicked.connect(self._browse_cache_folder)
cache_layout.addWidget(browse_btn) cache_layout.addWidget(browse_btn)
@ -905,54 +950,91 @@ class IconExtractorWindow(QMainWindow):
self.settings.setValue("output_dir", str(self.converter.output_dir)) self.settings.setValue("output_dir", str(self.converter.output_dir))
def _detect_subfolders(self): def _detect_subfolders(self):
"""Detect version subfolders in the cache directory.""" """Detect version subfolders in the selected cache source(s)."""
self.subfolder_combo.clear() self.version_combo.clear()
if not self.base_cache_path or not self.base_cache_path.exists(): # Get the selected source path(s)
self.cache_label.setText("❌ Cache folder not found") source_index = self.source_combo.currentIndex()
source_data = self.source_combo.currentData()
if not self.cache_sources or source_data is None:
self.cache_label.setText("❌ No cache source selected")
self.cache_label.setStyleSheet( self.cache_label.setStyleSheet(
"font-family: Consolas; font-size: 10px; color: #f44336; " "font-family: Consolas; font-size: 10px; color: #f44336; "
"padding: 6px 8px; background: #3e2723; border-radius: 3px;" "padding: 6px 8px; background: #3e2723; border-radius: 3px;"
) )
self.version_combo.addItem("No sources available")
self.status_label.setText("Click 'Browse...' to select the cache folder manually") self.status_label.setText("Click 'Browse...' to select the cache folder manually")
self.files_count_label.setText("No cache folder selected") self.files_count_label.setText("No cache folder selected")
self.convert_btn.setEnabled(False) self.convert_btn.setEnabled(False)
return return
# Determine which paths to scan
if source_index == 0 or source_data == "all":
# Scan all sources
paths_to_scan = [path for _, path in self.cache_sources]
else:
# Scan specific source
paths_to_scan = [Path(source_data)]
# Find all version subfolders across selected sources
subfolders = [] subfolders = []
for item in self.base_cache_path.iterdir(): for source_path in paths_to_scan:
if item.is_dir(): if source_path.exists():
tga_count = len(list(item.glob("*.tga"))) for item in source_path.iterdir():
if tga_count > 0: if item.is_dir():
subfolders.append((item.name, tga_count, item)) tga_count = len(list(item.glob("*.tga")))
if tga_count > 0:
# Include source name in display
source_name = None
for name, path in self.cache_sources:
if path == source_path:
source_name = name
break
subfolders.append((item.name, tga_count, item, source_name))
if not subfolders: if not subfolders:
self.cache_label.setText(f"No subfolders with icons in {self.base_cache_path}") self.cache_label.setText("No version folders found in selected source(s)")
self.status_label.setText("No version folders found") self.status_label.setText("No TGA files found")
self.version_combo.addItem("No versions found")
self.convert_btn.setEnabled(False)
return return
subfolders.sort(key=lambda x: x[0]) subfolders.sort(key=lambda x: x[0])
for name, count, path in subfolders: for name, count, path, source_name in subfolders:
self.subfolder_combo.addItem(f"{name} ({count} icons)", str(path)) display = f"{name} ({count} icons)"
if source_name and len(self.cache_sources) > 1:
display += f" [{source_name}]"
self.version_combo.addItem(display, str(path))
total_icons = sum(s[1] for s in subfolders) total_icons = sum(s[1] for s in subfolders)
self.subfolder_combo.insertItem(0, f"All Folders ({total_icons} icons)", "all") self.version_combo.insertItem(0, f"📁 All Folders ({total_icons} icons)", "all")
self.subfolder_combo.setCurrentIndex(0) self.version_combo.setCurrentIndex(0)
# Update cache label to show active source(s)
if source_index == 0 or source_data == "all":
if len(self.cache_sources) == 1:
display_path = str(self.cache_sources[0][1])
else:
display_path = f"{len(self.cache_sources)} sources (All)"
else:
display_path = str(source_data)
if sys.platform == 'win32':
display_path = display_path.replace("/", "\\")
# Update display
display_path = str(self.base_cache_path).replace("/", "\\")
self.cache_label.setText(display_path) self.cache_label.setText(display_path)
self.cache_label.setStyleSheet( self.cache_label.setStyleSheet(
"font-family: Consolas; font-size: 10px; color: #aaa; " "font-family: Consolas; font-size: 10px; color: #aaa; "
"padding: 6px 8px; background: #252525; border-radius: 3px;" "padding: 6px 8px; background: #252525; border-radius: 3px;"
) )
self.status_label.setText(f"Found {len(subfolders)} version folders") self.status_label.setText(f"Found {len(subfolders)} version folders with {total_icons} icons")
self._refresh_file_list() self._refresh_file_list()
def _on_subfolder_changed(self): def _on_version_changed(self):
"""Handle subfolder selection change.""" """Handle version selection change."""
self._refresh_file_list() self._refresh_file_list()
def _browse_output(self): def _browse_output(self):
@ -965,7 +1047,10 @@ class IconExtractorWindow(QMainWindow):
if folder: if folder:
self.converter.output_dir = Path(folder) self.converter.output_dir = Path(folder)
self.output_label.setText("Documents\\Entropia Universe\\Icons\\") if sys.platform == 'win32':
self.output_label.setText("Documents\\Entropia Universe\\Icons\\")
else:
self.output_label.setText("~/Documents/Entropia Universe/Icons/")
self._save_settings() self._save_settings()
def _refresh_file_list(self): def _refresh_file_list(self):
@ -973,14 +1058,31 @@ class IconExtractorWindow(QMainWindow):
self.files_list.clear() self.files_list.clear()
self.found_files = [] self.found_files = []
if not self.base_cache_path or not self.base_cache_path.exists(): # Get the selected source
source_index = self.source_combo.currentIndex()
source_data = self.source_combo.currentData()
if not self.cache_sources or source_data is None:
return return
path_data = self.subfolder_combo.currentData() # Determine which source paths to scan
if path_data == "all" or path_data is None: if source_index == 0 or source_data == "all":
folders_to_scan = [d for d in self.base_cache_path.iterdir() if d.is_dir()] source_paths = [path for _, path in self.cache_sources]
else: else:
folders_to_scan = [Path(path_data)] source_paths = [Path(source_data)]
# Get the selected version
version_data = self.version_combo.currentData()
if version_data == "all" or version_data is None:
# Scan all version folders in selected source(s)
folders_to_scan = []
for source_path in source_paths:
if source_path.exists():
folders_to_scan.extend([d for d in source_path.iterdir() if d.is_dir()])
else:
# Scan specific version folder
folders_to_scan = [Path(version_data)]
tga_files = [] tga_files = []
for folder in folders_to_scan: for folder in folders_to_scan:
@ -992,7 +1094,17 @@ class IconExtractorWindow(QMainWindow):
for tga_file in sorted(tga_files): for tga_file in sorted(tga_files):
try: try:
rel_path = f"{tga_file.parent.name}/{tga_file.name}" # Show source in path if multiple sources
source_name = None
for name, path in self.cache_sources:
if str(tga_file).startswith(str(path)):
source_name = name
break
if len(self.cache_sources) > 1 and source_name:
rel_path = f"[{source_name}] {tga_file.parent.name}/{tga_file.name}"
else:
rel_path = f"{tga_file.parent.name}/{tga_file.name}"
except: except:
rel_path = tga_file.name rel_path = tga_file.name