Lemontropia-Suite/modules/icon_manager.py

313 lines
10 KiB
Python

"""
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']