true, CURLOPT_POSTFIELDS => http_build_query($fields), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 12, CURLOPT_HTTPHEADER => array_merge([ 'Content-Type: application/x-www-form-urlencoded' ], $headers), ]); $raw = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($raw === false) return ['ok'=>false,'code'=>$code,'error'=>$err ?: 'curl_error','data'=>null]; $data = json_decode($raw, true); return ['ok'=>($code >= 200 && $code < 300),'code'=>$code,'error'=>$err ?: null,'data'=>$data]; } function spotify_http_get(string $url, string $accessToken): array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Accept: application/json', ], ]); $raw = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($raw === false) return ['ok'=>false,'code'=>$code,'error'=>$err ?: 'curl_error','data'=>null]; $data = ($raw !== '' ? json_decode($raw, true) : null); return ['ok'=>($code >= 200 && $code < 300),'code'=>$code,'error'=>$err ?: null,'data'=>$data]; } function spotify_save_tokens(?string $refresh, ?string $access, int $expiresIn): void { $expiresAt = time() + max(0, $expiresIn) - 30; // safety margin $stmt = pdo()->prepare(" UPDATE spotify_tokens SET refresh_token = COALESCE(?, refresh_token), access_token = ?, access_expires = ?, updated_at = ? WHERE id = 1 "); $stmt->execute([$refresh, $access, $expiresAt, time()]); } function spotify_get_stored_row(): ?array { try { $row = pdo()->query("SELECT refresh_token, access_token, access_expires FROM spotify_tokens WHERE id=1")->fetch(); return $row ?: null; } catch (Throwable $e) { return null; } } function spotify_exchange_code_for_tokens(string $code): bool { $cid = spotify_client_id(); $sec = spotify_client_secret(); $redir = spotify_redirect_uri(); if ($cid === '' || $sec === '' || $redir === '') return false; $basic = base64_encode($cid . ':' . $sec); $res = spotify_http_post( 'https://accounts.spotify.com/api/token', [ 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => $redir, ], ['Authorization: Basic ' . $basic] ); if (!$res['ok'] || !is_array($res['data'])) return false; $access = (string)($res['data']['access_token'] ?? ''); $refresh = (string)($res['data']['refresh_token'] ?? ''); $expires = (int)($res['data']['expires_in'] ?? 0); if ($access === '' || $refresh === '' || $expires <= 0) return false; spotify_save_tokens($refresh, $access, $expires); return true; } function spotify_refresh_access_token(): ?string { $row = spotify_get_stored_row(); $refresh = (string)($row['refresh_token'] ?? ''); if ($refresh === '') return null; $cid = spotify_client_id(); $sec = spotify_client_secret(); if ($cid === '' || $sec === '') return null; $basic = base64_encode($cid . ':' . $sec); $res = spotify_http_post( 'https://accounts.spotify.com/api/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $refresh, ], ['Authorization: Basic ' . $basic] ); if (!$res['ok'] || !is_array($res['data'])) return null; $access = (string)($res['data']['access_token'] ?? ''); $expires = (int)($res['data']['expires_in'] ?? 0); if ($access === '' || $expires <= 0) return null; // refresh token usually not returned here; keep existing one spotify_save_tokens(null, $access, $expires); return $access; } function spotify_get_access_token(): ?string { $row = spotify_get_stored_row(); if (!$row) return null; $access = (string)($row['access_token'] ?? ''); $exp = (int)($row['access_expires'] ?? 0); if ($access !== '' && $exp > time() + 30) return $access; return spotify_refresh_access_token(); }