what the fuck
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 1m55s

This commit is contained in:
2025-11-27 20:03:12 +00:00
parent f15d9a9cfd
commit 054e970df9
36 changed files with 1434 additions and 363 deletions

View File

@@ -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) {