313 lines
10 KiB
Python
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']
|