From 35fd7bb29479b6fe32f30ea5261316a777209823 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sat, 21 Feb 2026 00:32:04 -0800 Subject: [PATCH] Add Hydrabase dev UI and WebSocket support Introduce a developer-only Hydrabase testing UI and backend WebSocket integration. Adds a simple dev-mode toggle (password 'hydratest') and new API endpoints (/api/dev-mode, /api/hydrabase/connect, /api/hydrabase/disconnect, /api/hydrabase/status, /api/hydrabase/send) that use websocket-client to connect/send raw JSON to a Hydrabase instance. Frontend changes include a Hydrabase nav/page, payload editors, response panel, dev-mode UI in Settings, associated JS handlers, CSS styling, and an icon asset. Also add websocket-client to requirements. --- requirements-webui.txt | 5 +- web_server.py | 103 +++++++++++++++++++++++++ webui/index.html | 96 ++++++++++++++++++++++++ webui/static/hydrabase.png | Bin 0 -> 24396 bytes webui/static/script.js | 149 +++++++++++++++++++++++++++++++++++++ webui/static/style.css | 87 ++++++++++++++++++++++ 6 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 webui/static/hydrabase.png diff --git a/requirements-webui.txt b/requirements-webui.txt index 16c3398c..6d07e134 100644 --- a/requirements-webui.txt +++ b/requirements-webui.txt @@ -39,4 +39,7 @@ lrclibapi>=0.3.1 asyncio-mqtt>=0.16.0 # Audio fingerprinting for download verification -pyacoustid>=1.3.0 \ No newline at end of file +pyacoustid>=1.3.0 + +# WebSocket client for Hydrabase connection +websocket-client>=1.7.0 \ No newline at end of file diff --git a/web_server.py b/web_server.py index 3c0113d2..fdf85960 100644 --- a/web_server.py +++ b/web_server.py @@ -2482,6 +2482,109 @@ def handle_settings(): except Exception as e: return jsonify({"error": str(e)}), 500 +dev_mode_enabled = False + +@app.route('/api/dev-mode', methods=['GET', 'POST']) +def handle_dev_mode(): + global dev_mode_enabled + if request.method == 'POST': + data = request.get_json() + if data.get('password') == 'hydratest': + dev_mode_enabled = True + print("๐Ÿ”ง Dev mode activated") + return jsonify({"success": True, "enabled": True}) + return jsonify({"success": False, "error": "Invalid password"}), 401 + return jsonify({"enabled": dev_mode_enabled}) + +# โ”€โ”€ Hydrabase WebSocket Connection โ”€โ”€ +_hydrabase_ws = None +_hydrabase_lock = threading.Lock() + +@app.route('/api/hydrabase/connect', methods=['POST']) +def hydrabase_connect(): + """Connect to a Hydrabase instance via WebSocket.""" + global _hydrabase_ws + if not dev_mode_enabled: + return jsonify({"success": False, "error": "Dev mode not active"}), 403 + data = request.get_json() + url = data.get('url', '').strip() + api_key = data.get('api_key', '').strip() + if not url or not api_key: + return jsonify({"success": False, "error": "URL and API key required"}), 400 + try: + import websocket + with _hydrabase_lock: + # Close existing connection if any + if _hydrabase_ws: + try: + _hydrabase_ws.close() + except: + pass + ws = websocket.create_connection( + url, + header={"x-api-key": api_key}, + timeout=10 + ) + _hydrabase_ws = ws + print(f"๐Ÿงช [Hydrabase] Connected to {url}") + return jsonify({"success": True, "message": "Connected"}) + except Exception as e: + print(f"โš ๏ธ [Hydrabase] Connection failed: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/hydrabase/disconnect', methods=['POST']) +def hydrabase_disconnect(): + """Disconnect from Hydrabase.""" + global _hydrabase_ws + with _hydrabase_lock: + if _hydrabase_ws: + try: + _hydrabase_ws.close() + except: + pass + _hydrabase_ws = None + print("๐Ÿงช [Hydrabase] Disconnected") + return jsonify({"success": True}) + +@app.route('/api/hydrabase/status') +def hydrabase_status(): + """Check if connected to Hydrabase.""" + connected = _hydrabase_ws is not None and _hydrabase_ws.connected + return jsonify({"connected": connected}) + +@app.route('/api/hydrabase/send', methods=['POST']) +def hydrabase_send(): + """Send a raw JSON payload to Hydrabase and return the response.""" + global _hydrabase_ws + if not dev_mode_enabled: + return jsonify({"success": False, "error": "Dev mode not active"}), 403 + if not _hydrabase_ws or not _hydrabase_ws.connected: + return jsonify({"success": False, "error": "Not connected to Hydrabase"}), 400 + data = request.get_json() + payload = data.get('payload') + if not payload: + return jsonify({"success": False, "error": "No payload provided"}), 400 + try: + message = json.dumps(payload) if isinstance(payload, dict) else str(payload) + with _hydrabase_lock: + _hydrabase_ws.send(message) + response = _hydrabase_ws.recv() + try: + result = json.loads(response) + except json.JSONDecodeError: + result = response + print(f"๐Ÿงช [Hydrabase] Sent payload โ€” got response") + return jsonify({"success": True, "data": result}) + except Exception as e: + print(f"โš ๏ธ [Hydrabase] Send failed: {e}") + with _hydrabase_lock: + try: + _hydrabase_ws.close() + except: + pass + _hydrabase_ws = None + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/api/settings/log-level', methods=['GET', 'POST']) def handle_log_level(): """Get or set the application log level""" diff --git a/webui/index.html b/webui/index.html index b1dcf517..bbff5ac4 100644 --- a/webui/index.html +++ b/webui/index.html @@ -58,6 +58,10 @@ โš™๏ธ Settings + @@ -3298,6 +3302,19 @@
logs/app.log
+ + +
+

