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)]; } /** * Strip a featuring-artist credit ("(feat. X)", "(ft. X)", "(featuring X)", * "(with X)", bracket variants, or an unparenthesized trailing "feat. X") * before it goes into a Genius/dumb search query. * * Genius's canonical song title never includes the featuring credit (it's * separate metadata), but translation/cover pages often spell it out * verbatim in their title — e.g. searching "Havana (feat. Young Thug)" * surfaces only German/French translation pages (titled "... ft. Young * Thug ... (Deutsche Übersetzung)" etc.), while "Havana" alone correctly * finds the original as the first hit. Leaves other qualifiers like * "(Remix)"/"(Live)" alone — those genuinely change which page you want. */ function strip_feature_credit($title) { $title = preg_replace('/[\(\[]\s*(?:feat\.?|ft\.?|featuring|with)\b[^\)\]]*[\)\]]/i', '', $title); $title = preg_replace('/\s+(?:feat\.?|ft\.?|featuring)\b.+$/i', '', $title); return trim(preg_replace('/\s{2,}/', ' ', $title)); } /** * Loose "is this search hit actually by the artist we want" check — * case-insensitive, ignores punctuation/spacing and anything in brackets * (so "Camila Cabello (Ft. Young Thug)" still matches "Camila Cabello"). * An empty side always matches (nothing to compare against). Used to skip * past hits attributed to someone else entirely — most notably Genius's * "Genius Deutsche Übersetzungen" / "Genius traductions françaises" / etc. * pseudo-artist accounts that translation pages get filed under. */ function artist_loosely_matches($hitArtist, $wantArtist) { $norm = function ($s) { $s = preg_replace('/[\(\[][^\)\]]*[\)\]]/', '', (string) $s); $s = preg_replace('/[^\p{L}\p{N}]+/u', '', $s); return strtolower($s); }; $a = $norm($hitArtist); $b = $norm($wantArtist); if ($a === '' || $b === '') return true; return strpos($a, $b) !== false || strpos($b, $a) !== false; } /** * 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 a song result's page path (e.g. * "/Artist-title-lyrics"), or '' if none. Scans the first 5 song hits for * one actually attributed to $artist (see artist_loosely_matches) and * returns that; falls back to the top hit if none match rather than * returning nothing, so an unusual artist-name spelling can't regress * availability entirely. */ function dumb_search($artist, $title, $timeout) { $base = cfg('dumb.url'); if ($base === '') return ''; $query = rawurlencode(trim($artist . ' - ' . strip_feature_credit($title), ' -')); $html = http_get($base . '/search?q=' . $query, $timeout); if ($html === null) return ''; if (!preg_match('/

\s*Songs\s*<\/h2>(.*?)(?=

|$)/is', $html, $section)) return ''; if (!preg_match_all('/]+id="search-item"[^>]+href="([^"]+)"[^>]*>.*?([^<]*)<\/span>/is', $section[1], $hits, PREG_SET_ORDER)) { return ''; } $fallback = ''; $checked = 0; foreach ($hits as $h) { $href = html_entity_decode($h[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); if ($fallback === '') $fallback = $href; if ($checked++ >= 5) break; $hitArtist = html_entity_decode($h[2], ENT_QUOTES | ENT_HTML5, 'UTF-8'); if (artist_loosely_matches($hitArtist, $artist)) return $href; } return $fallback; } /** * Extract Genius annotation ("referent") links out of a raw lyrics HTML * fragment. Returns an ordered list of ['text' => plain annotated lyric * text, 'path' => dumb path to fetch the annotation JSON from]. Matched * structurally — href = "/{id}/{song-slug}/{verse-slug}" — rather than by * CSS class, since Genius's class names are build-hashed (styled-components) * and change on their end without notice. */ function dumb_extract_annotations($fragment) { $out = []; if (!preg_match_all('#]*>(.*?)#is', $fragment, $matches, PREG_SET_ORDER)) { return $out; } foreach ($matches as $m) { $text = trim(container_to_text($m[2])); if ($text === '') continue; $out[] = ['text' => $text, 'path' => $m[1] . '/annotations']; } return $out; } /** * Fetch + parse a dumb song page ($path from dumb_search()). Returns * ['lyrics' => plain text, 'annotations' => [{text, path}, …] (see * dumb_extract_annotations), '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' => '', 'annotations' => [], '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; }