Create karoke style lyrics using LRCLIB library with manual sync ability
This commit is contained in:
1 parent
28d2ede47f
commit
d2e7b7e97d
6 files changed
+149
-23
No files matched your search
+40
-13
@@ -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]);
|
||||
Reference in new issue
Block a user