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
+
+
@@ -3307,6 +3324,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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