Create karoke style lyrics using LRCLIB library with manual sync ability

This commit is contained in:
iamdoubz committed 2026-07-27 12:06:04 -05:00
1 parent 28d2ede47f
commit d2e7b7e97d
6 files changed
+149 -23

No files matched your search

+40 -13
View File
@@ -6,11 +6,18 @@
* falls back to scraping the public Genius song page if LRCLIB has nothing
* and a Genius token is configured. Returns JSON:
*
* { "found": true|false, "lyrics": "plain text\nwith newlines", "url": "…" }
* {
* "found": true|false,
* "lyrics": "plain text\nwith newlines",
* "synced": [{ "time": 12.34, "text": "…" }, …], // [] when unavailable
* "url": "…"
* }
*
* Lyrics are returned as plain text (tags/timestamps stripped) so the
* front-end can render them with textContent — no HTML injection from a
* third-party page.
* third-party page. `synced` (line-level LRC timestamps, seconds) is only
* populated when LRCLIB has synced lyrics; the Genius fallback has no timing
* data so it's always [] there.
*
* Note: the Genius fallback scrapes HTML, which can change at any time. If
* Genius restructures their page the extraction may need updating.
@@ -18,7 +25,7 @@
require_once __DIR__ . '/_bootstrap.php';
$empty = ['found' => false, 'lyrics' => '', 'url' => ''];
$empty = ['found' => false, 'lyrics' => '', 'synced' => [], 'url' => ''];
if (!cfg('lyrics.enabled')) {
json_out($empty);
@@ -31,6 +38,26 @@ if ($artist === '' && $title === '') {
$timeout = 5;
/**
* Parse an LRC string into [{time, text}, …] sorted by time. A line can carry
* more than one leading "[mm:ss.xx]" tag (repeated text, e.g. a chorus reused
* at several timestamps) — each tag becomes its own entry. Non-timestamp
* metadata lines ("[ar:...]", "[ti:...]", …) don't match and are skipped.
*/
function parse_lrc($lrc) {
$out = [];
foreach (explode("\n", $lrc) as $line) {
if (!preg_match_all('/\[(\d+):(\d+(?:\.\d+)?)\]/', $line, $tags, PREG_SET_ORDER)) continue;
$text = trim((string) preg_replace('/\[\d+:\d+(?:\.\d+)?\]/', '', $line));
if ($text === '') continue;
foreach ($tags as $tag) {
$out[] = ['time' => ((int) $tag[1]) * 60 + (float) $tag[2], 'text' => $text];
}
}
usort($out, function ($a, $b) { return $a['time'] <=> $b['time']; });
return $out;
}
/* -- LRCLIB (primary, no key required) -------------------------------------- */
function lrclib_lyrics($artist, $title, $timeout) {
$url = 'https://lrclib.net/api/search?track_name=' . rawurlencode($title)
@@ -41,17 +68,17 @@ function lrclib_lyrics($artist, $title, $timeout) {
$hits = json_decode($raw, true);
if (!is_array($hits) || count($hits) === 0) return null;
$synced = !empty($hits[0]['syncedLyrics']) ? parse_lrc((string) $hits[0]['syncedLyrics']) : [];
$lyrics = trim((string) ($hits[0]['plainLyrics'] ?? ''));
if ($lyrics === '' && !empty($hits[0]['syncedLyrics'])) {
// No plain text given — strip the leading "[mm:ss.xx] " timestamps.
$lyrics = trim((string) preg_replace('/^\[\d+:\d+\.\d+\]\s*/m', '', (string) $hits[0]['syncedLyrics']));
if ($lyrics === '' && !empty($synced)) {
$lyrics = implode("\n", array_map(function ($l) { return $l['text']; }, $synced));
}
return $lyrics === '' ? null : $lyrics;
return $lyrics === '' ? null : ['lyrics' => $lyrics, 'synced' => $synced];
}
$lrcLyrics = lrclib_lyrics($artist, $title, $timeout);
if ($lrcLyrics !== null) {
json_out(['found' => true, 'lyrics' => $lrcLyrics, 'url' => '']);
$lrc = lrclib_lyrics($artist, $title, $timeout);
if ($lrc !== null) {
json_out(['found' => true, 'lyrics' => $lrc['lyrics'], 'synced' => $lrc['synced'], 'url' => '']);
}
/* -- Genius (fallback) ------------------------------------------------------- */
@@ -132,7 +159,7 @@ if ($pageUrl === '') json_out($empty);
/* -- Scrape the lyric containers ------------------------------------------- */
$page = http_get($pageUrl, $timeout);
if ($page === null) {
json_out(['found' => false, 'lyrics' => '', 'url' => $pageUrl]);
json_out(['found' => false, 'lyrics' => '', 'synced' => [], 'url' => $pageUrl]);
}
$marker = 'data-lyrics-container="true"';
@@ -154,7 +181,7 @@ $lyrics = implode("\n", array_map('trim', explode("\n", $lyrics)));
$lyrics = preg_replace("/\n{3,}/", "\n\n", $lyrics);
if ($lyrics === '') {
json_out(['found' => false, 'lyrics' => '', 'url' => $pageUrl]);
json_out(['found' => false, 'lyrics' => '', 'synced' => [], 'url' => $pageUrl]);
}
json_out(['found' => true, 'lyrics' => $lyrics, 'url' => $pageUrl]);
json_out(['found' => true, 'lyrics' => $lyrics, 'synced' => [], 'url' => $pageUrl]);