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[^>]*>.*?\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);
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'),