164 lines
6.4 KiB
PHP
164 lines
6.4 KiB
PHP
<?php
|
|
/**
|
|
* api/artwork.php
|
|
* ---------------
|
|
* Optional album-art lookup. The browser calls this only when the stream
|
|
* provider itself did not supply artwork and ARTWORK_SOURCE allows a lookup.
|
|
* Tries dumb (if USE_DUMB is on), then the official Genius API (if a token
|
|
* is configured), then Last.fm — each tier only runs if the previous one
|
|
* found nothing, so dumb and Genius can both be configured at once. Returns:
|
|
*
|
|
* { "art": "https://…" | "", "album": "…", "url": "…" }
|
|
*
|
|
* With nothing configured it returns empty fields and the front-end falls back
|
|
* to the placeholder image.
|
|
*/
|
|
|
|
require_once __DIR__ . '/_bootstrap.php';
|
|
|
|
$empty = ['art' => '', '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 . ' - ' . $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);
|
|
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]);
|