summaryrefslogtreecommitdiff
path: root/src/TokenManager.php
diff options
context:
space:
mode:
authorDavid T. Sadler <davidtsadler@googlemail.com>2026-08-09 20:04:39 +0100
committerDavid T. Sadler <davidtsadler@googlemail.com>2026-08-09 20:04:39 +0100
commitb9c4f1dcf1333c14e2d1b67561ec7a5f955baa6e (patch)
tree610536953be739d0778f19c90c477f080a24718a /src/TokenManager.php
Initial commitHEADmain
Diffstat (limited to 'src/TokenManager.php')
-rw-r--r--src/TokenManager.php179
1 files changed, 179 insertions, 0 deletions
diff --git a/src/TokenManager.php b/src/TokenManager.php
new file mode 100644
index 0000000..96b3969
--- /dev/null
+++ b/src/TokenManager.php
@@ -0,0 +1,179 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TCB;
+
+use TCB\Credentials;
+use TCB\HttpServer;
+use TCB\Request;
+use TCB\Response;
+use TCB\TokenStorage;
+
+final class TokenManager
+{
+ private string $authorisationCode;
+
+ public function __construct(
+ private readonly Credentials $credentials,
+ private readonly HttpServer $httpServer,
+ private readonly TokenStorage $storage,
+ ) {}
+
+ public function getAccessToken(): string
+ {
+ if ($this->storage->accessToken === '' || $this->storage->refreshToken === '') {
+ $this->authorise();
+ $this->requestAccessToken();
+ }
+
+ if ($this->storage->expiresOn < time()) {
+ $this->refreshAccessToken();
+ }
+
+ return $this->storage->accessToken;
+ }
+
+ private function authorise(): void
+ {
+ $this->authorisationCode = '';
+
+ $this->httpServer->get(
+ '/callback',
+ function (Request $request, HttpServer $server): Response {
+ $server->closeAfterResponse();
+
+ if (
+ !array_key_exists('state', $request->params)
+ || $request->params['state'] !== $this->credentials->state
+ ) {
+ $body = 'State mismatch.';
+ } elseif (array_key_exists('error', $request->params)) {
+ $body = "Error from Spotify: {$request->params['error']}";
+ } elseif (!array_key_exists('code', $request->params)) {
+ $body = "Missing authorisation code from Spotify.";
+ } else {
+ $this->authorisationCode = $request->params['code'];
+ $body = 'Authorisation code received from Spotify. You can close this page and go back to the terminal.';
+ }
+
+ return new Response(
+ statusCode: 200,
+ contentType: 'text/plain; charset=UTF-8',
+ body: htmlspecialchars($body, ENT_QUOTES),
+ );
+ },
+ );
+
+ $query = http_build_query([
+ 'response_type' => 'code',
+ 'client_id' => $this->credentials->clientID,
+ 'redirect_uri' => $this->credentials->redirectURI,
+ 'state' => $this->credentials->state,
+ 'scope' => 'playlist-read-private',
+ 'show_dialog' => false,
+ ]);
+
+ echo "Visit the below URL in your browser to authorise access to Spotify.\n";
+ echo "https://accounts.spotify.com/authorize?$query\n";
+ echo "Waiting for authorisation from Spotify...\n";
+
+ $this->httpServer->run();
+
+ if ($this->authorisationCode === '') {
+ echo "Failed to get authorisation code from Spotify.\n";
+ exit(1);
+ }
+ }
+
+ private function requestAccessToken(): void
+ {
+ echo "Requesting an access token from Spotify.\n";
+
+ $ch = curl_init();
+
+ curl_setopt_array($ch, [
+ CURLOPT_URL => 'https://accounts.spotify.com/api/token',
+ CURLOPT_POST => true,
+ CURLOPT_HTTPHEADER => [
+ 'Authorization: Basic ' . base64_encode($this->credentials->clientID . ':' . $this->credentials->clientSecret),
+ 'Content-Type: application/x-www-form-urlencoded',
+ ],
+ CURLOPT_POSTFIELDS => http_build_query([
+ 'grant_type' => 'authorization_code',
+ 'code' => $this->authorisationCode,
+ 'redirect_uri' => $this->credentials->redirectURI,
+ ]),
+ CURLOPT_RETURNTRANSFER => true,
+ ]);
+
+ $response = curl_exec($ch);
+ $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+
+ if ($statusCode !== 200) {
+ echo "Non 200 response from Spotify: $statusCode $response\n";
+ exit(1);
+ }
+
+ $data = json_decode($response, true) ?? [];
+
+ $accessToken = $data['access_token'] ?? '';
+ $refreshToken = $data['refresh_token'] ?? '';
+ $expiresIn = $data['expires_in'] ?? 0;
+
+ if ($accessToken === '' || $refreshToken === '') {
+ echo "Failed to get access token from Spotify.\n";
+ exit(1);
+ }
+
+ $this->storage->accessToken = $accessToken;
+ $this->storage->refreshToken = $refreshToken;
+ $this->storage->expiresOn = time() + $expiresIn;
+ $this->storage->save();
+ }
+
+ private function refreshAccessToken(): void
+ {
+ echo "Refreshing the Spotify access token.\n";
+
+ $ch = curl_init();
+
+ curl_setopt_array($ch, [
+ CURLOPT_URL => 'https://accounts.spotify.com/api/token',
+ CURLOPT_POST => true,
+ CURLOPT_HTTPHEADER => [
+ 'Authorization: Basic ' . base64_encode($this->credentials->clientID . ':' . $this->credentials->clientSecret),
+ 'Content-Type: application/x-www-form-urlencoded',
+ ],
+ CURLOPT_POSTFIELDS => http_build_query([
+ 'grant_type' => 'refresh_token',
+ 'refresh_token' => $this->storage->refreshToken,
+ ]),
+ CURLOPT_RETURNTRANSFER => true,
+ ]);
+
+ $response = curl_exec($ch);
+ $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+
+ if ($statusCode !== 200) {
+ echo "Non 200 response from Spotify: $statusCode $response\n";
+ exit(1);
+ }
+
+ $data = json_decode($response, true) ?? [];
+
+ $accessToken = $data['access_token'] ?? '';
+ $refreshToken = $data['refresh_token'] ?? $this->storage->refreshToken;
+ $expiresIn = $data['expires_in'] ?? 0;
+
+ if ($accessToken === '' || $refreshToken === '') {
+ echo "Failed to refresh the Spotify access token.\n";
+ exit(1);
+ }
+
+ $this->storage->accessToken = $accessToken;
+ $this->storage->refreshToken = $refreshToken;
+ $this->storage->expiresOn = time() + $expiresIn;
+ $this->storage->save();
+ }
+}