[CHORE] Better dumb search #6

Merged
iamdoubz merged 1 commits from chore_better_dumb into main 2026-07-30 18:28:38 -05:00
3 changed files with 105 additions and 21 deletions

No files matched your search

+65 -4
View File
@@ -102,6 +102,46 @@ function request_track() {
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 <div> whose opening tag starts at $lt.
* Correctly handles nested <div> elements. Shared by the Genius and dumb
@@ -154,15 +194,36 @@ function container_to_text($fragment) {
* 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. */
/**
* 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 . ' - ' . $title, ' -'));
$query = rawurlencode(trim($artist . ' - ' . strip_feature_credit($title), ' -'));
$html = http_get($base . '/search?q=' . $query, $timeout);
if ($html === null) return '';
if (!preg_match('/<h2>\s*Songs\s*<\/h2>.*?<a[^>]+id="search-item"[^>]+href="([^"]+)"/is', $html, $m)) return '';
return html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
if (!preg_match('/<h2>\s*Songs\s*<\/h2>(.*?)(?=<h2>|$)/is', $html, $section)) return '';
if (!preg_match_all('/<a[^>]+id="search-item"[^>]+href="([^"]+)"[^>]*>.*?<span>([^<]*)<\/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;
}
/**
+15 -2
View File
@@ -85,11 +85,24 @@ function genius_artwork($artist, $title) {
if ($token === '') return null;
$headers = ['Authorization: Bearer ' . $token];
$query = rawurlencode(trim($artist . ' - ' . $title, ' -'));
$query = rawurlencode(trim($artist . ' - ' . strip_feature_credit($title), ' -'));
$search = http_get('https://api.genius.com/search?q=' . $query, 5, $headers);
if ($search === null) return null;
$hit = dot_get(json_decode($search, true), 'response.hits.0.result', null);
$hits = dot_get(json_decode($search, true), 'response.hits', []);
if (!is_array($hits) || count($hits) === 0) return null;
// Same artist-attribution scan as api/lyrics.php — skip translation-page
// hits filed under a pseudo-artist account, falling back to the raw top
// hit if nothing in the first 5 matches.
$hit = null;
$checked = 0;
foreach ($hits as $h) {
if ($checked++ >= 5) break;
$r = dot_get($h, 'result', null);
if ($r && artist_loosely_matches((string) ($r['artist_names'] ?? ''), $artist)) { $hit = $r; break; }
}
if ($hit === null) $hit = dot_get($hits, '0.result', null);
if (!$hit) return null;
$out = [
+25 -15
View File
@@ -112,7 +112,7 @@ $token = cfg('genius.token');
$headers = ['Authorization: Bearer ' . $token];
/* -- Find the song on Genius ----------------------------------------------- */
$query = rawurlencode(trim($artist . ' - ' . $title, ' -'));
$query = rawurlencode(trim($artist . ' - ' . strip_feature_credit($title), ' -'));
$search = http_get('https://api.genius.com/search?q=' . $query, $timeout, $headers);
if ($search === null) json_out($empty);
@@ -120,23 +120,33 @@ $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', '');
// Scan the first 5 hits for one actually attributed to $artist — skips
// past translation pages, which Genius files under pseudo-artist accounts
// like "Genius Deutsche Übersetzungen" rather than the real artist. Falls
// back to the raw top hit if nothing matches, so an unusual artist-name
// spelling can't regress availability entirely. When prefer_english is on,
// keep scanning matching hits until one is confirmed English.
$pageUrl = '';
$fallbackUrl = (string) dot_get($hits, '0.result.url', '');
$checked = 0;
foreach ($hits as $h) {
if ($checked++ >= 5) break;
if (!artist_loosely_matches((string) dot_get($h, 'result.artist_names', ''), $artist)) continue;
// 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;
}
$hitUrl = (string) dot_get($h, 'result.url', '');
if ($pageUrl === '') $pageUrl = $hitUrl;
if (!cfg('lyrics.prefer_english')) 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 = $hitUrl;
break;
}
}
if ($pageUrl === '') $pageUrl = $fallbackUrl;
if ($pageUrl === '') json_out($empty);
/* -- Scrape the lyric containers ------------------------------------------- */