false, 'lyrics' => '', 'synced' => [], 'url' => '']; if (!cfg('lyrics.enabled')) { json_out($empty); } [$artist, $title] = request_track(); if ($artist === '' && $title === '') { json_out($empty); } $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) . '&artist_name=' . rawurlencode($artist); $raw = http_get($url, $timeout); if ($raw === null) return null; $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($synced)) { $lyrics = implode("\n", array_map(function ($l) { return $l['text']; }, $synced)); } return $lyrics === '' ? null : ['lyrics' => $lyrics, 'synced' => $synced]; } $lrc = lrclib_lyrics($artist, $title, $timeout); if ($lrc !== null) { json_out(['found' => true, 'lyrics' => $lrc['lyrics'], 'synced' => $lrc['synced'], 'url' => '']); } /* -- Genius (fallback) ------------------------------------------------------- */ if (cfg('genius.token') === '') json_out($empty); $token = cfg('genius.token'); $headers = ['Authorization: Bearer ' . $token]; /** * Return the inner HTML of a balanced
whose opening tag starts at $lt. * Correctly handles nested
elements. */ function inner_balanced_div($html, $lt) { $open = strpos($html, '>', $lt); if ($open === false) return ''; $i = $open + 1; $start = $i; $len = strlen($html); $depth = 1; while ($i < $len && $depth > 0) { $next = strpos($html, '<', $i); if ($next === false) break; if (strncasecmp(substr($html, $next, 5), ' newlines before stripping tags. $fragment = preg_replace('##i', "\n", $fragment); // Drop any stray script/style. $fragment = preg_replace('#<(script|style)\b[^>]*>.*?#is', '', $fragment); // Block-level tags imply a line break. $fragment = preg_replace('##i', "\n", $fragment); $text = strip_tags($fragment); $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return $text; } /* -- Find the song on Genius ----------------------------------------------- */ $query = rawurlencode(trim($artist . ' - ' . $title, ' -')); $search = http_get('https://api.genius.com/search?q=' . $query, $timeout, $headers); if ($search === null) json_out($empty); $json = json_decode($search, true); $hits = dot_get($json, 'response.hits', []); if (!is_array($hits) || count($hits) === 0) json_out($empty); $pageUrl = (string) dot_get($hits, '0.result.url', ''); // Optionally prefer the first English-language match (a few extra API calls). if (cfg('lyrics.prefer_english')) { $checked = 0; foreach ($hits as $h) { if ($checked++ >= 5) break; $id = dot_get($h, 'result.id', null); if (!$id) continue; $songRaw = http_get('https://api.genius.com/songs/' . rawurlencode((string) $id), $timeout, $headers); if ($songRaw === null) continue; if (dot_get(json_decode($songRaw, true), 'response.song.language', '') === 'en') { $pageUrl = (string) dot_get($h, 'result.url', $pageUrl); break; } } } if ($pageUrl === '') json_out($empty); /* -- Scrape the lyric containers ------------------------------------------- */ $page = http_get($pageUrl, $timeout); if ($page === null) { json_out(['found' => false, 'lyrics' => '', 'synced' => [], 'url' => $pageUrl]); } $marker = 'data-lyrics-container="true"'; $pieces = []; $offset = 0; while (($mpos = strpos($page, $marker, $offset)) !== false) { // Backtrack to the start of this opening
tag. $lt = strrpos(substr($page, 0, $mpos), ' false, 'lyrics' => '', 'synced' => [], 'url' => $pageUrl]); } json_out(['found' => true, 'lyrics' => $lyrics, 'synced' => [], 'url' => $pageUrl]);