Skip to content

Commit 2d257f5

Browse files
authored
Merge pull request #2 from stainless-commons/release-please--branches--main--changes--next--components--spotify
release: 0.2.0
2 parents c9a06a1 + e01330d commit 2d257f5

72 files changed

Lines changed: 1688 additions & 359 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.1.0"
2+
".": "0.2.0"
33
}

.stats.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
configured_endpoints: 89
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless-commons%2Fspotify-139a4fc2ce0b9db4ceab46e2f76e0700a847f4e65d155f6aa7829d9753a30daf.yml
3-
openapi_spec_hash: e96ace8fb2e27de2e776bd575bcf93a1
4-
config_hash: f9595b42469865c2e89f2922b389ff04
1+
configured_endpoints: 97
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless-commons%2Fspotify-7ac9fe2ee73e38b2892f0393435f2d3a275d04b1d0728708382dd752da1d44de.yml
3+
openapi_spec_hash: 6be3d4faa079ee82335208bec39c917a
4+
config_hash: 656921a0de616cc4f5d5c0de5c5a64e7

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 0.2.0 (2026-02-17)
4+
5+
Full Changelog: [v0.1.0...v0.2.0](https://github.com/stainless-commons/spotify-typescript/compare/v0.1.0...v0.2.0)
6+
7+
### Features
8+
9+
* **api:** manual updates ([186fb31](https://github.com/stainless-commons/spotify-typescript/commit/186fb31ff5953da67c9bdd793dc854bc90990b01))
10+
* **api:** manual updates ([a147d29](https://github.com/stainless-commons/spotify-typescript/commit/a147d29e80f471cf69736dbc95c18665f1e6fc58))
11+
* **api:** manual updates ([cef5db7](https://github.com/stainless-commons/spotify-typescript/commit/cef5db7f206aad549423261e53e9f5681d88ce54))
12+
313
## 0.1.0 (2026-02-16)
414

515
Full Changelog: [v0.0.1...v0.1.0](https://github.com/stainless-commons/spotify-typescript/compare/v0.0.1...v0.1.0)

README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This library provides convenient access to the Spotify REST API from server-side TypeScript or JavaScript.
66

7-
The REST API documentation can be found on [spotify.cjav.dev](https://spotify.cjav.dev). The full API of this library can be found in [api.md](api.md).
7+
The REST API documentation can be found on [stainless.com](https://stainless.com). The full API of this library can be found in [api.md](api.md).
88

99
It is generated with [Stainless](https://www.stainless.com/).
1010

@@ -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
```

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ or products provided by Spotify, please follow the respective company's security
2020

2121
### Spotify Terms and Policies
2222

23-
Please contact wave@cjav.dev for any questions or concerns regarding the security of our services.
23+
Please contact commons@stainless.com for any questions or concerns regarding the security of our services.
2424

2525
---
2626

api.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,13 @@ Methods:
123123

124124
## Playlists
125125

126+
Types:
127+
128+
- <code><a href="./src/resources/me/playlists.ts">PlaylistCreateResponse</a></code>
129+
126130
Methods:
127131

132+
- <code title="post /me/playlists">client.me.playlists.<a href="./src/resources/me/playlists.ts">create</a>({ ...params }) -> PlaylistCreateResponse</code>
128133
- <code title="get /me/playlists">client.me.playlists.<a href="./src/resources/me/playlists.ts">list</a>({ ...params }) -> SimplifiedPlaylistObjectsCursorURLPage</code>
129134

130135
## Top
@@ -242,6 +247,18 @@ Methods:
242247
- <code title="post /me/player/queue">client.me.player.queue.<a href="./src/resources/me/player/queue.ts">add</a>({ ...params }) -> void</code>
243248
- <code title="get /me/player/queue">client.me.player.queue.<a href="./src/resources/me/player/queue.ts">get</a>() -> QueueGetResponse</code>
244249

250+
## Library
251+
252+
Types:
253+
254+
- <code><a href="./src/resources/me/library.ts">LibraryCheckSavedItemsResponse</a></code>
255+
256+
Methods:
257+
258+
- <code title="get /me/library/contains">client.me.library.<a href="./src/resources/me/library.ts">checkSavedItems</a>({ ...params }) -> LibraryCheckSavedItemsResponse</code>
259+
- <code title="delete /me/library">client.me.library.<a href="./src/resources/me/library.ts">removeItems</a>({ ...params }) -> void</code>
260+
- <code title="put /me/library">client.me.library.<a href="./src/resources/me/library.ts">saveItems</a>({ ...params }) -> void</code>
261+
245262
# Chapters
246263

247264
Types:
@@ -324,6 +341,21 @@ Methods:
324341
- <code title="put /playlists/{playlist_id}/images">client.playlists.images.<a href="./src/resources/playlists/images.ts">update</a>(playlistID, body) -> Response</code>
325342
- <code title="get /playlists/{playlist_id}/images">client.playlists.images.<a href="./src/resources/playlists/images.ts">list</a>(playlistID) -> ImageListResponse</code>
326343

344+
## Items
345+
346+
Types:
347+
348+
- <code><a href="./src/resources/playlists/items.ts">ItemUpdateResponse</a></code>
349+
- <code><a href="./src/resources/playlists/items.ts">ItemAddResponse</a></code>
350+
- <code><a href="./src/resources/playlists/items.ts">ItemRemoveResponse</a></code>
351+
352+
Methods:
353+
354+
- <code title="put /playlists/{playlist_id}/items">client.playlists.items.<a href="./src/resources/playlists/items.ts">update</a>(playlistID, { ...params }) -> ItemUpdateResponse</code>
355+
- <code title="get /playlists/{playlist_id}/items">client.playlists.items.<a href="./src/resources/playlists/items.ts">list</a>(playlistID, { ...params }) -> PlaylistTrackObjectsCursorURLPage</code>
356+
- <code title="post /playlists/{playlist_id}/items">client.playlists.items.<a href="./src/resources/playlists/items.ts">add</a>(playlistID, { ...params }) -> ItemAddResponse</code>
357+
- <code title="delete /playlists/{playlist_id}/items">client.playlists.items.<a href="./src/resources/playlists/items.ts">remove</a>(playlistID, { ...params }) -> ItemRemoveResponse</code>
358+
327359
# Users
328360

329361
Types:

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "@stainless-commons/spotify",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "The official TypeScript library for the Spotify API",
5-
"author": "Spotify <wave@cjav.dev>",
5+
"author": "Spotify <commons@stainless.com>",
66
"types": "dist/index.d.ts",
77
"main": "dist/index.js",
88
"type": "commonjs",

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

src/resources/albums.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class Albums extends APIResource {
2323
/**
2424
* Get Spotify catalog information for multiple albums identified by their Spotify
2525
* IDs.
26+
*
27+
* @deprecated
2628
*/
2729
bulkRetrieve(
2830
query: AlbumBulkRetrieveParams,
@@ -60,7 +62,7 @@ export interface AlbumRetrieveResponse {
6062
album_type: 'album' | 'single' | 'compilation';
6163

6264
/**
63-
* The markets in which the album is available:
65+
* @deprecated The markets in which the album is available:
6466
* [ISO 3166-1 alpha-2 country codes](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
6567
* _**NOTE**: an album is considered available in a market when at least 1 of its
6668
* tracks is available in that market._
@@ -126,7 +128,7 @@ export interface AlbumRetrieveResponse {
126128
copyrights?: Array<Shared.CopyrightObject>;
127129

128130
/**
129-
* Known external IDs for the album.
131+
* @deprecated Known external IDs for the album.
130132
*/
131133
external_ids?: Shared.ExternalIDObject;
132134

@@ -136,13 +138,13 @@ export interface AlbumRetrieveResponse {
136138
genres?: Array<string>;
137139

138140
/**
139-
* The label associated with the album.
141+
* @deprecated The label associated with the album.
140142
*/
141143
label?: string;
142144

143145
/**
144-
* The popularity of the album. The value will be between 0 and 100, with 100 being
145-
* the most popular.
146+
* @deprecated The popularity of the album. The value will be between 0 and 100,
147+
* with 100 being the most popular.
146148
*/
147149
popularity?: number;
148150

@@ -232,7 +234,7 @@ export namespace AlbumBulkRetrieveResponse {
232234
album_type: 'album' | 'single' | 'compilation';
233235

234236
/**
235-
* The markets in which the album is available:
237+
* @deprecated The markets in which the album is available:
236238
* [ISO 3166-1 alpha-2 country codes](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
237239
* _**NOTE**: an album is considered available in a market when at least 1 of its
238240
* tracks is available in that market._
@@ -298,7 +300,7 @@ export namespace AlbumBulkRetrieveResponse {
298300
copyrights?: Array<Shared.CopyrightObject>;
299301

300302
/**
301-
* Known external IDs for the album.
303+
* @deprecated Known external IDs for the album.
302304
*/
303305
external_ids?: Shared.ExternalIDObject;
304306

@@ -308,13 +310,13 @@ export namespace AlbumBulkRetrieveResponse {
308310
genres?: Array<string>;
309311

310312
/**
311-
* The label associated with the album.
313+
* @deprecated The label associated with the album.
312314
*/
313315
label?: string;
314316

315317
/**
316-
* The popularity of the album. The value will be between 0 and 100, with 100 being
317-
* the most popular.
318+
* @deprecated The popularity of the album. The value will be between 0 and 100,
319+
* with 100 being the most popular.
318320
*/
319321
popularity?: number;
320322

0 commit comments

Comments
 (0)