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 00000000..ee796a8b Binary files /dev/null and b/webui/static/hydrabase.png differ 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