Wire Amazon Music enrichment worker into dashboard UI

Adds full parity with Deezer/Qobuz/Tidal/Discogs in every dashboard
UI layer — orb button, live tooltip, WebSocket push, rate speedometer.

- webui/index.html: Amazon enrichment orb button after Discogs
- webui/static/amazon.svg: local icon (a + smile, same pattern as
  hydrabase.png — avoids external URL dependency)
- webui/static/style.css: Amazon button/spinner/tooltip CSS with
  FF9900 brand color; added to mobile tooltip suppress list
- webui/static/worker-orbs.js: Amazon orb in WORKER_DEFS [255,153,0]
- webui/static/api-monitor.js: Amazon in rate gauge services list,
  label, and color map
- webui/static/enrichment.js: updateAmazonEnrichmentStatusFromData,
  toggleAmazonEnrichment, DOMContentLoaded init + 2s poll
- webui/static/core.js: socket.on enrichment:amazon-enrichment listener
- web_server.py: amazon-enrichment added to _emit_enrichment_status_loop
  workers dict so WebSocket pushes fire every 2s
This commit is contained in:
Broque Thomas 2026-05-16 17:43:38 -07:00
parent 4fce832ae1
commit 5450f4ac5e
8 changed files with 320 additions and 1 deletions

View file

@ -33898,6 +33898,7 @@ def _emit_enrichment_status_loop():
'genius-enrichment': lambda: genius_worker,
'tidal-enrichment': lambda: tidal_enrichment_worker,
'qobuz-enrichment': lambda: qobuz_enrichment_worker,
'amazon-enrichment': lambda: amazon_worker,
'hydrabase': lambda: hydrabase_worker,
'soulid': lambda: soulid_worker,
'listening-stats': lambda: listening_stats_worker,

View file

@ -541,6 +541,29 @@
</div>
</div>
</div>
<!-- Amazon Music Enrichment Status Icon -->
<div class="amazon-enrich-button-container">
<button class="amazon-enrich-button" id="amazon-enrich-button" title="Amazon Music Library Enrichment"
onclick="toggleAmazonEnrichment()">
<img src="/static/amazon.svg"
alt="Amazon Music" class="amazon-enrich-logo">
<div class="amazon-enrich-spinner"></div>
</button>
<div class="amazon-enrich-tooltip" id="amazon-enrich-tooltip">
<div class="amazon-enrich-tooltip-content">
<div class="amazon-enrich-tooltip-header">Amazon Music Enrichment</div>
<div class="amazon-enrich-tooltip-body" id="amazon-enrich-tooltip-body">
<div class="tooltip-status">Status: <span
id="amazon-enrich-tooltip-status">Idle</span>
</div>
<div class="tooltip-current" id="amazon-enrich-tooltip-current">No active matches
</div>
<div class="tooltip-progress" id="amazon-enrich-tooltip-progress">Progress: 0 / 0
</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">

5
webui/static/amazon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black">
<text x="12" y="13" text-anchor="middle" font-family="Arial,sans-serif" font-size="11" font-weight="bold" fill="black">a</text>
<path d="M4.5 17 Q12 21 19.5 17" stroke="black" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<path d="M17.2 15 L19.5 17 L17.2 19" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 451 B

View file

@ -4,17 +4,19 @@
const _rateMonitorState = {};
const _RATE_GAUGE_SERVICES = [
'spotify', 'itunes', 'deezer', 'lastfm', 'genius',
'musicbrainz', 'audiodb', 'tidal', 'qobuz', 'discogs',
'musicbrainz', 'audiodb', 'tidal', 'qobuz', 'discogs', 'amazon',
];
const _RATE_GAUGE_LABELS = {
spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer',
lastfm: 'Last.fm', genius: 'Genius', musicbrainz: 'MusicBrainz',
audiodb: 'AudioDB', tidal: 'Tidal', qobuz: 'Qobuz', discogs: 'Discogs',
amazon: 'Amazon Music',
};
const _RATE_GAUGE_COLORS = {
spotify: '#1DB954', itunes: '#FC3C44', deezer: '#A238FF',
lastfm: '#D51007', genius: '#FFFF64', musicbrainz: '#BA478F',
audiodb: '#00BCD4', tidal: '#00FFFF', qobuz: '#FF6B35', discogs: '#D4A574',
amazon: '#FF9900',
};
// SVG constants — 240° arc, gap at bottom

