feat: Nexus Search now uses actual API

- Add NexusAPIClient class for API calls
- Use /api/market/exchange for item search
- Use /api/users/search for user search
- Use /api/market/prices/latest for price data
- Results shown in table with Name/Type/Price
- Click to open item on Nexus website

Thanks to EntropiaNexus for the API!
This commit is contained in:
LemonNexus 2026-02-12 19:07:54 +00:00
parent d6a768d83c
commit 527b3f34b1
1 changed files with 283 additions and 143 deletions

View File

@ -2,24 +2,108 @@
EU-Utility - Nexus Search Plugin EU-Utility - Nexus Search Plugin
Built-in plugin for searching EntropiaNexus via API. Built-in plugin for searching EntropiaNexus via API.
Uses official Nexus API endpoints.
""" """
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import json import json
import webbrowser
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QComboBox, QLineEdit, QPushButton, QLabel, QComboBox,
QListWidget, QListWidgetItem, QScrollArea QListWidget, QListWidgetItem, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtCore import Qt, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin from plugins.base_plugin import BasePlugin
class NexusAPISearchThread(QThread): class NexusAPIClient:
"""Client for EntropiaNexus API."""
BASE_URL = "https://www.entropianexus.com"
@classmethod
def fetch_exchange_items(cls, search_query=None):
"""Fetch exchange items from Nexus API."""
try:
url = f"{cls.BASE_URL}/api/market/exchange"
req = urllib.request.Request(
url,
headers={
'Accept': 'application/json',
'Accept-Encoding': 'gzip',
}
)
with urllib.request.urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode('utf-8'))
# Filter by search query if provided
if search_query and data:
search_lower = search_query.lower()
filtered = []
for category in data:
if 'items' in category:
for item in category['items']:
if search_lower in item.get('name', '').lower():
filtered.append(item)
return filtered
return data
except Exception as e:
print(f"API Error: {e}")
return None
@classmethod
def fetch_item_prices(cls, item_ids):
"""Fetch latest prices for items."""
try:
if not item_ids:
return {}
ids_str = ','.join(str(id) for id in item_ids[:100]) # Max 100
url = f"{cls.BASE_URL}/api/market/prices/latest?items={ids_str}"
req = urllib.request.Request(
url,
headers={'Accept': 'application/json'}
)
with urllib.request.urlopen(req, timeout=10) as response:
return json.loads(response.read().decode('utf-8'))
except Exception as e:
print(f"Price API Error: {e}")
return {}
@classmethod
def search_users(cls, query):
"""Search for verified users."""
try:
params = urllib.parse.urlencode({'q': query, 'limit': 10})
url = f"{cls.BASE_URL}/api/users/search?{params}"
req = urllib.request.Request(
url,
headers={'Accept': 'application/json'}
)
with urllib.request.urlopen(req, timeout=10) as response:
return json.loads(response.read().decode('utf-8'))
except Exception as e:
print(f"User Search Error: {e}")
return None
class NexusSearchThread(QThread):
"""Background thread for API searches.""" """Background thread for API searches."""
results_ready = pyqtSignal(list) results_ready = pyqtSignal(list, str) # results, search_type
error_occurred = pyqtSignal(str) error_occurred = pyqtSignal(str)
def __init__(self, query, search_type): def __init__(self, query, search_type):
@ -30,51 +114,53 @@ class NexusAPISearchThread(QThread):
def run(self): def run(self):
"""Perform API search.""" """Perform API search."""
try: try:
# Nexus API endpoint (inferred from website structure) results = []
base_url = "https://www.entropianexus.com"
# Map search types to URL paths if self.search_type == "Items":
type_paths = { # Search exchange items
"Items": "items", data = NexusAPIClient.fetch_exchange_items(self.query)
"Mobs": "mobs", if data:
"Locations": "locations", if isinstance(data, list) and len(data) > 0 and 'name' in data[0]:
"Blueprints": "blueprints", # Already filtered items
"Skills": "skills" results = data[:20] # Limit to 20
} else:
# Full category structure
for category in data:
if 'items' in category:
for item in category['items']:
if self.query.lower() in item.get('name', '').lower():
results.append(item)
if len(results) >= 20:
break
if len(results) >= 20:
break
path = type_paths.get(self.search_type, "items") elif self.search_type == "Users":
# Search users
data = NexusAPIClient.search_users(self.query)
if data:
results = data[:10]
# Construct search URL with query parameter self.results_ready.emit(results, self.search_type)
params = urllib.parse.urlencode({'q': self.query})
url = f"{base_url}/{path}?{params}"
# For now, return the URL to open
# TODO: Implement actual API parsing when Nexus API is available
results = [{
'name': f"Search '{self.query}' on Nexus",
'url': url,
'type': self.search_type
}]
self.results_ready.emit(results)
except Exception as e: except Exception as e:
self.error_occurred.emit(str(e)) self.error_occurred.emit(str(e))
class NexusSearchPlugin(BasePlugin): class NexusSearchPlugin(BasePlugin):
"""Search EntropiaNexus from overlay.""" """Search EntropiaNexus via API."""
name = "Nexus Search" name = "EntropiaNexus"
version = "1.0.0" version = "1.1.0"
author = "ImpulsiveFPS" author = "ImpulsiveFPS"
description = "Search items, mobs, and more on EntropiaNexus" description = "Search items, users, and market data via Nexus API"
hotkey = "ctrl+shift+n" hotkey = "ctrl+shift+n"
def initialize(self): def initialize(self):
"""Setup the plugin.""" """Setup the plugin."""
self.base_url = "https://www.entropianexus.com" self.base_url = "https://www.entropianexus.com"
self.search_thread = None self.search_thread = None
self.current_results = []
def get_ui(self): def get_ui(self):
"""Create plugin UI.""" """Create plugin UI."""
@ -82,21 +168,18 @@ class NexusSearchPlugin(BasePlugin):
layout = QVBoxLayout(widget) layout = QVBoxLayout(widget)
# Title # Title
title = QLabel("🔍 EntropiaNexus Search") title = QLabel("🔍 EntropiaNexus")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
layout.addWidget(title) layout.addWidget(title)
# Search type # Search type
type_layout = QHBoxLayout() type_layout = QHBoxLayout()
type_layout.addWidget(QLabel("Search for:")) type_layout.addWidget(QLabel("Search:"))
self.search_type = QComboBox() self.search_type = QComboBox()
self.search_type.addItems([ self.search_type.addItems([
"Items", "Items",
"Mobs", "Users",
"Locations",
"Blueprints",
"Skills"
]) ])
self.search_type.setStyleSheet(""" self.search_type.setStyleSheet("""
QComboBox { QComboBox {
@ -104,172 +187,229 @@ class NexusSearchPlugin(BasePlugin):
color: white; color: white;
padding: 5px; padding: 5px;
border-radius: 4px; border-radius: 4px;
min-width: 100px;
} }
""") """)
type_layout.addWidget(self.search_type) type_layout.addWidget(self.search_type)
type_layout.addStretch()
layout.addLayout(type_layout)
# Search input # Search input
search_layout = QHBoxLayout()
self.search_input = QLineEdit() self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Enter search term...") self.search_input.setPlaceholderText("Enter search term...")
self.search_input.setStyleSheet(""" self.search_input.setStyleSheet("""
QLineEdit { QLineEdit {
background-color: #333; background-color: #333;
color: white; color: white;
padding: 10px; padding: 8px;
border: 2px solid #555; border: 2px solid #555;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 13px;
} }
QLineEdit:focus { QLineEdit:focus {
border-color: #4a9eff; border-color: #4a9eff;
} }
""") """)
self.search_input.returnPressed.connect(self._do_search) self.search_input.returnPressed.connect(self._do_search)
search_layout.addWidget(self.search_input) type_layout.addWidget(self.search_input, 1)
search_btn = QPushButton("Search") # Search button
search_btn = QPushButton("🔍")
search_btn.setFixedWidth(40)
search_btn.setStyleSheet(""" search_btn.setStyleSheet("""
QPushButton { QPushButton {
background-color: #4a9eff; background-color: #4a9eff;
color: white; color: white;
padding: 10px 20px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
font-weight: bold; font-size: 14px;
} }
QPushButton:hover { QPushButton:hover {
background-color: #5aafff; background-color: #5aafff;
} }
""") """)
search_btn.clicked.connect(self._do_search) search_btn.clicked.connect(self._do_search)
search_layout.addWidget(search_btn) type_layout.addWidget(search_btn)
layout.addLayout(search_layout) layout.addLayout(type_layout)
# Results area # Status
results_label = QLabel("Results:") self.status_label = QLabel("Ready")
results_label.setStyleSheet("color: #999; margin-top: 10px;") self.status_label.setStyleSheet("color: #666; font-size: 11px;")
layout.addWidget(results_label)
self.results_list = QListWidget()
self.results_list.setStyleSheet("""
QListWidget {
background-color: #333;
color: white;
border: 1px solid #555;
border-radius: 4px;
padding: 5px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #444;
}
QListWidget::item:hover {
background-color: #444;
}
QListWidget::item:selected {
background-color: #4a9eff;
}
""")
self.results_list.itemClicked.connect(self._on_result_clicked)
layout.addWidget(self.results_list)
# Status label
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
# Results table
self.results_table = QTableWidget()
self.results_table.setColumnCount(3)
self.results_table.setHorizontalHeaderLabels(["Name", "Type", "Price"])
self.results_table.horizontalHeader().setStretchLastSection(True)
self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.results_table.setStyleSheet("""
QTableWidget {
background-color: #2a2a2a;
color: white;
border: 1px solid #444;
border-radius: 4px;
gridline-color: #444;
}
QTableWidget::item {
padding: 8px;
border-bottom: 1px solid #333;
}
QTableWidget::item:selected {
background-color: #4a9eff;
}
QHeaderView::section {
background-color: #333;
color: #aaa;
padding: 8px;
border: none;
font-weight: bold;
}
""")
self.results_table.cellClicked.connect(self._on_item_clicked)
self.results_table.setMaximumHeight(300)
layout.addWidget(self.results_table)
# Action buttons
btn_layout = QHBoxLayout()
open_btn = QPushButton("🔗 Open on Nexus")
open_btn.setStyleSheet("""
QPushButton {
background-color: #333;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #444;
}
""")
open_btn.clicked.connect(self._open_selected)
btn_layout.addWidget(open_btn)
btn_layout.addStretch()
# Quick links # Quick links
links_label = QLabel("Quick Links:") links_label = QLabel("Quick:")
links_label.setStyleSheet("color: #999; margin-top: 10px;") links_label.setStyleSheet("color: #666;")
layout.addWidget(links_label) btn_layout.addWidget(links_label)
links_layout = QHBoxLayout() for name, url in [
("Market", "/market/exchange"),
links = [ ("Items", "/items"),
("Nexus Home", "https://www.entropianexus.com"), ("Mobs", "/mobs"),
("Items", "https://www.entropianexus.com/items"), ]:
("Mobs", "https://www.entropianexus.com/mobs"),
("Maps", "https://www.entropianexus.com/locations"),
]
for name, url in links:
btn = QPushButton(name) btn = QPushButton(name)
btn.setStyleSheet(""" btn.setStyleSheet("""
QPushButton { QPushButton {
background-color: #333; background-color: transparent;
color: #aaa; color: #4a9eff;
padding: 5px 10px;
border: none; border: none;
border-radius: 3px; padding: 5px;
} }
QPushButton:hover { QPushButton:hover {
background-color: #444; color: #5aafff;
color: white; text-decoration: underline;
} }
""") """)
btn.clicked.connect(lambda checked, u=url: self._open_url(u)) btn.clicked.connect(lambda checked, u=self.base_url + url: webbrowser.open(u))
links_layout.addWidget(btn) btn_layout.addWidget(btn)
links_layout.addStretch() layout.addLayout(btn_layout)
layout.addLayout(links_layout) layout.addStretch()
return widget return widget
def _do_search(self): def _do_search(self):
"""Perform search.""" """Perform API search."""
query = self.search_input.text().strip() query = self.search_input.text().strip()
if not query: if not query or len(query) < 2:
self.status_label.setText("Enter at least 2 characters")
return return
search_type = self.search_type.currentText() search_type = self.search_type.currentText()
# Clear previous results # Clear previous results
self.results_list.clear() self.results_table.setRowCount(0)
self.current_results = []
self.status_label.setText("Searching...") self.status_label.setText("Searching...")
# Build the direct URL # Start search thread
type_paths = { self.search_thread = NexusSearchThread(query, search_type)
"Items": "items", self.search_thread.results_ready.connect(self._on_results)
"Mobs": "mobs", self.search_thread.error_occurred.connect(self._on_error)
"Locations": "locations", self.search_thread.start()
"Blueprints": "blueprints",
"Skills": "skills"
}
path = type_paths.get(search_type, "items")
params = urllib.parse.urlencode({'q': query})
url = f"{self.base_url}/{path}?{params}"
# Add result that opens browser
item = QListWidgetItem(f"🔍 Search '{query}' in {search_type}")
item.setData(Qt.ItemDataRole.UserRole, url)
self.results_list.addItem(item)
# Also add direct links to common results
if search_type == "Items":
# Try to construct direct item URL
item_url = f"{self.base_url}/items/{query.replace(' ', '-')}"
item2 = QListWidgetItem(f"📦 Direct link: {query}")
item2.setData(Qt.ItemDataRole.UserRole, item_url)
self.results_list.addItem(item2)
self.status_label.setText(f"Found results for '{query}'")
def _on_result_clicked(self, item): def _on_results(self, results, search_type):
"""Handle result click.""" """Handle search results."""
url = item.data(Qt.ItemDataRole.UserRole) self.current_results = results
if url:
self._open_url(url) if not results:
self.status_label.setText("No results found")
return
# Populate table
self.results_table.setRowCount(len(results))
for row, item in enumerate(results):
if search_type == "Items":
name = item.get('name', 'Unknown')
item_type = item.get('type', 'Item')
# Price info
buy_price = item.get('buy', [])
sell_price = item.get('sell', [])
if buy_price:
price_text = f"Buy: {buy_price[0].get('price', 'N/A')}"
elif sell_price:
price_text = f"Sell: {sell_price[0].get('price', 'N/A')}"
else:
price_text = "No orders"
self.results_table.setItem(row, 0, QTableWidgetItem(name))
self.results_table.setItem(row, 1, QTableWidgetItem(item_type))
self.results_table.setItem(row, 2, QTableWidgetItem(price_text))
elif search_type == "Users":
name = item.get('name', 'Unknown')
eu_name = item.get('euName', '')
self.results_table.setItem(row, 0, QTableWidgetItem(name))
self.results_table.setItem(row, 1, QTableWidgetItem("User"))
self.results_table.setItem(row, 2, QTableWidgetItem(eu_name or ''))
self.status_label.setText(f"Found {len(results)} results")
def _open_url(self, url): def _on_error(self, error):
"""Open URL in browser.""" """Handle search error."""
import webbrowser self.status_label.setText(f"Error: {error}")
webbrowser.open(url)
def _on_item_clicked(self, row, column):
"""Handle item click."""
if row < len(self.current_results):
item = self.current_results[row]
search_type = self.search_type.currentText()
if search_type == "Items":
item_id = item.get('id')
if item_id:
url = f"{self.base_url}/items/{item_id}"
webbrowser.open(url)
elif search_type == "Users":
user_id = item.get('id')
if user_id:
url = f"{self.base_url}/users/{user_id}"
webbrowser.open(url)
def _open_selected(self):
"""Open selected item in browser."""
selected = self.results_table.selectedItems()
if selected:
row = selected[0].row()
self._on_item_clicked(row, 0)
def on_hotkey(self): def on_hotkey(self):
"""Focus search when hotkey pressed.""" """Focus search when hotkey pressed."""