true, CURLOPT_CONNECTTIMEOUT => (int) $timeout, CURLOPT_TIMEOUT => (int) $timeout, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_USERAGENT => cfg('runtime.user_agent'), ]); if (!empty($headers)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; return $body; } // Fallback without cURL. $ctx = stream_context_create([ 'http' => [ 'timeout' => (int) $timeout, 'header' => implode("\r\n", array_merge(['User-Agent: ' . cfg('runtime.user_agent')], $headers)), 'ignore_errors' => true, ], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $body = @file_get_contents($url, false, $ctx); return ($body === false) ? null : $body; } /** * Read a value out of a decoded-JSON structure using dot notation. * Supports array indexes: "a.b.0.c". */ function dot_get($data, $path, $default = null) { if ($path === '' || $path === null) return $default; $node = $data; foreach (explode('.', $path) as $key) { if (is_array($node) && array_key_exists($key, $node)) { $node = $node[$key]; } elseif (is_object($node) && isset($node->$key)) { $node = $node->$key; } else { return $default; } } return $node; } /** Read the artist/title the browser sends to artwork/lyrics endpoints. */ function request_track() { $artist = $_POST['artist'] ?? $_GET['artist'] ?? ''; $title = $_POST['title'] ?? $_GET['title'] ?? ''; return [trim((string) $artist), trim((string) $title)]; }