true, CURLOPT_CONNECTTIMEOUT => (int) $timeout, CURLOPT_TIMEOUT => (int) $timeout, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_USERAGENT => cfg('runtime.user_agent'), ]); if (!empty($headers)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; return $body; } // Fallback without cURL. $ctx = stream_context_create([ 'http' => [ 'timeout' => (int) $timeout, 'header' => implode("\r\n", array_merge(['User-Agent: ' . cfg('runtime.user_agent')], $headers)), 'ignore_errors' => true, ], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $body = @file_get_contents($url, false, $ctx); return ($body === false) ? null : $body; } /** * Read a value out of a decoded-JSON structure using dot notation. * Supports array indexes: "a.b.0.c". */ function dot_get($data, $path, $default = null) { if ($path === '' || $path === null) return $default; $node = $data; foreach (explode('.', $path) as $key) { if (is_array($node) && array_key_exists($key, $node)) { $node = $node[$key]; } elseif (is_object($node) && isset($node->$key)) { $node = $node->$key; } else { return $default; } } return $node; } /** Read the artist/title the browser sends to artwork/lyrics endpoints. */ function request_track() { $artist = $_POST['artist'] ?? $_GET['artist'] ?? ''; $title = $_POST['title'] ?? $_GET['title'] ?? ''; return [trim((string) $artist), trim((string) $title)]; } /** * Return the inner HTML of a balanced
whose opening tag starts at $lt. * Correctly handles nested
elements. Shared by the Genius and dumb * lyrics scrapers (both extract a lyrics container out of a raw HTML page). */ 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; } /** * dumb (https://github.com/rramiachraf/dumb): a self-hosted/public Genius * frontend that server-renders search + song pages. When USE_DUMB is on, * these replace the official Genius API as the lyrics/artwork source (no * GENIUS_TOKEN needed) — same idea as the Genius HTML scrape elsewhere in * this codebase, just against dumb's own pages instead of genius.com. * Fragile by nature (scrapes another project's HTML): if dumb changes its * templates, these regexes may need updating. */ /** Search dumb for a track; returns the first song result's page path (e.g. "/Artist-title-lyrics"), or '' if none. */ function dumb_search($artist, $title, $timeout) { $base = cfg('dumb.url'); if ($base === '') return ''; $query = rawurlencode(trim($artist . ' - ' . $title, ' -')); $html = http_get($base . '/search?q=' . $query, $timeout); if ($html === null) return ''; if (!preg_match('/

\s*Songs\s*<\/h2>.*?]+id="search-item"[^>]+href="([^"]+)"/is', $html, $m)) return ''; return html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** * Fetch + parse a dumb song page ($path from dumb_search()). Returns * ['lyrics' => plain text, 'album' => name, 'art' => absolute album art url, * 'song_art' => absolute song thumbnail url] (any field may be ''), or null * if the page couldn't be fetched. */ function dumb_song($path, $timeout) { $base = cfg('dumb.url'); if ($base === '' || $path === '') return null; $html = http_get($base . $path, $timeout); if ($html === null) return null; $out = ['lyrics' => '', 'album' => '', 'art' => '', 'song_art' => '']; //
wraps the raw Genius lyrics HTML that dumb already // extracted server-side (no data-lyrics-container markers left in it). $idPos = strpos($html, 'id="lyrics"'); if ($idPos !== false) { $lt = strrpos(substr($html, 0, $idPos), ' tags can appear: the song thumbnail // (#metadata, always present) and the album cover (#lyrics-album- // container, only when the song belongs to an album — marked with a // title="Album: …" attribute so the two are distinguishable). if (preg_match_all('/]*>/i', $html, $imgs)) { foreach ($imgs[0] as $img) { if (strpos($img, 'id="album-artwork"') === false) continue; if (!preg_match('/\bsrc="([^"]+)"/i', $img, $src)) continue; $url = $base . html_entity_decode($src[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); if (preg_match('/\btitle="Album:\s*([^"]*)"/i', $img, $t)) { $out['album'] = html_entity_decode($t[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); $out['art'] = $url; } elseif ($out['song_art'] === '') { $out['song_art'] = $url; } } } return $out; }