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
Built-in plugin for searching EntropiaNexus via API.
Uses official Nexus API endpoints.
"""
import urllib.request
import urllib.parse
import json
import webbrowser
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QComboBox,
QListWidget, QListWidgetItem, QScrollArea
QListWidget, QListWidgetItem, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
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."""
results_ready = pyqtSignal(list)
results_ready = pyqtSignal(list, str) # results, search_type
error_occurred = pyqtSignal(str)
def __init__(self, query, search_type):
@ -30,51 +114,53 @@ class NexusAPISearchThread(QThread):
def run(self):
"""Perform API search."""
try:
# Nexus API endpoint (inferred from website structure)
base_url = "https://www.entropianexus.com"
results = []
# Map search types to URL paths
type_paths = {
"Items": "items",
"Mobs": "mobs",
"Locations": "locations",
"Blueprints": "blueprints",
"Skills": "skills"
}
if self.search_type == "Items":
# Search exchange items
data = NexusAPIClient.fetch_exchange_items(self.query)
if data:
if isinstance(data, list) and len(data) > 0 and 'name' in data[0]:
# Already filtered items
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
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)
self.results_ready.emit(results, self.search_type)
except Exception as e:
self.error_occurred.emit(str(e))
class NexusSearchPlugin(BasePlugin):
"""Search EntropiaNexus from overlay."""
"""Search EntropiaNexus via API."""
name = "Nexus Search"
version = "1.0.0"
name = "EntropiaNexus"
version = "1.1.0"
author = "ImpulsiveFPS"
description = "Search items, mobs, and more on EntropiaNexus"
description = "Search items, users, and market data via Nexus API"
hotkey = "ctrl+shift+n"
def initialize(self):
"""Setup the plugin."""
self.base_url = "https://www.entropianexus.com"
self.search_thread = None
self.current_results = []
def get_ui(self):
"""Create plugin UI."""
@ -82,21 +168,18 @@ class NexusSearchPlugin(BasePlugin):
layout = QVBoxLayout(widget)
# Title
title = QLabel("🔍 EntropiaNexus Search")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
title = QLabel("🔍 EntropiaNexus")
title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
layout.addWidget(title)
# Search type
type_layout = QHBoxLayout()
type_layout.addWidget(QLabel("Search for:"))
type_layout.addWidget(QLabel("Search:"))
self.search_type = QComboBox()
self.search_type.addItems([
"Items",
"Mobs",
"Locations",
"Blueprints",
"Skills"
"Users",
])
self.search_type.setStyleSheet("""
QComboBox {
@ -104,173 +187,230 @@ class NexusSearchPlugin(BasePlugin):
color: white;
padding: 5px;
border-radius: 4px;
min-width: 100px;
}
""")
type_layout.addWidget(self.search_type)
type_layout.addStretch()
layout.addLayout(type_layout)
# Search input
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Enter search term...")
self.search_input.setStyleSheet("""
QLineEdit {
background-color: #333;
color: white;
padding: 10px;
padding: 8px;
border: 2px solid #555;
border-radius: 4px;
font-size: 14px;
font-size: 13px;
}
QLineEdit:focus {
border-color: #4a9eff;
}
""")
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("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #5aafff;
}
""")
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
results_label = QLabel("Results:")
results_label.setStyleSheet("color: #999; margin-top: 10px;")
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;")
# Status
self.status_label = QLabel("Ready")
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
layout.addWidget(self.status_label)
# Quick links
links_label = QLabel("Quick Links:")
links_label.setStyleSheet("color: #999; margin-top: 10px;")
layout.addWidget(links_label)
links_layout = QHBoxLayout()
links = [
("Nexus Home", "https://www.entropianexus.com"),
("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.setStyleSheet("""
QPushButton {
# 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: 5px 10px;
padding: 8px;
border: none;
border-radius: 3px;
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;
color: white;
}
""")
btn.clicked.connect(lambda checked, u=url: self._open_url(u))
links_layout.addWidget(btn)
open_btn.clicked.connect(self._open_selected)
btn_layout.addWidget(open_btn)
links_layout.addStretch()
layout.addLayout(links_layout)
btn_layout.addStretch()
# Quick links
links_label = QLabel("Quick:")
links_label.setStyleSheet("color: #666;")
btn_layout.addWidget(links_label)
for name, url in [
("Market", "/market/exchange"),
("Items", "/items"),
("Mobs", "/mobs"),
]:
btn = QPushButton(name)
btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #4a9eff;
border: none;
padding: 5px;
}
QPushButton:hover {
color: #5aafff;
text-decoration: underline;
}
""")
btn.clicked.connect(lambda checked, u=self.base_url + url: webbrowser.open(u))
btn_layout.addWidget(btn)
layout.addLayout(btn_layout)
layout.addStretch()
return widget
def _do_search(self):
"""Perform search."""
"""Perform API search."""
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
search_type = self.search_type.currentText()
# Clear previous results
self.results_list.clear()
self.results_table.setRowCount(0)
self.current_results = []
self.status_label.setText("Searching...")
# Build the direct URL
type_paths = {
"Items": "items",
"Mobs": "mobs",
"Locations": "locations",
"Blueprints": "blueprints",
"Skills": "skills"
}
# Start search thread
self.search_thread = NexusSearchThread(query, search_type)
self.search_thread.results_ready.connect(self._on_results)
self.search_thread.error_occurred.connect(self._on_error)
self.search_thread.start()
path = type_paths.get(search_type, "items")
params = urllib.parse.urlencode({'q': query})
url = f"{self.base_url}/{path}?{params}"
def _on_results(self, results, search_type):
"""Handle search results."""
self.current_results = results
# Add result that opens browser
item = QListWidgetItem(f"🔍 Search '{query}' in {search_type}")
item.setData(Qt.ItemDataRole.UserRole, url)
self.results_list.addItem(item)
if not results:
self.status_label.setText("No results found")
return
# Also add direct links to common results
# Populate table
self.results_table.setRowCount(len(results))
for row, item in enumerate(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)
name = item.get('name', 'Unknown')
item_type = item.get('type', 'Item')
self.status_label.setText(f"Found results for '{query}'")
# Price info
buy_price = item.get('buy', [])
sell_price = item.get('sell', [])
def _on_result_clicked(self, item):
"""Handle result click."""
url = item.data(Qt.ItemDataRole.UserRole)
if url:
self._open_url(url)
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"
def _open_url(self, url):
"""Open URL in browser."""
import webbrowser
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 _on_error(self, error):
"""Handle search error."""
self.status_label.setText(f"Error: {error}")
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):
"""Focus search when hotkey pressed."""
if hasattr(self, 'search_input'):