From 12bc10449e74ead37213d87e84f57dc6548e4c19 Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Thu, 30 Jul 2026 14:24:48 -0500 Subject: [PATCH] Add dumb: a privacy focused genius frontend alternative --- .env.example | 12 ++++- api/_bootstrap.php | 106 +++++++++++++++++++++++++++++++++++++++++++++ api/artwork.php | 14 ++++++ api/lyrics.php | 58 +++++++------------------ config.php | 11 ++++- 5 files changed, 155 insertions(+), 46 deletions(-) diff --git a/.env.example b/.env.example index 32d31c4..ecac345 100644 --- a/.env.example +++ b/.env.example @@ -141,12 +141,20 @@ LYRICS_PREFER_ENGLISH=false # Genius API token — optional, only used as a lyrics fallback when LRCLIB has # nothing (still used as the primary/only source for album art lookups). -# Get one at https://genius.com/api-clients (Client Access Token). -# Provide EITHER the plain token... +# Ignored when USE_DUMB is on below. Get one at https://genius.com/api-clients +# (Client Access Token). Provide EITHER the plain token... GENIUS_TOKEN="" # ...OR a base64-encoded token (kept for compatibility with the original project). GENIUS_API_BASE64="" +# dumb (https://github.com/rramiachraf/dumb): a lightweight Genius frontend/ +# proxy. When on, it replaces the official Genius API as the lyrics/artwork +# source above — no GENIUS_TOKEN needed. Point it at a public instance (see +# https://github.com/rramiachraf/dumb#public-instances, e.g. +# https://dumb.ducks.party) or your own self-hosted one. +USE_DUMB=false +DUMB_URL="" + # ----------------------------------------------------------------------------- # Theme & colors (also drives CSS variables — restyle without editing CSS) diff --git a/api/_bootstrap.php b/api/_bootstrap.php index e142d20..f77e83f 100644 --- a/api/_bootstrap.php +++ b/api/_bootstrap.php @@ -101,3 +101,109 @@ function request_track() { $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; +} diff --git a/api/artwork.php b/api/artwork.php index bfe0b28..4131ad9 100644 --- a/api/artwork.php +++ b/api/artwork.php @@ -59,8 +59,22 @@ foreach ($overrides as $rule) { } } +/* -- dumb (Genius-API-free) ------------------------------------------------ */ +function dumb_artwork($artist, $title) { + $path = dumb_search($artist, $title, 5); + if ($path === '') return null; + $song = dumb_song($path, 5); + if ($song === null) return null; + + $art = $song['art'] !== '' ? $song['art'] : $song['song_art']; + if ($art === '' && $song['album'] === '') return null; + return ['art' => $art, 'album' => $song['album'], 'url' => cfg('dumb.url') . $path]; +} + /* -- Genius --------------------------------------------------------------- */ function genius_artwork($artist, $title) { + if (cfg('dumb.enabled')) return dumb_artwork($artist, $title); + $token = cfg('genius.token'); if ($token === '') return null; $headers = ['Authorization: Bearer ' . $token]; diff --git a/api/lyrics.php b/api/lyrics.php index d78b731..e2d2270 100644 --- a/api/lyrics.php +++ b/api/lyrics.php @@ -3,8 +3,9 @@ * api/lyrics.php * -------------- * Lyrics lookup. Tries LRCLIB first (free, no key, has synced lyrics), then - * falls back to scraping the public Genius song page if LRCLIB has nothing - * and a Genius token is configured. Returns JSON: + * falls back to dumb (if USE_DUMB is on) or scraping the public Genius song + * page (if a Genius token is configured) when LRCLIB has nothing. Returns + * JSON: * * { * "found": true|false, @@ -81,53 +82,24 @@ if ($lrc !== null) { json_out(['found' => true, 'lyrics' => $lrc['lyrics'], 'synced' => $lrc['synced'], 'url' => '']); } +/* -- 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]); + } + json_out(['found' => true, 'lyrics' => $song['lyrics'], 'synced' => [], 'url' => $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); diff --git a/config.php b/config.php index 07871a1..503f3cf 100644 --- a/config.php +++ b/config.php @@ -197,6 +197,15 @@ if (!defined('RADIO_CONFIG_LOADED')) { return radio_env('GENIUS_TOKEN', ''); })(), ], + // ---- dumb (https://github.com/rramiachraf/dumb) ---------------- + // A self-hosted or public Genius frontend/proxy. When enabled, it + // replaces the official Genius API as the lyrics/artwork source (no + // GENIUS_TOKEN needed) by scraping its server-rendered pages instead. + // Public instances: https://github.com/rramiachraf/dumb#public-instances + 'dumb' => [ + 'url' => rtrim(radio_env('DUMB_URL', ''), '/'), + 'enabled' => radio_env_bool('USE_DUMB', false) && radio_env('DUMB_URL', '') !== '', + ], // ---- Theme & colors ------------------------------------------- 'theme' => [ @@ -347,7 +356,7 @@ function radio_waveform_active() { * Emitted as `window.RADIO_CONFIG` by the page. */ function radio_client_config() { - $artworkAvailable = cfg('genius.token') !== '' || cfg('artwork.lastfm_key') !== ''; + $artworkAvailable = cfg('genius.token') !== '' || cfg('artwork.lastfm_key') !== '' || cfg('dumb.enabled'); return [ 'pollSeconds' => cfg('nowplaying.poll'), 'artworkSource' => cfg('artwork.source'),