From 527b3f34b1dd11fac927fc81571e661d76783995 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Thu, 12 Feb 2026 19:07:54 +0000 Subject: [PATCH] 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! --- .../EU-Utility/plugins/nexus_search/plugin.py | 426 ++++++++++++------ 1 file changed, 283 insertions(+), 143 deletions(-) diff --git a/projects/EU-Utility/plugins/nexus_search/plugin.py b/projects/EU-Utility/plugins/nexus_search/plugin.py index 78fd1d7..9d05fe5 100644 --- a/projects/EU-Utility/plugins/nexus_search/plugin.py +++ b/projects/EU-Utility/plugins/nexus_search/plugin.py @@ -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,172 +187,229 @@ 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) + # 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 - links_label = QLabel("Quick Links:") - links_label.setStyleSheet("color: #999; margin-top: 10px;") - layout.addWidget(links_label) + links_label = QLabel("Quick:") + links_label.setStyleSheet("color: #666;") + btn_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: + for name, url in [ + ("Market", "/market/exchange"), + ("Items", "/items"), + ("Mobs", "/mobs"), + ]: btn = QPushButton(name) btn.setStyleSheet(""" QPushButton { - background-color: #333; - color: #aaa; - padding: 5px 10px; + background-color: transparent; + color: #4a9eff; border: none; - border-radius: 3px; + padding: 5px; } QPushButton:hover { - background-color: #444; - color: white; + color: #5aafff; + text-decoration: underline; } """) - btn.clicked.connect(lambda checked, u=url: self._open_url(u)) - links_layout.addWidget(btn) + btn.clicked.connect(lambda checked, u=self.base_url + url: webbrowser.open(u)) + btn_layout.addWidget(btn) - links_layout.addStretch() - layout.addLayout(links_layout) + 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" - } - - 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}'") + # 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() - def _on_result_clicked(self, item): - """Handle result click.""" - url = item.data(Qt.ItemDataRole.UserRole) - if url: - self._open_url(url) + def _on_results(self, results, search_type): + """Handle search results.""" + self.current_results = results + + 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): - """Open URL in browser.""" - import webbrowser - webbrowser.open(url) + 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."""