View file

@ -446,6 +446,7 @@ function initializeWebSocket() {
socket.on('enrichment:genius-enrichment', (data) => updateGeniusEnrichmentStatusFromData(data));
socket.on('enrichment:tidal-enrichment', (data) => updateTidalEnrichmentStatusFromData(data));
socket.on('enrichment:qobuz-enrichment', (data) => updateQobuzEnrichmentStatusFromData(data));
socket.on('enrichment:amazon-enrichment', (data) => updateAmazonEnrichmentStatusFromData(data));
socket.on('enrichment:hydrabase', (data) => updateHydrabaseStatusFromData(data));
socket.on('enrichment:repair', (data) => updateRepairStatusFromData(data));
socket.on('enrichment:soulid', (data) => updateSoulIDStatusFromData(data));

View file

@ -1205,6 +1205,112 @@ if (document.readyState === 'loading') {
}
}
// ===================================================================
// AMAZON MUSIC ENRICHMENT WORKER
// ===================================================================
async function updateAmazonEnrichmentStatus() {
if (socketConnected) return;
if (document.hidden) return;
try {
const response = await fetch('/api/enrichment/amazon/status');
if (!response.ok) { console.warn('Amazon enrichment status endpoint unavailable'); return; }
const data = await response.json();
updateAmazonEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Amazon enrichment status:', error);
}
}
function updateAmazonEnrichmentStatusFromData(data) {
const button = document.getElementById('amazon-enrich-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.paused) {
button.classList.add('paused');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('amazon-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('amazon-enrich-tooltip-current');
const tooltipProgress = document.getElementById('amazon-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
} else {
tooltipCurrent.textContent = 'No active matches';
}
}
if (data.progress && tooltipProgress) {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (!albumsComplete && !currentType)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
async function toggleAmazonEnrichment() {
try {
const button = document.getElementById('amazon-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/amazon/pause' : '/api/enrichment/amazon/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Amazon enrichment`);
}
await updateAmazonEnrichmentStatus();
console.log(`Amazon enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Amazon enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('amazon-enrich-button');
if (button) {
button.addEventListener('click', toggleAmazonEnrichment);
updateAmazonEnrichmentStatus();
setInterval(updateAmazonEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('amazon-enrich-button');
if (button) {
button.addEventListener('click', toggleAmazonEnrichment);
updateAmazonEnrichmentStatus();
setInterval(updateAmazonEnrichmentStatus, 2000);
}
}
// ===================================================================
// HYDRABASE P2P MIRROR WORKER
// ===================================================================

View file

@ -9648,6 +9648,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
.tidal-enrich-tooltip,
.qobuz-enrich-tooltip,
.discogs-tooltip,
.amazon-enrich-tooltip,
.hydrabase-tooltip,
.repair-tooltip,
.soulid-tooltip {
@ -36888,6 +36889,185 @@ div.artist-hero-badge {
font-weight: 600;
}
/* ============================================================
AMAZON MUSIC ENRICHMENT WORKER BUTTON
============================================================ */
.amazon-enrich-button-container {
position: relative;
display: inline-block;
margin-right: 12px;
}
.amazon-enrich-button {
position: relative;
width: 44px;
height: 44px;
background: linear-gradient(135deg,
rgba(255, 153, 0, 0.10) 0%,
rgba(255, 120, 0, 0.16) 100%);
backdrop-filter: blur(20px) saturate(1.4);
-webkit-backdrop-filter: blur(20px) saturate(1.4);
border: 1.5px solid rgba(255, 153, 0, 0.22);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 4px 16px rgba(255, 153, 0, 0.12),
0 2px 8px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.amazon-enrich-logo {
width: 24px;
height: 24px;
opacity: 0.6;
filter: brightness(0) invert(1);
transition: all 0.3s ease;
}
.amazon-enrich-button:hover {
transform: scale(1.1);
border-color: rgba(255, 153, 0, 0.45);
box-shadow:
0 6px 24px rgba(255, 153, 0, 0.28),
0 3px 12px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
.amazon-enrich-spinner {
position: absolute;
width: 44px;
height: 44px;
border: 3px solid transparent;
border-top-color: rgba(255, 153, 0, 0.85);
border-right-color: rgba(255, 153, 0, 0.5);
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
@keyframes amazon-enrich-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.amazon-enrich-button.active .amazon-enrich-spinner {
opacity: 1;
animation: amazon-enrich-spin 1.2s linear infinite;
}
.amazon-enrich-button.active {
background: linear-gradient(135deg,
rgba(255, 153, 0, 0.20) 0%,
rgba(255, 100, 0, 0.26) 100%);
border-color: rgba(255, 153, 0, 0.55);
box-shadow:
0 6px 24px rgba(255, 153, 0, 0.35),
0 3px 12px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.amazon-enrich-button.active .amazon-enrich-logo {
opacity: 1;
filter: brightness(0) invert(1) drop-shadow(0 0 6px rgba(255, 180, 50, 0.5));
}
.amazon-enrich-button.complete {
background: linear-gradient(135deg,
rgba(76, 175, 80, 0.15) 0%,
rgba(56, 142, 60, 0.20) 100%);
border-color: rgba(76, 175, 80, 0.35);
box-shadow:
0 4px 16px rgba(76, 175, 80, 0.2),
0 2px 8px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.amazon-enrich-button.complete .amazon-enrich-logo {
opacity: 0.8;
filter: brightness(0) invert(0.6) sepia(1) saturate(3) hue-rotate(80deg);
}
.amazon-enrich-button.complete .amazon-enrich-spinner {
opacity: 0;
}
.amazon-enrich-button.paused {
background: linear-gradient(135deg,
rgba(255, 193, 7, 0.12) 0%,
rgba(255, 152, 0, 0.18) 100%);
border-color: rgba(255, 193, 7, 0.35);
box-shadow:
0 4px 16px rgba(255, 193, 7, 0.2),
0 2px 8px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.amazon-enrich-button.paused .amazon-enrich-logo {
opacity: 0.5;
filter: brightness(0) invert(0.6) sepia(1) saturate(5) hue-rotate(15deg);
}
.amazon-enrich-button.paused .amazon-enrich-spinner {
opacity: 0;
}
.amazon-enrich-tooltip {
position: absolute;
left: 50%;
top: calc(100% + 12px);
transform: translateX(-50%) translateY(-5px);
z-index: 5000;
opacity: 0;
visibility: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.amazon-enrich-button:hover + .amazon-enrich-tooltip {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
.amazon-enrich-tooltip-content {
min-width: 260px;
background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(20, 20, 20, 0.99) 100%);
backdrop-filter: blur(40px) saturate(1.6);
-webkit-backdrop-filter: blur(40px) saturate(1.6);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 16px;
padding: 16px 18px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), 0 6px 20px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.amazon-enrich-tooltip-header {
font-family: 'SF Pro Display', -apple-system, sans-serif;
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.95);
letter-spacing: -0.2px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.amazon-enrich-tooltip-body {
display: flex;
flex-direction: column;
gap: 8px;
}
#amazon-enrich-tooltip-status {
color: #FF9900;
font-weight: 600;
}
.deezer-button-container {
position: relative;
display: inline-block;

View file

@ -21,6 +21,7 @@
{ container: '.tidal-enrich-button-container', color: [180, 180, 255] },
{ container: '.qobuz-enrich-button-container', color: [1, 112, 239] },
{ container: '.discogs-button-container', color: [180, 180, 180] },
{ container: '.amazon-enrich-button-container', color: [255, 153, 0] },
{ container: '.hydrabase-button-container', color: [200, 200, 200] },
{ container: '.soulid-button-container', color: [29, 185, 84], rainbow: true },
{ container: '.repair-button-container', color: [180, 130, 255], rainbow: true },