Skip to content

Commit 186fb31

Browse files
feat(api): manual updates
1 parent a147d29 commit 186fb31

37 files changed

Lines changed: 259 additions & 98 deletions

.stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 97
22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless-commons%2Fspotify-7ac9fe2ee73e38b2892f0393435f2d3a275d04b1d0728708382dd752da1d44de.yml
33
openapi_spec_hash: 6be3d4faa079ee82335208bec39c917a
4-
config_hash: f1b6bcd18da45964d97ad878412bd04b
4+
config_hash: 656921a0de616cc4f5d5c0de5c5a64e7

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ The full API of this library can be found in [api.md](api.md).
2525
```js
2626
import Spotify from '@stainless-commons/spotify';
2727

28-
const client = new Spotify({
29-
accessToken: process.env['SPOTIFY_ACCESS_TOKEN'], // This is the default and can be omitted
30-
});
28+
const client = new Spotify();
3129

3230
const album = await client.albums.retrieve('4aawyAB9vmqN3uQ7FjRGTy');
3331

@@ -42,9 +40,7 @@ This library includes TypeScript definitions for all request params and response
4240
```ts
4341
import Spotify from '@stainless-commons/spotify';
4442

45-
const client = new Spotify({
46-
accessToken: process.env['SPOTIFY_ACCESS_TOKEN'], // This is the default and can be omitted
47-
});
43+
const client = new Spotify();
4844

4945
const album: Spotify.AlbumRetrieveResponse = await client.albums.retrieve('4aawyAB9vmqN3uQ7FjRGTy');
5046
```

src/client.ts

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import { UserRetrieveProfileResponse, Users } from './resources/users/users';
108108
import { type Fetch } from './internal/builtin-types';
109109
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
110110
import { FinalRequestOptions, RequestOptions } from './internal/request-options';
111+
import { toBase64 } from './internal/utils/base64';
111112
import { readEnv } from './internal/utils/env';
112113
import {
113114
type LogLevel,
@@ -120,9 +121,14 @@ import { isEmptyObj } from './internal/utils/values';
120121

121122
export interface ClientOptions {
122123
/**
123-
* Defaults to process.env['SPOTIFY_ACCESS_TOKEN'].
124+
* Defaults to process.env['SPOTIFY_CLIENT_ID'].
124125
*/
125-
accessToken?: string | undefined;
126+
clientID?: string | null | undefined;
127+
128+
/**
129+
* Defaults to process.env['SPOTIFY_CLIENT_SECRET'].
130+
*/
131+
clientSecret?: string | null | undefined;
126132

127133
/**
128134
* Override the default base URL for the API, e.g., "https://api.example.com/v2/"
@@ -197,7 +203,8 @@ export interface ClientOptions {
197203
* API Client for interfacing with the Spotify API.
198204
*/
199205
export class Spotify {
200-
accessToken: string;
206+
clientID: string | null;
207+
clientSecret: string | null;
201208

202209
baseURL: string;
203210
maxRetries: number;
@@ -214,7 +221,8 @@ export class Spotify {
214221
/**
215222
* API Client for interfacing with the Spotify API.
216223
*
217-
* @param {string | undefined} [opts.accessToken=process.env['SPOTIFY_ACCESS_TOKEN'] ?? undefined]
224+
* @param {string | null | undefined} [opts.clientID=process.env['SPOTIFY_CLIENT_ID'] ?? null]
225+
* @param {string | null | undefined} [opts.clientSecret=process.env['SPOTIFY_CLIENT_SECRET'] ?? null]
218226
* @param {string} [opts.baseURL=process.env['SPOTIFY_BASE_URL'] ?? https://api.spotify.com/v1] - Override the default base URL for the API.
219227
* @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
220228
* @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
@@ -225,17 +233,13 @@ export class Spotify {
225233
*/
226234
constructor({
227235
baseURL = readEnv('SPOTIFY_BASE_URL'),
228-
accessToken = readEnv('SPOTIFY_ACCESS_TOKEN'),
236+
clientID = readEnv('SPOTIFY_CLIENT_ID') ?? null,
237+
clientSecret = readEnv('SPOTIFY_CLIENT_SECRET') ?? null,
229238
...opts
230239
}: ClientOptions = {}) {
231-
if (accessToken === undefined) {
232-
throw new Errors.SpotifyError(
233-
"The SPOTIFY_ACCESS_TOKEN environment variable is missing or empty; either provide it, or instantiate the Spotify client with an accessToken option, like new Spotify({ accessToken: 'My Access Token' }).",
234-
);
235-
}
236-
237240
const options: ClientOptions = {
238-
accessToken,
241+
clientID,
242+
clientSecret,
239243
...opts,
240244
baseURL: baseURL || `https://api.spotify.com/v1`,
241245
};
@@ -257,7 +261,8 @@ export class Spotify {
257261

258262
this._options = options;
259263

260-
this.accessToken = accessToken;
264+
this.clientID = clientID;
265+
this.clientSecret = clientSecret;
261266
}
262267

