'', 'album' => '', 'url' => '']; $source = cfg('artwork.source'); if ($source === 'none' || $source === 'provider') { json_out($empty); } [$artist, $title] = request_track(); if ($artist === '' && $title === '') { json_out($empty); } /* -- Operator-defined overrides (config/overrides.json) -------------------- */ $overrides = []; $overridesFile = __DIR__ . '/../config/overrides.json'; if (is_file($overridesFile)) { $ov = json_decode((string) file_get_contents($overridesFile), true); if (is_array($ov)) $overrides = $ov['artwork'] ?? []; } /** * Does an override rule match this track? A rule may set any of artist / title * / album (all case-insensitive). artist & title are exact matches; album is a * substring match, so "Because the Internet" also catches "...(Deluxe)". An * album rule can only match once $album is known (after the provider lookup), * which is what lets you target one album without hitting the artist's others. */ function override_matches($rule, $artist, $title, $album) { if (isset($rule['artist']) && strcasecmp($rule['artist'], $artist) !== 0) return false; if (isset($rule['title']) && strcasecmp($rule['title'], $title) !== 0) return false; if (isset($rule['album'])) { if ($album === '' || stripos($album, (string) $rule['album']) === false) return false; } return true; } // Fast path: artist/title-only rules match before any API call. (Rules that // target an album can't match yet — the album is still unknown.) foreach ($overrides as $rule) { if (!empty($rule['art']) && !isset($rule['album']) && override_matches($rule, $artist, $title, '')) { json_out(['art' => $rule['art'], 'album' => '', 'url' => '']); } } /* -- 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')) { $res = dumb_artwork($artist, $title); if ($res !== null) return $res; // Nothing on dumb — fall through to the official API below. } $token = cfg('genius.token'); if ($token === '') return null; $headers = ['Authorization: Bearer ' . $token]; $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; $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 = [ 'art' => (string) ($hit['song_art_image_url'] ?? $hit['header_image_url'] ?? ''), 'album' => '', 'url' => (string) ($hit['url'] ?? ''), ]; if (!empty($hit['id'])) { $songRaw = http_get('https://api.genius.com/songs/' . rawurlencode((string) $hit['id']), 5, $headers); if ($songRaw !== null) { $s = dot_get(json_decode($songRaw, true), 'response.song', null); if ($s) { $out['album'] = (string) dot_get($s, 'album.name', ''); $better = (string) ($s['song_art_image_url'] ?? $s['header_image_url'] ?? ''); if ($better !== '') $out['art'] = $better; } } } // Genius' generic default cover counts as "no art". if (strpos($out['art'], 'default_cover_image') !== false) $out['art'] = ''; return ($out['art'] === '' && $out['album'] === '') ? null : $out; } /* -- Last.fm -------------------------------------------------------------- */ function lastfm_artwork($artist, $title) { $key = cfg('artwork.lastfm_key'); if ($key === '' || $artist === '') return null; $url = 'https://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json&autocorrect=1' . '&api_key=' . rawurlencode($key) . '&artist=' . rawurlencode($artist) . '&track=' . rawurlencode($title); $raw = http_get($url, 5); if ($raw === null) return null; $data = json_decode($raw, true); $images = dot_get($data, 'track.album.image', []); $art = ''; if (is_array($images)) { foreach ($images as $img) { // last non-empty = largest if (!empty($img['#text'])) $art = $img['#text']; } } if ($art === '') return null; return ['art' => $art, 'album' => (string) dot_get($data, 'track.album.title', ''), 'url' => '']; } /* -- Try providers in the configured order -------------------------------- */ $order = $source === 'lastfm' ? ['lastfm'] : ($source === 'genius' ? ['genius'] : ['genius', 'lastfm']); $art = ''; $album = ''; $url = ''; foreach ($order as $provider) { $res = ($provider === 'genius') ? genius_artwork($artist, $title) : lastfm_artwork($artist, $title); if ($res === null) continue; if ($album === '' && $res['album'] !== '') $album = $res['album']; if ($res['art'] !== '') { $art = $res['art']; $url = $res['url']; break; } } // Album-level overrides: now that the album is known, swap the art if a rule // targets this album (e.g. a special animated cover for one album only). This // replaces whatever the provider returned, but only for the matching album. foreach ($overrides as $rule) { if (!empty($rule['art']) && isset($rule['album']) && override_matches($rule, $artist, $title, $album)) { $art = $rule['art']; break; } } json_out(['art' => $art, 'album' => $album, 'url' => $url]);