what the fuck
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 1m55s
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 1m55s
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use App\Repository\SettingRepository;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* SpotifyClient wraps API calls with caching and error handling.
|
||||
*/
|
||||
class SpotifyClient
|
||||
{
|
||||
private HttpClientInterface $httpClient;
|
||||
@@ -14,10 +15,10 @@ class SpotifyClient
|
||||
private ?string $clientId;
|
||||
private ?string $clientSecret;
|
||||
private SettingRepository $settings;
|
||||
private int $rateWindowSeconds;
|
||||
private int $rateMaxRequests;
|
||||
private int $rateMaxRequestsSensitive;
|
||||
|
||||
/**
|
||||
* Builds the client with HTTP, cache, and configuration dependencies.
|
||||
*/
|
||||
public function __construct(
|
||||
HttpClientInterface $httpClient,
|
||||
CacheInterface $cache,
|
||||
@@ -30,10 +31,6 @@ class SpotifyClient
|
||||
$this->clientId = $clientId;
|
||||
$this->clientSecret = $clientSecret;
|
||||
$this->settings = $settings;
|
||||
// Allow tuning via env vars; fallback to conservative defaults
|
||||
$this->rateWindowSeconds = (int) (getenv('SPOTIFY_RATE_WINDOW_SECONDS') ?: 30);
|
||||
$this->rateMaxRequests = (int) (getenv('SPOTIFY_RATE_MAX_REQUESTS') ?: 50);
|
||||
$this->rateMaxRequestsSensitive = (int) (getenv('SPOTIFY_RATE_MAX_REQUESTS_SENSITIVE') ?: 20);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +53,7 @@ class SpotifyClient
|
||||
'headers' => [ 'Authorization' => 'Bearer ' . $accessToken ],
|
||||
'query' => [ 'q' => $query, 'type' => 'album', 'limit' => $limit ],
|
||||
];
|
||||
return $this->sendRequest('GET', $url, $options, 600, false);
|
||||
return $this->sendRequest('GET', $url, $options, 600);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +71,7 @@ class SpotifyClient
|
||||
$url = 'https://api.spotify.com/v1/albums/' . urlencode($albumId);
|
||||
$options = [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken ] ];
|
||||
try {
|
||||
return $this->sendRequest('GET', $url, $options, 3600, false);
|
||||
return $this->sendRequest('GET', $url, $options, 3600);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
@@ -97,19 +94,19 @@ class SpotifyClient
|
||||
'query' => [ 'ids' => implode(',', $albumIds) ],
|
||||
];
|
||||
try {
|
||||
return $this->sendRequest('GET', $url, $options, 3600, false);
|
||||
return $this->sendRequest('GET', $url, $options, 3600);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized request with basic throttling, caching and 429 handling.
|
||||
* Centralized request helper with lightweight caching.
|
||||
*
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function sendRequest(string $method, string $url, array $options, int $cacheTtlSeconds = 0, bool $sensitive = false): array
|
||||
private function sendRequest(string $method, string $url, array $options, int $cacheTtlSeconds = 0): array
|
||||
{
|
||||
$cacheKey = null;
|
||||
if ($cacheTtlSeconds > 0 && strtoupper($method) === 'GET') {
|
||||
@@ -124,63 +121,20 @@ class SpotifyClient
|
||||
}
|
||||
}
|
||||
|
||||
$this->throttle($sensitive);
|
||||
|
||||
$attempts = 0;
|
||||
while (true) {
|
||||
++$attempts;
|
||||
$response = $this->httpClient->request($method, $url, $options);
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status === 429) {
|
||||
$retryAfter = (int) ($response->getHeaders()['retry-after'][0] ?? 1);
|
||||
$retryAfter = max(1, min(30, $retryAfter));
|
||||
sleep($retryAfter);
|
||||
if ($attempts < 3) { continue; }
|
||||
}
|
||||
|
||||
$data = $response->toArray(false);
|
||||
if ($cacheKey && $cacheTtlSeconds > 0 && is_array($data)) {
|
||||
$this->cache->get($cacheKey, function($item) use ($data, $cacheTtlSeconds) {
|
||||
$item->expiresAfter($cacheTtlSeconds);
|
||||
return $data;
|
||||
});
|
||||
}
|
||||
return $data;
|
||||
$response = $this->httpClient->request($method, $url, $options);
|
||||
$data = $response->toArray(false);
|
||||
if ($cacheKey && $cacheTtlSeconds > 0 && is_array($data)) {
|
||||
$this->cache->get($cacheKey, function($item) use ($data, $cacheTtlSeconds) {
|
||||
$item->expiresAfter($cacheTtlSeconds);
|
||||
return $data;
|
||||
});
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function throttle(bool $sensitive): void
|
||||
{
|
||||
$windowKey = $sensitive ? 'spotify_rate_sensitive' : 'spotify_rate';
|
||||
$max = $sensitive ? $this->rateMaxRequestsSensitive : $this->rateMaxRequests;
|
||||
$now = time();
|
||||
$entry = $this->cache->get($windowKey, function($item) use ($now) {
|
||||
$item->expiresAfter($this->rateWindowSeconds);
|
||||
return ['start' => $now, 'count' => 0];
|
||||
});
|
||||
if (!is_array($entry) || !isset($entry['start'], $entry['count'])) {
|
||||
$entry = ['start' => $now, 'count' => 0];
|
||||
}
|
||||
$start = (int) $entry['start'];
|
||||
$count = (int) $entry['count'];
|
||||
$elapsed = $now - $start;
|
||||
if ($elapsed >= $this->rateWindowSeconds) {
|
||||
$start = $now; $count = 0;
|
||||
}
|
||||
if ($count >= $max) {
|
||||
$sleep = max(1, $this->rateWindowSeconds - $elapsed);
|
||||
sleep($sleep);
|
||||
$start = time(); $count = 0;
|
||||
}
|
||||
$count++;
|
||||
$newEntry = ['start' => $start, 'count' => $count];
|
||||
$this->cache->get($windowKey, function($item) use ($newEntry) {
|
||||
$item->expiresAfter($this->rateWindowSeconds);
|
||||
return $newEntry;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a cached access token or refreshes credentials when missing.
|
||||
*/
|
||||
private function getAccessToken(): ?string
|
||||
{
|
||||
return $this->cache->get('spotify_client_credentials_token', function ($item) {
|
||||
|
||||
Reference in New Issue
Block a user