263268
/**
@@ -273,9 +278,11 @@ export class Spotify {
273278
logLevel: this.logLevel,
274279
fetch: this.fetch,
275280
fetchOptions: this.fetchOptions,
276-
accessToken: this.accessToken,
281+
clientID: this.clientID,
282+
clientSecret: this.clientSecret,
277283
...options,
278284
});
285+
client.oauth2_0AuthState = this.oauth2_0AuthState;
279286
return client;
280287
}
281288

@@ -294,8 +301,72 @@ export class Spotify {
294301
return;
295302
}
296303

304+
private oauth2_0AuthState:
305+
| {
306+
promise: Promise<{
307+
access_token: string;
308+
token_type: string;
309+
expires_in: number;
310+
expires_at: Date;
311+
refresh_token?: string;
312+
}>;
313+
clientID: string;
314+
clientSecret: string;
315+
}
316+
| undefined;
297317
protected async authHeaders(opts: FinalRequestOptions): Promise<NullableHeaders | undefined> {
298-
return buildHeaders([{ Authorization: `Bearer ${this.accessToken}` }]);
318+
if (!this.clientID || !this.clientSecret) {
319+
return undefined;
320+
}
321+
322+
// Invalidate the cache if the token is expired
323+
if (this.oauth2_0AuthState && +(await this.oauth2_0AuthState.promise).expires_at < Date.now()) {
324+
this.oauth2_0AuthState = undefined;
325+
}
326+
327+
// Invalidate the cache if the relevant state has been changed
328+
if (
329+
this.oauth2_0AuthState &&
330+
this.oauth2_0AuthState.clientID !== this.clientID &&
331+
this.oauth2_0AuthState.clientSecret !== this.clientSecret
332+
) {
333+
this.oauth2_0AuthState = undefined;
334+
}
335+
336+
if (!this.oauth2_0AuthState) {
337+
this.oauth2_0AuthState = {
338+
promise: this.fetch(this.buildURL('https://accounts.spotify.com/api/token', {}), {
339+
method: 'POST',
340+
headers: {
341+
'Content-Type': 'application/x-www-form-urlencoded',
342+
Authorization: `Basic ${toBase64(`${this.clientID}:${this.clientSecret}`)}`,
343+
},
344+
body: 'grant_type=client_credentials',
345+
}).then(async (res) => {
346+
if (!res.ok) {
347+
const errText = await res.text().catch(() => '');
348+
const errJSON = errText ? safeJSON(errText) : undefined;
349+
const errMessage = errJSON ? undefined : errText;
350+
throw this.makeStatusError(res.status, errJSON, errMessage, res.headers);
351+
}
352+
const json = (await res.json()) as {
353+
access_token: string;
354+
token_type: string;
355+
expires_in: number;
356+
refresh_token?: string;
357+
};
358+
const now = new Date();
359+
now.setSeconds(now.getSeconds() + json.expires_in);
360+
return { ...json, expires_at: now };
361+
}),
362+
clientID: this.clientID,
363+
clientSecret: this.clientSecret,
364+
};
365+
}
366+
367+
const token = await this.oauth2_0AuthState.promise;
368+
369+
return buildHeaders([{ Authorization: `Bearer ${token.access_token}` }]);
299370
}
300371

301372
protected stringifyQuery(query: Record<string, unknown>): string {
@@ -622,6 +693,13 @@ export class Spotify {
622693
if (shouldRetryHeader === 'true') return true;
623694
if (shouldRetryHeader === 'false') return false;
624695

696+
// Retry if the token has expired
697+
const oauth2_0Auth = await this.oauth2_0AuthState?.promise;
698+
if (response.status === 401 && oauth2_0Auth && +oauth2_0Auth.expires_at - Date.now() < 10 * 1000) {
699+
this.oauth2_0AuthState = undefined;
700+
return true;
701+
}
702+
625703
// Retry on request timeouts.
626704
if (response.status === 408) return true;
627705

tests/api-resources/albums.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

tests/api-resources/artists.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

tests/api-resources/audio-analysis.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

tests/api-resources/audio-features.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

tests/api-resources/audiobooks.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

tests/api-resources/browse/browse.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

tests/api-resources/browse/categories.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import Spotify from '@stainless-commons/spotify';
44

55
const client = new Spotify({
6-
accessToken: 'My Access Token',
6+
clientID: 'My Client ID',
7+
clientSecret: 'My Client Secret',
78
baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
89
});
910

0 commit comments

Comments
 (0)