Add Hydrabase P2P mirror worker
Introduce a Hydrabase P2P mirror worker and integrate it into the web UI and server flows. Adds core/hydrabase_worker.py: a background thread with a capped queue (1000), enqueue API, rate limiting, basic stats (sent/dropped/errors), and logic to send JSON requests over a provided WebSocket (responses received and discarded). Integrates the worker into web_server.py (import, startup init, status/pause/resume endpoints, and enqueues queries from multiple search endpoints when dev mode is enabled). Adds UI elements, JavaScript polling/toggle logic, and CSS styling for a Hydrabase status button in webui (index.html, static/script.js, static/style.css) to display and control worker state.
This commit is contained in:
parent
5f558106bf
commit
49a6c58ea8
5 changed files with 504 additions and 1 deletions
145
core/hydrabase_worker.py
Normal file
145
core/hydrabase_worker.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
"""
|
||||||
|
Hydrabase P2P Mirror Worker
|
||||||
|
|
||||||
|
Background worker that intercepts search queries and mirrors them to the
|
||||||
|
Hydrabase P2P network via WebSocket. Fire-and-forget — responses are received
|
||||||
|
(required by protocol) but discarded. Only processes items when the Hydrabase
|
||||||
|
WebSocket is connected; items are silently dropped when not connected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HydrabaseWorker:
|
||||||
|
def __init__(self, get_ws_and_lock):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
get_ws_and_lock: Callable returning (ws, lock) tuple for the
|
||||||
|
Hydrabase WebSocket connection.
|
||||||
|
"""
|
||||||
|
self.get_ws_and_lock = get_ws_and_lock
|
||||||
|
|
||||||
|
# Worker state
|
||||||
|
self.running = False
|
||||||
|
self.paused = False
|
||||||
|
self.should_stop = False
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
# Queue with cap
|
||||||
|
self.queue = queue.Queue(maxsize=1000)
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
self.stats = {
|
||||||
|
'sent': 0,
|
||||||
|
'dropped': 0,
|
||||||
|
'errors': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
self.running = True
|
||||||
|
self.should_stop = False
|
||||||
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
logger.info("Hydrabase P2P mirror worker started")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
self.should_stop = True
|
||||||
|
self.running = False
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join(timeout=5)
|
||||||
|
logger.info("Hydrabase P2P mirror worker stopped")
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
self.paused = True
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
def enqueue(self, query, query_type):
|
||||||
|
"""Non-blocking enqueue. Drops oldest item if queue is full."""
|
||||||
|
if not query or not self.running:
|
||||||
|
return
|
||||||
|
item = {'query': query, 'type': query_type}
|
||||||
|
try:
|
||||||
|
self.queue.put_nowait(item)
|
||||||
|
except queue.Full:
|
||||||
|
# Drop oldest, then add new
|
||||||
|
try:
|
||||||
|
self.queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.queue.put_nowait(item)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_stats(self):
|
||||||
|
is_actually_running = self.running and (self.thread is not None and self.thread.is_alive())
|
||||||
|
return {
|
||||||
|
'enabled': True,
|
||||||
|
'running': is_actually_running and not self.paused,
|
||||||
|
'paused': self.paused,
|
||||||
|
'queue_size': self.queue.qsize(),
|
||||||
|
'stats': self.stats.copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
while not self.should_stop:
|
||||||
|
try:
|
||||||
|
if self.paused:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Non-blocking dequeue with timeout
|
||||||
|
try:
|
||||||
|
item = self.queue.get(timeout=1)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._process_item(item)
|
||||||
|
time.sleep(0.5) # Rate limit
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in Hydrabase worker loop: {e}")
|
||||||
|
self.stats['errors'] += 1
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
def _process_item(self, item):
|
||||||
|
ws, lock = self.get_ws_and_lock()
|
||||||
|
|
||||||
|
if ws is None:
|
||||||
|
self.stats['dropped'] += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
'request': {
|
||||||
|
'type': item['type'],
|
||||||
|
'query': item['query']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
with lock:
|
||||||
|
if not ws.connected:
|
||||||
|
self.stats['dropped'] += 1
|
||||||
|
return
|
||||||
|
ws.send(payload)
|
||||||
|
ws.recv() # Required by protocol, response discarded
|
||||||
|
self.stats['sent'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Hydrabase send failed: {e}")
|
||||||
|
self.stats['dropped'] += 1
|
||||||
105
web_server.py
105
web_server.py
|
|
@ -71,6 +71,7 @@ from beatport_unified_scraper import BeatportUnifiedScraper
|
||||||
from core.musicbrainz_worker import MusicBrainzWorker
|
from core.musicbrainz_worker import MusicBrainzWorker
|
||||||
from core.audiodb_worker import AudioDBWorker
|
from core.audiodb_worker import AudioDBWorker
|
||||||
from core.deezer_worker import DeezerWorker
|
from core.deezer_worker import DeezerWorker
|
||||||
|
from core.hydrabase_worker import HydrabaseWorker
|
||||||
|
|
||||||
# --- Flask App Setup ---
|
# --- Flask App Setup ---
|
||||||
base_dir = os.path.abspath(os.path.dirname(__file__))
|
base_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
@ -3941,6 +3942,12 @@ def enhanced_search():
|
||||||
|
|
||||||
logger.info(f"Enhanced search initiated for: '{query}'")
|
logger.info(f"Enhanced search initiated for: '{query}'")
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, 'track')
|
||||||
|
hydrabase_worker.enqueue(query, 'album')
|
||||||
|
hydrabase_worker.enqueue(query, 'artist')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Search local database for artists
|
# Search local database for artists
|
||||||
database = get_database()
|
database = get_database()
|
||||||
|
|
@ -5578,6 +5585,10 @@ def get_artist_discography(artist_id):
|
||||||
# Get optional artist name for fallback searches
|
# Get optional artist name for fallback searches
|
||||||
artist_name = request.args.get('artist_name', '')
|
artist_name = request.args.get('artist_name', '')
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled and artist_name:
|
||||||
|
hydrabase_worker.enqueue(artist_name, 'discography')
|
||||||
|
|
||||||
# Determine which source to use
|
# Determine which source to use
|
||||||
spotify_available = spotify_client and spotify_client.is_spotify_authenticated()
|
spotify_available = spotify_client and spotify_client.is_spotify_authenticated()
|
||||||
|
|
||||||
|
|
@ -6792,6 +6803,10 @@ def search_match():
|
||||||
if not query:
|
if not query:
|
||||||
return jsonify({"results": []})
|
return jsonify({"results": []})
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, context)
|
||||||
|
|
||||||
if context == 'artist':
|
if context == 'artist':
|
||||||
# Search for artists
|
# Search for artists
|
||||||
artist_matches = spotify_client.search_artists(query, limit=8)
|
artist_matches = spotify_client.search_artists(query, limit=8)
|
||||||
|
|
@ -16423,6 +16438,10 @@ def search_spotify():
|
||||||
if not query:
|
if not query:
|
||||||
return jsonify({"error": "Query parameter 'q' is required"}), 400
|
return jsonify({"error": "Query parameter 'q' is required"}), 400
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, search_type)
|
||||||
|
|
||||||
# Search using spotify_client
|
# Search using spotify_client
|
||||||
tracks = spotify_client.search_tracks(query, limit=limit)
|
tracks = spotify_client.search_tracks(query, limit=limit)
|
||||||
|
|
||||||
|
|
@ -16456,6 +16475,10 @@ def search_spotify_tracks():
|
||||||
if not query:
|
if not query:
|
||||||
return jsonify({"error": "Query parameter is required"}), 400
|
return jsonify({"error": "Query parameter is required"}), 400
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, 'track')
|
||||||
|
|
||||||
# Search using spotify_client
|
# Search using spotify_client
|
||||||
tracks = spotify_client.search_tracks(query, limit=limit)
|
tracks = spotify_client.search_tracks(query, limit=limit)
|
||||||
|
|
||||||
|
|
@ -16487,6 +16510,10 @@ def search_itunes_tracks():
|
||||||
if not query:
|
if not query:
|
||||||
return jsonify({"error": "Query parameter is required"}), 400
|
return jsonify({"error": "Query parameter is required"}), 400
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, 'track')
|
||||||
|
|
||||||
# Search using iTunes client
|
# Search using iTunes client
|
||||||
itunes_client = iTunesClient()
|
itunes_client = iTunesClient()
|
||||||
tracks = itunes_client.search_tracks(query, limit=limit)
|
tracks = itunes_client.search_tracks(query, limit=limit)
|
||||||
|
|
@ -21804,6 +21831,10 @@ def search_artists_for_playlist():
|
||||||
if not query:
|
if not query:
|
||||||
return jsonify({"success": False, "error": "Query required"}), 400
|
return jsonify({"success": False, "error": "Query required"}), 400
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, 'artist')
|
||||||
|
|
||||||
# Search Spotify for artists
|
# Search Spotify for artists
|
||||||
results = spotify_client.sp.search(q=query, type='artist', limit=10)
|
results = spotify_client.sp.search(q=query, type='artist', limit=10)
|
||||||
|
|
||||||
|
|
@ -26256,6 +26287,76 @@ def deezer_resume():
|
||||||
# ================================================================================================
|
# ================================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================================================
|
||||||
|
# HYDRABASE P2P MIRROR WORKER
|
||||||
|
# ================================================================================================
|
||||||
|
|
||||||
|
# --- Hydrabase Worker Initialization ---
|
||||||
|
hydrabase_worker = None
|
||||||
|
try:
|
||||||
|
def _get_hydrabase_ws_and_lock():
|
||||||
|
return (_hydrabase_ws, _hydrabase_lock)
|
||||||
|
hydrabase_worker = HydrabaseWorker(get_ws_and_lock=_get_hydrabase_ws_and_lock)
|
||||||
|
hydrabase_worker.start()
|
||||||
|
print("✅ Hydrabase P2P mirror worker initialized and started")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Hydrabase worker initialization failed: {e}")
|
||||||
|
hydrabase_worker = None
|
||||||
|
|
||||||
|
# --- Hydrabase Worker API Endpoints ---
|
||||||
|
|
||||||
|
@app.route('/api/hydrabase-worker/status', methods=['GET'])
|
||||||
|
def hydrabase_worker_status():
|
||||||
|
"""Get Hydrabase P2P mirror worker status for UI polling"""
|
||||||
|
try:
|
||||||
|
if hydrabase_worker is None:
|
||||||
|
return jsonify({
|
||||||
|
'enabled': False,
|
||||||
|
'running': False,
|
||||||
|
'paused': False,
|
||||||
|
'queue_size': 0,
|
||||||
|
'stats': {'sent': 0, 'dropped': 0, 'errors': 0}
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
status = hydrabase_worker.get_stats()
|
||||||
|
return jsonify(status), 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting Hydrabase worker status: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/hydrabase-worker/pause', methods=['POST'])
|
||||||
|
def hydrabase_worker_pause():
|
||||||
|
"""Pause Hydrabase P2P mirror worker"""
|
||||||
|
try:
|
||||||
|
if hydrabase_worker is None:
|
||||||
|
return jsonify({'error': 'Hydrabase worker not initialized'}), 400
|
||||||
|
|
||||||
|
hydrabase_worker.pause()
|
||||||
|
logger.info("Hydrabase worker paused via UI")
|
||||||
|
return jsonify({'status': 'paused'}), 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error pausing Hydrabase worker: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/hydrabase-worker/resume', methods=['POST'])
|
||||||
|
def hydrabase_worker_resume():
|
||||||
|
"""Resume Hydrabase P2P mirror worker"""
|
||||||
|
try:
|
||||||
|
if hydrabase_worker is None:
|
||||||
|
return jsonify({'error': 'Hydrabase worker not initialized'}), 400
|
||||||
|
|
||||||
|
hydrabase_worker.resume()
|
||||||
|
logger.info("Hydrabase worker resumed via UI")
|
||||||
|
return jsonify({'status': 'running'}), 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error resuming Hydrabase worker: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
# ================================================================================================
|
||||||
|
# END HYDRABASE P2P MIRROR WORKER
|
||||||
|
# ================================================================================================
|
||||||
|
|
||||||
|
|
||||||
# ================================================================================================
|
# ================================================================================================
|
||||||
# IMPORT / STAGING SYSTEM
|
# IMPORT / STAGING SYSTEM
|
||||||
# ================================================================================================
|
# ================================================================================================
|
||||||
|
|
@ -26430,6 +26531,10 @@ def import_search_albums():
|
||||||
if not query:
|
if not query:
|
||||||
return jsonify({'success': False, 'error': 'Missing query parameter'}), 400
|
return jsonify({'success': False, 'error': 'Missing query parameter'}), 400
|
||||||
|
|
||||||
|
# Mirror to Hydrabase P2P network
|
||||||
|
if hydrabase_worker and dev_mode_enabled:
|
||||||
|
hydrabase_worker.enqueue(query, 'album')
|
||||||
|
|
||||||
limit = min(int(request.args.get('limit', 12)), 50)
|
limit = min(int(request.args.get('limit', 12)), 50)
|
||||||
albums = spotify_client.search_albums(query, limit=limit)
|
albums = spotify_client.search_albums(query, limit=limit)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Hydrabase P2P Mirror Status Icon -->
|
||||||
|
<div class="hydrabase-button-container" id="hydrabase-button-container" style="display: none;">
|
||||||
|
<button class="hydrabase-button" id="hydrabase-button" title="Hydrabase P2P Mirror">
|
||||||
|
<img src="/static/hydrabase.png" alt="Hydrabase" class="hydrabase-worker-logo">
|
||||||
|
<div class="hydrabase-spinner"></div>
|
||||||
|
</button>
|
||||||
|
<!-- Hydrabase Worker Tooltip -->
|
||||||
|
<div class="hydrabase-tooltip" id="hydrabase-tooltip">
|
||||||
|
<div class="hydrabase-tooltip-content">
|
||||||
|
<div class="hydrabase-tooltip-header">Hydrabase P2P Mirror</div>
|
||||||
|
<div class="hydrabase-tooltip-body" id="hydrabase-tooltip-body">
|
||||||
|
<div class="tooltip-status">Status: <span id="hydrabase-tooltip-status">Active</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="import-button" id="import-button" onclick="openImportModal()"
|
<button class="import-button" id="import-button" onclick="openImportModal()"
|
||||||
title="Import Music from Staging">
|
title="Import Music from Staging">
|
||||||
<img src="https://cdn-icons-png.flaticon.com/512/8765/8765164.png" alt="Import"
|
<img src="https://cdn-icons-png.flaticon.com/512/8765/8765164.png" alt="Import"
|
||||||
|
|
|
||||||
|
|
@ -1870,6 +1870,7 @@ async function loadSettingsData() {
|
||||||
document.getElementById('dev-mode-status').textContent = 'Active';
|
document.getElementById('dev-mode-status').textContent = 'Active';
|
||||||
document.getElementById('dev-mode-status').style.color = '#1ed760';
|
document.getElementById('dev-mode-status').style.color = '#1ed760';
|
||||||
document.getElementById('hydrabase-nav').style.display = '';
|
document.getElementById('hydrabase-nav').style.display = '';
|
||||||
|
document.getElementById('hydrabase-button-container').style.display = '';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking dev mode:', error);
|
console.error('Error checking dev mode:', error);
|
||||||
|
|
@ -2236,6 +2237,7 @@ async function activateDevMode() {
|
||||||
document.getElementById('dev-mode-status').textContent = 'Active';
|
document.getElementById('dev-mode-status').textContent = 'Active';
|
||||||
document.getElementById('dev-mode-status').style.color = '#1ed760';
|
document.getElementById('dev-mode-status').style.color = '#1ed760';
|
||||||
document.getElementById('hydrabase-nav').style.display = '';
|
document.getElementById('hydrabase-nav').style.display = '';
|
||||||
|
document.getElementById('hydrabase-button-container').style.display = '';
|
||||||
document.getElementById('dev-mode-password').value = '';
|
document.getElementById('dev-mode-password').value = '';
|
||||||
showToast('Dev mode activated', 'success');
|
showToast('Dev mode activated', 'success');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -35901,6 +35903,76 @@ if (document.readyState === 'loading') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// HYDRABASE P2P MIRROR WORKER
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
async function updateHydrabaseStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/hydrabase-worker/status');
|
||||||
|
if (!response.ok) return;
|
||||||
|
const data = await response.json();
|
||||||
|
const button = document.getElementById('hydrabase-button');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
button.classList.remove('active', 'paused');
|
||||||
|
if (data.running && !data.paused) {
|
||||||
|
button.classList.add('active');
|
||||||
|
} else if (data.paused) {
|
||||||
|
button.classList.add('paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tooltip
|
||||||
|
const statusEl = document.getElementById('hydrabase-tooltip-status');
|
||||||
|
if (statusEl) {
|
||||||
|
if (data.paused) {
|
||||||
|
statusEl.textContent = 'Paused';
|
||||||
|
statusEl.style.color = '#ffc107';
|
||||||
|
} else if (data.running) {
|
||||||
|
statusEl.textContent = 'Active';
|
||||||
|
statusEl.style.color = '#ffffff';
|
||||||
|
} else {
|
||||||
|
statusEl.textContent = 'Stopped';
|
||||||
|
statusEl.style.color = '#ff5252';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently ignore — worker may not be available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleHydrabaseWorker() {
|
||||||
|
const button = document.getElementById('hydrabase-button');
|
||||||
|
if (!button) return;
|
||||||
|
const isRunning = button.classList.contains('active');
|
||||||
|
const endpoint = isRunning ? '/api/hydrabase-worker/pause' : '/api/hydrabase-worker/resume';
|
||||||
|
try {
|
||||||
|
await fetch(endpoint, { method: 'POST' });
|
||||||
|
await updateHydrabaseStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling Hydrabase worker:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Hydrabase UI on page load
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const button = document.getElementById('hydrabase-button');
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', toggleHydrabaseWorker);
|
||||||
|
updateHydrabaseStatus();
|
||||||
|
setInterval(updateHydrabaseStatus, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const button = document.getElementById('hydrabase-button');
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', toggleHydrabaseWorker);
|
||||||
|
updateHydrabaseStatus();
|
||||||
|
setInterval(updateHydrabaseStatus, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// IMPORT / STAGING SYSTEM
|
// IMPORT / STAGING SYSTEM
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
|
||||||
|
|
@ -22662,6 +22662,171 @@ body {
|
||||||
border-bottom: 8px solid rgba(30, 30, 30, 0.98);
|
border-bottom: 8px solid rgba(30, 30, 30, 0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
HYDRABASE P2P MIRROR WORKER BUTTON
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.hydrabase-button-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-worker-logo {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
opacity: 0.85;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.6);
|
||||||
|
border-right-color: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state — white spinning */
|
||||||
|
.hydrabase-button.active .hydrabase-spinner {
|
||||||
|
opacity: 1;
|
||||||
|
animation: hydrabase-spin 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button.active {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.10) 100%);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 0 20px rgba(255, 255, 255, 0.15), inset 0 0 10px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button.active .hydrabase-worker-logo {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paused state — yellow/orange */
|
||||||
|
.hydrabase-button.paused {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 193, 7, 0.18) 0%, rgba(255, 152, 0, 0.10) 100%);
|
||||||
|
border-color: rgba(255, 193, 7, 0.45);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 193, 7, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button.paused .hydrabase-worker-logo {
|
||||||
|
opacity: 0.7;
|
||||||
|
filter: brightness(0) invert(1) sepia(1) saturate(5) hue-rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button.paused .hydrabase-spinner {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hydrabase-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
.hydrabase-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-button-container:hover .hydrabase-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-tooltip-content {
|
||||||
|
min-width: 180px;
|
||||||
|
background: rgba(30, 30, 30, 0.98);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-tooltip-header {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-tooltip-body {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-tooltip-content::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -8px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 8px solid rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrabase-tooltip-content::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -7px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 8px solid rgba(30, 30, 30, 0.98);
|
||||||
|
}
|
||||||
|
|
||||||
/* MusicBrainz Integration */
|
/* MusicBrainz Integration */
|
||||||
.release-card {
|
.release-card {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue