feat: synced lyrics display from LRCLib

music-light fetches synced LRC lyrics from lrclib.net on track change,
tracks playback position via a 0.5s background thread, and exposes the
current lyric line in /current. dancer.html polls /current at 700ms and
displays the line bottom-left with a cyan glow, subtitle style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ltadeu6 2026-06-07 13:50:04 -03:00
parent 99851a2ee7
commit 347b3729c8
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D

View file

@ -236,6 +236,32 @@ html, body {
100% { transform: translateY(-110px) rotate(18deg) scale(1.3); opacity: 0; }
}
/* ============================================================
LYRICS DISPLAY
============================================================ */
.lyric-display {
position: absolute;
left: 3%;
bottom: 16%;
max-width: 36%;
pointer-events: none;
opacity: 0;
transition: opacity 0.5s ease;
}
.lyric-line {
color: #ffffff;
font-family: 'Courier New', monospace;
font-size: clamp(13px, 1.5vw, 20px);
line-height: 1.5;
letter-spacing: 0.03em;
text-shadow:
0 0 18px rgba(0,200,255,0.85),
0 0 6px rgba(0,200,255,0.5),
0 2px 10px rgba(0,0,0,0.95),
-1px -1px 0 rgba(0,0,0,0.7),
1px 1px 0 rgba(0,0,0,0.7);
}
/* ============================================================
SCANLINES OVERLAY
============================================================ */
@ -861,6 +887,11 @@ html, body {
</div>
</div>
<!-- Lyrics display -->
<div class="lyric-display" id="lyricDisplay">
<div class="lyric-line" id="lyricLine"></div>
</div>
<!-- Scanlines overlay -->
<div class="scanlines"></div>
@ -1125,10 +1156,13 @@ function applyTheme(rgb) {
const albumArtEl = document.getElementById('albumArt');
const fogWisps = document.querySelectorAll('.fog-wisp');
const lyricDisplay = document.getElementById('lyricDisplay');
const lyricLineEl = document.getElementById('lyricLine');
let lastArtUrl = '';
let lastLyricLine = '';
function fetchTheme() {
function fetchCurrent() {
fetch('http://127.0.0.1:8765/current')
.then(r => r.json())
.then(d => {
@ -1138,10 +1172,18 @@ function fetchTheme() {
document.getElementById('albumArtImg').src =
'http://127.0.0.1:8765/art?' + Date.now();
}
const line = d.lyric_line || '';
if (line !== lastLyricLine) {
lastLyricLine = line;
lyricLineEl.textContent = line;
}
lyricDisplay.style.opacity = (isPlaying && line) ? '1' : '0';
})
.catch(() => {});
}
setInterval(() => { if (isPlaying) fetchCurrent(); }, 700);
// Connect to CAVA SSE server (started by cava-start.sh via QML)
(function connectCava() {
const sse = new EventSource('http://127.0.0.1:5555');
@ -1196,7 +1238,7 @@ function updateMusic(artist, title) {
document.querySelector('.music-ticker').style.opacity = playing ? '1' : '0';
albumArtEl.style.opacity = playing ? '1' : '0';
if (!playing) vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; });
if (playing) fetchTheme();
if (playing) fetchCurrent();
const key = artist + '|' + title;
if (el.dataset.key === key) return;
el.dataset.key = key;