diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index f2b07ac3..692249ca 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -19,13 +19,23 @@ const _RATE_GAUGE_COLORS = { amazon: '#FF9900', }; -// 2–3 character abbreviations rendered inside each equalizer bar's -// avatar disc. First-letter alone collided too often (3× "A" services, -// 2× "D"); these read as service shorthand at a glance. -const _RATE_GAUGE_GLYPHS = { - spotify: 'SP', itunes: 'AM', deezer: 'DZ', lastfm: 'LF', genius: 'GN', - musicbrainz: 'MB', audiodb: 'ADB', tidal: 'TD', qobuz: 'QB', discogs: 'DC', - amazon: 'AZ', +// Brand logos rendered inside each equalizer bar's avatar disc. +// Same URLs the header-actions worker-orb buttons use — sourced +// from each service's official press / SVG-repo asset so the row +// reads as branded chips, not anonymous initials. AudioDB ships +// no public logo URL, so it lives as a local static file. +const _RATE_GAUGE_LOGOS = { + spotify: 'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png', + itunes: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/ITunes_logo.svg/960px-ITunes_logo.svg.png', + deezer: 'https://cdn.brandfetch.io/idEUKgCNtu/theme/dark/symbol.svg?c=1bxid64Mup7aczewSAYMX&t=1758260798610', + lastfm: 'https://www.last.fm/static/images/lastfm_avatar_twitter.52a5d69a85ac.png', + genius: 'https://images.genius.com/8ed669cadd956443e29c70361ec4f372.1000x1000x1.png', + musicbrainz:'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/MusicBrainz_Logo_%282016%29.svg/500px-MusicBrainz_Logo_%282016%29.svg.png', + audiodb: '/static/audiodb.png', + tidal: 'https://www.svgrepo.com/show/519734/tidal.svg', + qobuz: 'https://www.svgrepo.com/show/504778/qobuz.svg', + discogs: 'https://www.svgrepo.com/show/305957/discogs.svg', + amazon: '/static/amazon.svg', }; // Per-service display state for the equalizer bars. Holds the last @@ -185,7 +195,8 @@ function _renderEqualizerBars(grid, data) { for (const svc of _RATE_GAUGE_SERVICES) { const accent = _RATE_GAUGE_COLORS[svc] || '#888'; const label = _RATE_GAUGE_LABELS[svc] || svc; - const glyph = _RATE_GAUGE_GLYPHS[svc] || (label[0] || '?').toUpperCase(); + const logoSrc = _RATE_GAUGE_LOGOS[svc] || ''; + const fallbackGlyph = (label[0] || '?').toUpperCase(); const bar = document.createElement('button'); bar.type = 'button'; bar.className = 'rate-eq'; @@ -194,12 +205,22 @@ function _renderEqualizerBars(grid, data) { bar.setAttribute('aria-label', `${label} rate detail`); bar.onclick = () => _openRateModal(svc); // Layout, top-to-bottom: - // - avatar disc (brand glyph) anchored above the track + // - avatar disc (brand logo) anchored above the track — + // ``onerror`` swap to the initial-letter glyph keeps the + // UI legible if a CDN URL ever breaks // - track (with reflection puddle as ::after) holding the // fill / shimmer / peak tip / live count // - meta column (state pill + service name) + const avatar = logoSrc + ? `` + : ``; bar.innerHTML = ` - + ${avatar}
diff --git a/webui/static/audiodb.png b/webui/static/audiodb.png new file mode 100644 index 00000000..fefba792 Binary files /dev/null and b/webui/static/audiodb.png differ diff --git a/webui/static/helper.js b/webui/static/helper.js index f63df992..4f2f7292 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3415,7 +3415,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.6.3': [ { unreleased: true }, - { title: 'Dashboard: enrichment equalizer bars get next-level polish', desc: 'follow-up to the equalizer redesign. four upgrades that take the bars from "nice" to "vibey": (1) every bar now carries a circular brand-color avatar disc above the track with the service\'s 2-3 letter glyph (SP, AM, DZ, LF, GN, MB, ADB, TD, QB, DC, AZ) — disc has its own radial gradient + inner highlight + slow halo pulse when the worker is running, ties the bar visually to its identity; (2) peak-flash detector — when the worker\'s calls/min actually steps upward between updates, the peak tip briefly flares white-hot and the fill flashes brighter for ~650ms, like a hardware VU meter\'s peak-detect LED catching a beat. only fires on real increases above a noise threshold so jitter doesn\'t pulse the bar constantly; (3) rolling number counter — the live count digit-animates from old→new value with easeOutCubic instead of snapping, the same premium-counter feel you\'d see on a fancy dashboard; (4) glass-surface reflection puddle under each bar — soft accent-colored glow whose opacity tracks the real (unclamped) rate, so the row looks like the bars are standing on a polished pane rather than floating in space. puddle ripples on peak-flash too. all four tied together by the same --eq-accent / --eq-glow CSS variables so future tweaks stay coherent.', page: 'dashboard' }, + { title: 'Dashboard: enrichment equalizer bars get next-level polish', desc: 'follow-up to the equalizer redesign. four upgrades that take the bars from "nice" to "vibey": (1) every bar now carries a circular avatar disc above the track loading the service\'s real brand logo (the same images the header-action worker orbs use — Spotify, Apple Music, Deezer, Last.fm, Genius, MusicBrainz, AudioDB, Tidal, Qobuz, Discogs, Amazon Music). Disc has a dark glass backdrop, accent-tinted ring, and slow halo pulse when the worker is running; the brand color shows through as a drop-shadow on the logo + ring tint. Initial-letter fallback kicks in automatically if any CDN URL fails; (2) peak-flash detector — when the worker\'s calls/min actually steps upward between updates, the peak tip briefly flares white-hot and the fill flashes brighter for ~650ms, like a hardware VU meter\'s peak-detect LED catching a beat. only fires on real increases above a noise threshold so jitter doesn\'t pulse the bar constantly; (3) rolling number counter — the live count digit-animates from old→new value with easeOutCubic instead of snapping, the same premium-counter feel you\'d see on a fancy dashboard; (4) glass-surface reflection puddle under each bar — soft accent-colored glow whose opacity tracks the real (unclamped) rate, so the row looks like the bars are standing on a polished pane rather than floating in space. puddle ripples on peak-flash too. all four tied together by the same --eq-accent / --eq-glow CSS variables so future tweaks stay coherent.', page: 'dashboard' }, { title: 'Dashboard: equalizer-bar redesign for the Enrichment Services panel', desc: 'the enrichment rate monitor on the dashboard used a 10-column grid of circular speedometers, which (a) lost symmetry the moment a new service was added — 11 services meant 10 in one row and a lonely orphan in the next, (b) wasted ~80% of pixels on empty gauge arcs showing "0" most of the time. Replaced with a VU-meter / equalizer-bar row: each service is a vertical capsule with the service\'s brand-color fill height tracking calls/min, a continuous shimmer scan + glowing peak tip when active, a slow breathing animation when idle, and a red pulsing variant when rate-limited. Symmetric by design at any service count — the row uses flex with even gaps so new services just slot in. Bar tip + value text adapt color to the service\'s accent. Status pill below each bar (Running / Idle / Paused / Stopped) with its own pulsing dot. Click any bar to open the same detail modal the speedometer used; nothing changed about the underlying data flow.', page: 'dashboard' }, { title: 'Auto-Sync: fix ListenBrainz pipelines stuck on "Refreshing:" for 5+ minutes', desc: 'Auto-Sync pipeline runs against a ListenBrainz playlist (Weekly Jams / Weekly Exploration / Top Discoveries / etc.) would sit on "Refreshing: \'\'" for minutes with zero UI updates before anything else happened. Two real bugs ganging up: (1) the refresh path ran the LB matching engine on every track to produce matched_data — 5+ minutes blocking with no progress emission — and Phase 2 of the pipeline then ran the SAME matching engine on the same tracks via the discovery worker. So LB tracks discovered twice, the first time silently. (2) ListenBrainz manager only exposed an `update_all_playlists` entry-point — refreshing one playlist re-pulled all 12+ cached LB playlists\' details from the API even though we only cared about one. Pipeline now skips the refresh-side discovery (Phase 2 handles it with progress emits every 3s — same pattern that already works for Spotify); refresh uses a new targeted `LBManager.refresh_playlist(mbid)` that hits just the requested playlist. Also killed a silent `except Exception: pass` in the LB adapter that was masking real API failures as stale-cache hits — refresh errors now log with traceback and surface to the run-history error tally. Pinned with 12 new unit tests.', page: 'automations' }, { title: 'Wishlist: harden Spotify backfill so a poisoned track_number can\'t mask a lean album', desc: 'follow-up to the earlier wishlist import-path work. residual per-track wishlist downloads (single tracks from different albums, falling below the album-bundle threshold) were producing folders without a year subfolder whenever the underlying wishlist row had a track_number=1 from an older default — the candidate dispatcher\'s "fall back to Spotify API" branch was gated entirely on track_number being missing, but the same API call was also the only thing that hydrated the lean album_context (release_date / total_tracks / cover art) when the original discovery match came from Deezer\'s search endpoint which doesn\'t carry those fields. so any row whose track_number happened to look "filled" (1 from a default) would short-circuit the API call and the year disappeared from the folder path even though the API knew it. split the two concerns: track_number resolution keeps its track_info → track object → API precedence, but album hydration now runs whenever release_date or total_tracks are missing regardless of where track_number came from. one network round-trip still serves both. lifted into core/downloads/track_metadata_backfill.py with 24 unit tests pinning every branch — including the regression: poisoned default-1 track_number does NOT block album backfill anymore.' }, diff --git a/webui/static/style.css b/webui/static/style.css index a8af4bbc..518fd0b3 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -62289,34 +62289,64 @@ body.reduce-effects .dash-card::after { transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); } -/* Avatar disc — circular brand-color chip above the bar, glyph - * centered. Anchors each capsule to its service so the row reads - * as identifiable rather than 11 anonymous bars. */ +/* Avatar disc — circular chip above the bar carrying the service's + * real brand logo. Dark glass backdrop with a thin accent ring + + * accent-colored drop-shadow halo so the logo gets its breathing + * room without losing the per-service color cue. ``--fallback`` + * variant kicks in via the ```` onerror handler when a CDN URL + * breaks, restoring the accent-gradient + initial-letter look. */ .rate-eq-avatar { - width: 30px; - height: 30px; + width: 34px; + height: 34px; border-radius: 50%; margin: 0 auto; display: flex; align-items: center; justify-content: center; - font-size: 11px; - font-weight: 800; - letter-spacing: 0.04em; - color: rgba(255, 255, 255, 0.95); + background: + radial-gradient(circle at 30% 25%, + rgba(255, 255, 255, 0.08), + rgba(0, 0, 0, 0.35) 70%); + border: 1px solid color-mix(in srgb, var(--eq-accent) 50%, rgba(255, 255, 255, 0.12)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 4px 14px color-mix(in srgb, var(--eq-accent) 30%, transparent), + 0 0 0 0 color-mix(in srgb, var(--eq-accent) 35%, transparent); + transition: box-shadow 0.3s, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + flex: 0 0 auto; + overflow: hidden; + position: relative; +} + +.rate-eq-avatar-logo { + width: 75%; + height: 75%; + object-fit: contain; + object-position: center; + display: block; + /* The brand-color drop-shadow pops the logo against the dark + * disc so even dark / multi-tone logos retain the per-service + * cue without washing out. */ + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55)) + drop-shadow(0 0 4px color-mix(in srgb, var(--eq-accent) 35%, transparent)); +} + +/* Fallback variant — restores the original accent-gradient disc + + * initial-letter glyph when the brand logo URL fails to load. */ +.rate-eq-avatar--fallback { background: radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--eq-accent) 65%, white 35%), var(--eq-accent) 55%, color-mix(in srgb, var(--eq-accent) 55%, black 45%) 100%); - border: 1px solid color-mix(in srgb, var(--eq-accent) 55%, transparent); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.25), - 0 4px 14px color-mix(in srgb, var(--eq-accent) 30%, transparent), - 0 0 0 0 color-mix(in srgb, var(--eq-accent) 35%, transparent); +} + +.rate-eq-avatar-glyph { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + color: rgba(255, 255, 255, 0.95); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); - transition: box-shadow 0.3s, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); - flex: 0 0 auto; } .rate-eq.active .rate-eq-avatar { @@ -62733,7 +62763,8 @@ body.reduce-effects .dash-card::after { .rate-eq-value { font-size: 16px; } .rate-eq-name { font-size: 10px; } .rate-eq-state { font-size: 8px; padding: 1px 6px; } - .rate-eq-avatar { width: 26px; height: 26px; font-size: 10px; } + .rate-eq-avatar { width: 28px; height: 28px; } + .rate-eq-avatar-glyph { font-size: 10px; } } @media (max-width: 720px) { @@ -62750,7 +62781,8 @@ body.reduce-effects .dash-card::after { .dash-card[data-card="enrichment"] .rate-monitor-grid--equalizer { --eq-track-h: 108px; } - .rate-eq-avatar { width: 24px; height: 24px; font-size: 9px; } + .rate-eq-avatar { width: 26px; height: 26px; } + .rate-eq-avatar-glyph { font-size: 9px; } } /* Active downloads container */