Files
radio/api/lyrics.php
T

132 lines
4.5 KiB
PHP

<?php
/**
* api/lyrics.php
* --------------
* Optional lyrics lookup. Finds the song on Genius, then scrapes the lyric
* containers from the public song page. Returns JSON:
*
* { "found": true|false, "lyrics": "plain text\nwith newlines", "url": "…" }
*
* Lyrics are returned as plain text (tags stripped) so the front-end can render
* them with textContent — no HTML injection from a third-party page.
*
* Note: this scrapes Genius' HTML, which can change at any time. If Genius
* restructures their page the extraction may need updating.
*/
require_once __DIR__ . '/_bootstrap.php';
$empty = ['found' => false, 'lyrics' => '', 'url' => ''];
if (!cfg('lyrics.enabled') || cfg('genius.token') === '') {
json_out($empty);
}
[$artist, $title] = request_track();
if ($artist === '' && $title === '') {
json_out($empty);
}
$token = cfg('genius.token');
$timeout = 5;
$headers = ['Authorization: Bearer ' . $token];
/**
* Return the inner HTML of a balanced <div> whose opening tag starts at $lt.
* Correctly handles nested <div> 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), '</div', 5) === 0) {
$depth--;
if ($depth === 0) return substr($html, $start, $next - $start);
$i = $next + 5;
} elseif (strncasecmp(substr($html, $next, 4), '<div', 4) === 0) {
$depth++;
$i = $next + 4;
} else {
$i = $next + 1;
}
}
return substr($html, $start, max(0, $i - $start));
}
/** Turn one container's HTML into plain text with newlines. */
function container_to_text($fragment) {
// Line breaks -> newlines before stripping tags.
$fragment = preg_replace('#<br\s*/?>#i', "\n", $fragment);
// Drop any stray script/style.
$fragment = preg_replace('#<(script|style)\b[^>]*>.*?</\1>#is', '', $fragment);
// Block-level tags imply a line break.
$fragment = preg_replace('#</(p|div)>#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' => '', 'url' => $pageUrl]);
}
$marker = 'data-lyrics-container="true"';
$pieces = [];
$offset = 0;
while (($mpos = strpos($page, $marker, $offset)) !== false) {
// Backtrack to the start of this opening <div ...> tag.
$lt = strrpos(substr($page, 0, $mpos), '<div');
if ($lt === false) { $offset = $mpos + strlen($marker); continue; }
$inner = inner_balanced_div($page, $lt);
$text = trim(container_to_text($inner));
if ($text !== '') $pieces[] = $text;
$offset = $mpos + strlen($marker);
}
$lyrics = trim(implode("\n\n", $pieces));
// Trim stray indentation left on each line, then collapse blank-line runs.
$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' => true, 'lyrics' => $lyrics, 'url' => $pageUrl]);