๐Ÿ”ง Developer Mode

+
+ +
+ + +
+
Inactive
+
+
@@ -3307,6 +3324,85 @@ + + +
+ + + +
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+

API Calls

+ +
+
+

Tracks

+ +
+ +
+ +
+
+

Albums

+ +
+ +
+ +
+
+

Artists

+ +
+ +
+ +
+
+

Discography

+ +
+ +
+ +
+
+

Album Tracks

+ +
+ +
+
+ + +
+

Response

+
API responses will appear here
+
+
+
+ diff --git a/webui/static/hydrabase.png b/webui/static/hydrabase.png new file mode 100644 index 0000000000000000000000000000000000000000..ee796a8bde08ab0af100d83ab8dbf2ae639151e4 GIT binary patch literal 24396 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 YS2xZ!88nRr(f|Me07*qoM6N<$f=iz}$N&HU literal 0 HcmV?d00001 diff --git a/webui/static/script.js b/webui/static/script.js index 0f5dba4b..1f243d48 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -596,6 +596,17 @@ async function loadPageData(pageId) { await loadSettingsData(); await loadQualityProfile(); break; + case 'hydrabase': + // Check connection status on page load + try { + const hsResp = await fetch('/api/hydrabase/status'); + const hsData = await hsResp.json(); + _hydrabaseConnected = hsData.connected; + document.getElementById('hydra-connection-status').textContent = hsData.connected ? 'Connected' : 'Disconnected'; + document.getElementById('hydra-connection-status').style.color = hsData.connected ? '#1ed760' : '#888'; + document.getElementById('hydra-connect-btn').textContent = hsData.connected ? 'Disconnect' : 'Connect'; + } catch (e) {} + break; } } catch (error) { console.error(`Error loading ${pageId} data:`, error); @@ -1851,6 +1862,19 @@ async function loadSettingsData() { console.error('Error loading log level:', error); } + // Check dev mode status + try { + const devResponse = await fetch('/api/dev-mode'); + const devData = await devResponse.json(); + if (devData.enabled) { + document.getElementById('dev-mode-status').textContent = 'Active'; + document.getElementById('dev-mode-status').style.color = '#1ed760'; + document.getElementById('hydrabase-nav').style.display = ''; + } + } catch (error) { + console.error('Error checking dev mode:', error); + } + } catch (error) { console.error('Error loading settings:', error); showToast('Failed to load settings', 'error'); @@ -2199,6 +2223,131 @@ async function saveQualityProfile() { // END QUALITY PROFILE FUNCTIONS // =============================== +async function activateDevMode() { + const password = document.getElementById('dev-mode-password').value; + try { + const response = await fetch('/api/dev-mode', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + const data = await response.json(); + if (data.success) { + document.getElementById('dev-mode-status').textContent = 'Active'; + document.getElementById('dev-mode-status').style.color = '#1ed760'; + document.getElementById('hydrabase-nav').style.display = ''; + document.getElementById('dev-mode-password').value = ''; + showToast('Dev mode activated', 'success'); + } else { + showToast('Invalid password', 'error'); + } + } catch (e) { + showToast('Failed to activate dev mode', 'error'); + } +} + +// โ”€โ”€ Hydrabase Functions โ”€โ”€ + +let _hydrabaseConnected = false; + +async function hydrabaseToggleConnection() { + if (_hydrabaseConnected) { + await hydrabaseDisconnect(); + } else { + await hydrabaseConnect(); + } +} + +async function hydrabaseConnect() { + const url = document.getElementById('hydra-ws-url').value.trim(); + const apiKey = document.getElementById('hydra-api-key').value.trim(); + if (!url || !apiKey) { + showToast('URL and API key required', 'error'); + return; + } + const statusEl = document.getElementById('hydra-connection-status'); + const btn = document.getElementById('hydra-connect-btn'); + statusEl.textContent = 'Connecting...'; + statusEl.style.color = '#f0ad4e'; + try { + const response = await fetch('/api/hydrabase/connect', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, api_key: apiKey }) + }); + const data = await response.json(); + if (data.success) { + _hydrabaseConnected = true; + statusEl.textContent = 'Connected'; + statusEl.style.color = '#1ed760'; + btn.textContent = 'Disconnect'; + showToast('Connected to Hydrabase', 'success'); + } else { + statusEl.textContent = 'Failed'; + statusEl.style.color = '#f44336'; + showToast(data.error || 'Connection failed', 'error'); + } + } catch (e) { + statusEl.textContent = 'Error'; + statusEl.style.color = '#f44336'; + showToast('Connection error', 'error'); + } +} + +async function hydrabaseDisconnect() { + try { + await fetch('/api/hydrabase/disconnect', { method: 'POST' }); + } catch (e) {} + _hydrabaseConnected = false; + document.getElementById('hydra-connection-status').textContent = 'Disconnected'; + document.getElementById('hydra-connection-status').style.color = '#888'; + document.getElementById('hydra-connect-btn').textContent = 'Connect'; + showToast('Disconnected from Hydrabase', 'success'); +} + +async function hydrabaseSendRaw(textareaId) { + const textarea = document.getElementById(textareaId); + const raw = textarea.value.trim(); + if (!raw) { + showToast('Payload is empty', 'error'); + return; + } + if (!_hydrabaseConnected) { + showToast('Not connected to Hydrabase', 'error'); + return; + } + let payload; + try { + payload = JSON.parse(raw); + } catch (e) { + showToast('Invalid JSON payload', 'error'); + return; + } + const responseArea = document.getElementById('hydra-response'); + responseArea.textContent = 'Sending...'; + try { + const response = await fetch('/api/hydrabase/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ payload }) + }); + const data = await response.json(); + if (data.success) { + responseArea.textContent = JSON.stringify(data.data, null, 2); + } else { + responseArea.textContent = 'Error: ' + (data.error || 'Unknown error'); + if (data.error && data.error.includes('Not connected')) { + _hydrabaseConnected = false; + document.getElementById('hydra-connection-status').textContent = 'Disconnected'; + document.getElementById('hydra-connection-status').style.color = '#888'; + document.getElementById('hydra-connect-btn').textContent = 'Connect'; + } + } + } catch (e) { + responseArea.textContent = 'Error: ' + e.message; + } +} + async function saveSettings(quiet = false) { // Validate file organization templates before saving const validationErrors = validateFileOrganizationTemplates(); diff --git a/webui/static/style.css b/webui/static/style.css index fc42b131..6f57b0f1 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -23675,4 +23675,91 @@ body { transform: translateX(-50%); border: 6px solid transparent; border-top-color: rgba(244, 67, 54, 0.3); +} + +/* ===================================== + HYDRABASE PAGE + ===================================== */ + +.hydrabase-container { + display: flex; + gap: 24px; + height: calc(100% - 80px); +} + +.hydrabase-panel { + flex: 1; + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; +} + +.hydrabase-panel h3 { + margin: 0 0 4px 0; + color: #fff; + font-size: 16px; + font-weight: 600; +} + +.hydrabase-card { + background: linear-gradient(135deg, rgba(35, 35, 35, 0.6), rgba(20, 20, 20, 0.8)); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 16px; +} + +.hydrabase-card h4 { + margin: 0 0 10px 0; + color: #ccc; + font-size: 14px; + font-weight: 500; +} + +.hydrabase-input-group { + display: flex; + gap: 8px; +} + +.hydrabase-input-group input { + flex: 1; +} + +.hydrabase-icon { + width: 18px; + height: 18px; + filter: invert(1); + vertical-align: middle; +} + +.hydra-payload { + width: 100%; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 10px; + color: #ccc; + font-family: 'Courier New', monospace; + font-size: 12px; + resize: vertical; + box-sizing: border-box; +} + +.hydra-payload:focus { + border-color: rgba(29, 185, 84, 0.4); + outline: none; +} + +.hydrabase-response-area { + flex: 1; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 16px; + font-family: 'Courier New', monospace; + font-size: 13px; + color: #aaa; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; } \ No newline at end of file