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' => '', 'source' => 'lrclib']); } /* -- dumb (Genius-API-free fallback) ----------------------------------------- */ if (cfg('dumb.enabled')) { $path = dumb_search($artist, $title, $timeout); if ($path === '') json_out($empty); $song = dumb_song($path, $timeout); $url = cfg('dumb.url') . $path; if ($song === null || $song['lyrics'] === '') { json_out(['found' => false, 'lyrics' => '', 'synced' => [], 'url' => $url, 'source' => 'dumb']); } json_out(['found' => true, 'lyrics' => $song['lyrics'], 'synced' => [], 'url' => $url, 'source' => 'dumb']); } /* -- Genius (fallback) ------------------------------------------------------- */ if (cfg('genius.token') === '') json_out($empty); $token = cfg('genius.token'); $headers = ['Authorization: Bearer ' . $token]; /* -- 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, 'source' => 'genius']); } $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, 'source' => 'genius']); } json_out(['found' => true, 'lyrics' => $lyrics, 'synced' => [], 'url' => $pageUrl, 'source' => 'genius']);