""" Lemontropia Suite - Icon Manager Download and manage item icons from Entropia Universe. Supports multiple sources: EntropiaWiki, EntropiaNexus, local cache. """ import json import logging import requests from pathlib import Path from typing import Optional, Dict, List, Tuple from dataclasses import dataclass from PIL import Image import io logger = logging.getLogger(__name__) @dataclass class IconSource: """Icon source configuration.""" name: str base_url: str icon_path_template: str # e.g., "/images/items/{item_id}.png" supports_search: bool = False search_url: Optional[str] = None class EntropiaWikiIcons: """ Icon fetcher from EntropiaWiki (entropiawiki.com). EntropiaWiki hosts item icons that can be accessed by item name. """ BASE_URL = "https://www.entropiawiki.com" def __init__(self, cache_dir: Optional[Path] = None): self.cache_dir = cache_dir or Path.home() / ".lemontropia" / "icons" / "wiki" self.cache_dir.mkdir(parents=True, exist_ok=True) self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Lemontropia-Suite/1.0 (Personal Use)' }) def _sanitize_name(self, name: str) -> str: """Convert item name to wiki format.""" # Wiki uses underscores for spaces, removes special chars sanitized = name.replace(' ', '_') sanitized = sanitized.replace('(', '') sanitized = sanitized.replace(')', '') sanitized = sanitized.replace("'", '') return sanitized def get_icon_url(self, item_name: str) -> str: """Get icon URL for item.""" wiki_name = self._sanitize_name(item_name) return f"{self.BASE_URL}/images/{wiki_name}.png" def download_icon(self, item_name: str, size: Tuple[int, int] = (64, 64)) -> Optional[Path]: """ Download icon from EntropiaWiki. Returns path to downloaded icon or None if not found. """ cache_path = self.cache_dir / f"{self._sanitize_name(item_name)}_{size[0]}x{size[1]}.png" # Check cache first if cache_path.exists(): logger.debug(f"Icon cached: {cache_path}") return cache_path url = self.get_icon_url(item_name) try: response = self.session.get(url, timeout=10) if response.status_code == 200: # Open image and resize img = Image.open(io.BytesIO(response.content)) # Convert to RGBA if needed if img.mode != 'RGBA': img = img.convert('RGBA') # Resize maintaining aspect ratio img.thumbnail(size, Image.Resampling.LANCZOS) # Save img.save(cache_path, 'PNG') logger.info(f"Downloaded icon: {item_name} -> {cache_path}") return cache_path else: logger.warning(f"Icon not found on wiki: {item_name} (HTTP {response.status_code})") return None except Exception as e: logger.error(f"Failed to download icon for {item_name}: {e}") return None class EntropiaNexusIcons: """ Icon fetcher from EntropiaNexus API. The Nexus API may provide icon URLs or data. """ BASE_URL = "https://api.entropianexus.com" def __init__(self, cache_dir: Optional[Path] = None): self.cache_dir = cache_dir or Path.home() / ".lemontropia" / "icons" / "nexus" self.cache_dir.mkdir(parents=True, exist_ok=True) self.session = requests.Session() def get_icon_for_weapon(self, weapon_id: int, size: Tuple[int, int] = (64, 64)) -> Optional[Path]: """Get icon for weapon by ID.""" # Nexus may not have direct icon URLs, but we can try # This is a placeholder for actual Nexus icon fetching logger.debug(f"Nexus icon fetch not yet implemented for weapon {weapon_id}") return None class IconManager: """ Central icon manager that tries multiple sources. """ def __init__(self, cache_dir: Optional[Path] = None): self.cache_dir = cache_dir or Path.home() / ".lemontropia" / "icons" self.cache_dir.mkdir(parents=True, exist_ok=True) self.wiki = EntropiaWikiIcons(self.cache_dir / "wiki") self.nexus = EntropiaNexusIcons(self.cache_dir / "nexus") # Icon size presets self.SIZES = { 'small': (32, 32), 'medium': (64, 64), 'large': (128, 128), 'hud': (48, 48), } # Failed lookups cache (avoid repeated requests) self.failed_lookups: set = set() self._load_failed_lookups() def _load_failed_lookups(self): """Load list of items that don't have icons.""" failed_file = self.cache_dir / "failed_lookups.json" if failed_file.exists(): try: with open(failed_file, 'r') as f: self.failed_lookups = set(json.load(f)) except: pass def _save_failed_lookups(self): """Save failed lookups to avoid repeated requests.""" failed_file = self.cache_dir / "failed_lookups.json" try: with open(failed_file, 'w') as f: json.dump(list(self.failed_lookups), f) except: pass def get_icon(self, item_name: str, size: str = 'medium') -> Optional[Path]: """ Get icon for item, trying multiple sources. Args: item_name: Name of the item (e.g., "ArMatrix BP-25 (L)") size: 'small', 'medium', 'large', or 'hud' Returns: Path to icon file or None if not found """ if item_name in self.failed_lookups: return None size_tuple = self.SIZES.get(size, (64, 64)) # Try Wiki first icon_path = self.wiki.download_icon(item_name, size_tuple) if icon_path: return icon_path # Add to failed lookups self.failed_lookups.add(item_name) self._save_failed_lookups() return None def get_icon_for_gear(self, weapon_name: Optional[str] = None, armor_name: Optional[str] = None) -> Dict[str, Optional[Path]]: """Get icons for currently equipped gear.""" return { 'weapon': self.get_icon(weapon_name) if weapon_name else None, 'armor': self.get_icon(armor_name) if armor_name else None, } def export_icon(self, item_name: str, export_path: Path, size: str = 'large') -> bool: """ Export icon to specified path. Args: item_name: Item name export_path: Where to save the PNG size: Icon size preset Returns: True if successful """ icon_path = self.get_icon(item_name, size) if not icon_path: logger.error(f"Cannot export: icon not found for {item_name}") return False try: # Copy to export location import shutil shutil.copy2(icon_path, export_path) logger.info(f"Exported icon: {item_name} -> {export_path}") return True except Exception as e: logger.error(f"Failed to export icon: {e}") return False def batch_export_icons(self, item_names: List[str], export_dir: Path, size: str = 'large') -> List[Tuple[str, bool]]: """ Export multiple icons. Returns: List of (item_name, success) tuples """ export_dir.mkdir(parents=True, exist_ok=True) results = [] for item_name in item_names: safe_name = "".join(c for c in item_name if c.isalnum() or c in "._- ").strip() export_path = export_dir / f"{safe_name}.png" success = self.export_icon(item_name, export_path, size) results.append((item_name, success)) return results def get_cache_stats(self) -> Dict: """Get icon cache statistics.""" wiki_count = len(list(self.wiki.cache_dir.glob("*.png"))) failed_count = len(self.failed_lookups) return { 'cached_icons': wiki_count, 'failed_lookups': failed_count, 'cache_dir': str(self.cache_dir), } def clear_cache(self) -> None: """Clear icon cache.""" import shutil if self.wiki.cache_dir.exists(): shutil.rmtree(self.wiki.cache_dir) self.wiki.cache_dir.mkdir(parents=True, exist_ok=True) self.failed_lookups.clear() self._save_failed_lookups() logger.info("Icon cache cleared") class IconExporterDialog: """ GUI dialog for exporting icons. (To be integrated with PyQt6) """ def __init__(self, icon_manager: IconManager): self.icon_manager = icon_manager def export_gear_icons(self, weapon_name: str, armor_name: str, export_dir: Path): """Export icons for current gear.""" results = [] if weapon_name: success = self.icon_manager.export_icon( weapon_name, export_dir / "weapon.png", size='large' ) results.append(('weapon', success)) if armor_name: success = self.icon_manager.export_icon( armor_name, export_dir / "armor.png", size='large' ) results.append(('armor', success)) return results def export_all_blueprint_icons(self, blueprints: List[str], export_dir: Path): """Export icons for all crafting blueprints.""" return self.icon_manager.batch_export_icons(blueprints, export_dir, size='medium') # Export main classes __all__ = ['IconManager', 'EntropiaWikiIcons', 'IconExporterDialog']