Lyrics and karaoke for the music player
build-container / build (push) Successful in 12m8s

- full-screen lyrics view that follows the song: highlighted current line with a colour sweep (word by word when the source has word timing), tap a line to jump, text size, timing nudge, 'Wrong lyrics?' search
- Karaoke mode: the current line large and centered
- lyrics from Plex first, then the bundled Lyrica service (LRCLIB, Musixmatch via Lrcmux, NetEase, Megalobiz); YouTube captions never used; wrong-length recordings refused; answers kept in /data/lyrics
- lyrica/: Lyrica pinned to one commit, built by the workflow as hub-lyrica; compose service sharing the hub's network
- tools/lyrics-test.js

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 11:13:00 -04:00
1 parent bfd37967c3
commit fc0ae05415
10 files changed
+397 -10

No files matched your search

+36
View File
@@ -446,3 +446,39 @@ body:not(.editing) #view { padding-top: 8px; }
.npc-text { align-self: center; } .npc-text b { font-size: 16px; }
.npc-ctl { justify-content: space-between; } .npc-q { display: none; }
}
/* Lyrics and karaoke */
body.lyropen { overflow: hidden; }
#lyr { position: fixed; inset: 0; z-index: 18; background: var(--bg); display: flex; flex-direction: column; padding-top: var(--sat); overflow: hidden; }
.lyrbg { position: absolute; inset: -40px; background-size: cover; background-position: center; filter: blur(46px) saturate(1.3); opacity: .35; pointer-events: none; }
#lyr > * { position: relative; }
.lyrtop { display: flex; align-items: center; gap: 10px; padding: 10px 16px; }
.lyrart { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; background: var(--panel2); flex: none; }
.lyrtop .grow { min-width: 0; } .lyrtop b, .lyrtop small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .lyrtop small { color: var(--muted); font-size: 12px; }
.lyrtop .ibtn { font-weight: 700; font-size: 15px; }
.lyrmode[aria-pressed="true"] { background: var(--accent); color: var(--accent-t); }
.lyrbody { flex: 1; overflow-y: auto; padding: 24vh 20px 30vh; text-align: center; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.lyrbody::-webkit-scrollbar { display: none; }
.lyrbody .empty { padding-top: 10vh; }
.ll { font-size: var(--lf, 30px); font-weight: 700; line-height: 1.3; margin: 0 auto 18px; max-width: 900px; color: var(--text); opacity: .32; transition: opacity .3s, transform .3s; transform-origin: center; cursor: pointer; }
.ll.past { opacity: .22; }
.ll.on { opacity: 1; transform: scale(1.04); }
.lgap { height: 24px; }
.ll .lt { color: inherit; }
.ll.on .lt { background: linear-gradient(90deg, var(--accent) calc(var(--p, 0) * 100%), var(--text) calc(var(--p, 0) * 100%)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.ll.on .lt:has(.w) { background: none; color: inherit; }
.ll .w { color: inherit; }
.ll.on .w { background: linear-gradient(90deg, var(--accent) calc(var(--p, 0) * 100%), var(--text) calc(var(--p, 0) * 100%)); -webkit-background-clip: text; background-clip: text; color: transparent; white-space: pre-wrap; }
.ll.on .w.sung { background: none; color: var(--accent); }
#lyr.kar .lyrbody { display: flex; flex-direction: column; justify-content: center; padding: 8vh 24px; overflow: hidden; }
#lyr.kar .ll { display: none; font-size: calc(var(--lf, 30px) * 1.7); opacity: .5; }
#lyr.kar .ll.on { display: block; opacity: 1; transform: none; }
#lyr.kar .ll.upnext { display: block; font-size: calc(var(--lf, 30px) * 1.05); opacity: .45; margin-top: 8px; }
#lyr.kar .ll.past, #lyr.kar .lgap { display: none; }
.lyrfoot { display: flex; align-items: center; gap: 6px; padding: 10px 16px calc(12px + var(--sab)); border-top: 1px solid var(--line); background: color-mix(in srgb, var(--bg) 80%, transparent); }
.lyrfoot .grow { flex: 1; }
.lyrnudge { display: inline-flex; align-items: center; gap: 2px; color: var(--muted); } .lyrnudge[hidden] { display: none; } .lyrnudge .ibtn { width: 36px; height: 36px; font-size: 20px; }
.lyrfoot .ibtn.big { width: 56px; height: 56px; }
.npc-l { min-height: 38px; font-size: 14px; }
@media (max-width: 560px) { .lyrtop .lyrmode { display: none; } .lyrfoot #lyrs { padding: 0 10px; font-size: 13px; } .npc-l { display: inline-flex; } .npc-ctl { gap: 0; } .npc-q { display: none; } }
.lyrnudge small, .lyrfoot #lyrs { white-space: nowrap; }