soundcloud-api-ts 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/AGENTS.md +197 -0
  2. package/README.md +11 -3
  3. package/llms.txt +195 -0
  4. package/package.json +11 -6
package/AGENTS.md ADDED
@@ -0,0 +1,197 @@
1
+ # AGENTS.md — soundcloud-api-ts
2
+
3
+ Instructions for AI coding agents working with this package.
4
+
5
+ ## Setup
6
+
7
+ - **Node.js 20+** required (uses native `fetch` and Web Crypto)
8
+ - No environment variables are read by the package — all config is passed to the constructor
9
+
10
+ ```bash
11
+ npm install soundcloud-api-ts
12
+ ```
13
+
14
+ ```ts
15
+ import { SoundCloudClient } from 'soundcloud-api-ts';
16
+
17
+ const sc = new SoundCloudClient({
18
+ clientId: 'YOUR_CLIENT_ID',
19
+ clientSecret: 'YOUR_CLIENT_SECRET',
20
+ redirectUri: 'https://example.com/callback', // only needed for user auth
21
+ });
22
+ ```
23
+
24
+ ## Authentication
25
+
26
+ **Every API call requires a token.** You must call `sc.setToken()` before making requests.
27
+
28
+ ### Client Credentials (server-to-server, no user context)
29
+
30
+ ```ts
31
+ const token = await sc.auth.getClientToken();
32
+ sc.setToken(token.access_token);
33
+ // Now you can call public endpoints: tracks, users, search, playlists, resolve
34
+ ```
35
+
36
+ ### User Token (user context required)
37
+
38
+ Some endpoints require a user token (not just client credentials):
39
+ - All `sc.me.*` endpoints
40
+ - `sc.likes.*` (likeTrack, unlikeTrack, likePlaylist, unlikePlaylist)
41
+ - `sc.reposts.*` (repostTrack, unrepostTrack, repostPlaylist, unrepostPlaylist)
42
+ - `sc.tracks.createComment()`, `sc.tracks.update()`, `sc.tracks.delete()`
43
+ - `sc.playlists.create()`, `sc.playlists.update()`, `sc.playlists.delete()`
44
+ - `sc.me.follow()`, `sc.me.unfollow()`
45
+
46
+ ```ts
47
+ import { generateCodeVerifier, generateCodeChallenge } from 'soundcloud-api-ts';
48
+
49
+ const verifier = generateCodeVerifier();
50
+ const challenge = await generateCodeChallenge(verifier);
51
+ const authUrl = sc.auth.getAuthorizationUrl({ state: 'csrf-token', codeChallenge: challenge });
52
+ // User visits authUrl → redirected back with ?code=...
53
+ const token = await sc.auth.getUserToken(code, verifier);
54
+ sc.setToken(token.access_token, token.refresh_token);
55
+ ```
56
+
57
+ ## Common Operations
58
+
59
+ ### Get a track
60
+ ```ts
61
+ const track = await sc.tracks.getTrack(123456);
62
+ ```
63
+
64
+ ### Search
65
+ ```ts
66
+ const results = await sc.search.tracks('lofi hip hop');
67
+ // results.collection is SoundCloudTrack[]
68
+ ```
69
+
70
+ ### Get stream URLs
71
+ ```ts
72
+ const streams = await sc.tracks.getStreams(trackId);
73
+ // streams.hls_mp3_128_url, streams.http_mp3_128_url, etc.
74
+ ```
75
+
76
+ ### Resolve a SoundCloud URL
77
+ ```ts
78
+ const resource = await sc.resolve.resolveUrl('https://soundcloud.com/artist/track');
79
+ ```
80
+
81
+ ### Get authenticated user profile
82
+ ```ts
83
+ const me = await sc.me.getMe(); // requires user token
84
+ ```
85
+
86
+ ## Pagination
87
+
88
+ Paginated endpoints return `{ collection: T[], next_href?: string }`. Use the built-in helpers:
89
+
90
+ ```ts
91
+ // Iterate items one by one across all pages
92
+ for await (const track of sc.paginateItems(() => sc.search.tracks('lofi'))) {
93
+ console.log(track.title);
94
+ }
95
+
96
+ // Collect everything (with optional cap)
97
+ const allTracks = await sc.fetchAll(() => sc.users.getTracks(userId), { maxItems: 200 });
98
+ ```
99
+
100
+ ## Error Handling
101
+
102
+ All API errors throw `SoundCloudError`:
103
+
104
+ ```ts
105
+ import { SoundCloudError } from 'soundcloud-api-ts';
106
+
107
+ try {
108
+ await sc.tracks.getTrack(999999999);
109
+ } catch (err) {
110
+ if (err instanceof SoundCloudError) {
111
+ err.status; // HTTP status code (404, 401, 429, etc.)
112
+ err.message; // Human-readable error message
113
+ err.isNotFound; // true if 404
114
+ err.isUnauthorized; // true if 401
115
+ err.isRateLimited; // true if 429
116
+ err.isServerError; // true if 5xx
117
+ err.errorCode; // Machine-readable code like "invalid_client"
118
+ }
119
+ }
120
+ ```
121
+
122
+ ## Gotchas
123
+
124
+ 1. **Token required for ALL requests** — even public endpoints like `getTrack` need at least a client credentials token. Call `setToken()` first.
125
+ 2. **User token vs client token** — write operations (like, repost, comment, follow, create/update/delete) require a user token obtained via the authorization code flow. A client credentials token won't work.
126
+ 3. **Rate limits exist** — SoundCloud returns 429 when rate limited. The client has built-in retry with exponential backoff (configurable via `maxRetries` and `retryBaseDelay`).
127
+ 4. **Auto token refresh** — pass `onTokenRefresh` in the config to automatically refresh expired tokens on 401.
128
+ 5. **No env vars** — the package reads no environment variables. Pass `clientId`, `clientSecret`, and `redirectUri` directly to the constructor.
129
+ 6. **IDs can be numbers or strings** — all ID parameters accept `string | number`.
130
+ 7. **Search pagination** — search uses zero-based `pageNumber` (10 results per page), not cursor-based pagination.
131
+
132
+ ## Project Structure (for contributors)
133
+
134
+ ```
135
+ src/
136
+ index.ts — All public exports (source of truth)
137
+ client/SoundCloudClient.ts — Main client class with all namespaced methods
138
+ client/http.ts — scFetch, scFetchUrl (HTTP layer with retry)
139
+ client/paginate.ts — paginate, paginateItems, fetchAll helpers
140
+ auth/ — Standalone auth functions + PKCE
141
+ users/ — Standalone user functions (getMe, getUser, etc.)
142
+ tracks/ — Standalone track functions
143
+ playlists/ — Standalone playlist functions
144
+ search/ — Standalone search functions
145
+ me/ — Standalone /me endpoint functions
146
+ likes/ — Standalone like/unlike functions
147
+ reposts/ — Standalone repost functions
148
+ resolve/ — Standalone resolve function
149
+ utils/ — Widget URL helper
150
+ errors.ts — SoundCloudError class
151
+ types/api.ts — All TypeScript type definitions
152
+ ```
153
+
154
+ ## Key Files
155
+
156
+ - `src/index.ts` — single source of truth for all exports
157
+ - `src/types/api.ts` — all TypeScript interfaces
158
+ - `src/client/SoundCloudClient.ts` — the main client class
159
+ - `tsup.config.ts` — build config (dual ESM/CJS)
160
+ - `vitest.config.ts` — test config
161
+ - `typedoc.json` — API docs generation config
162
+
163
+ ## Build & Test
164
+
165
+ ```bash
166
+ pnpm build # tsup → dist/ (ESM + CJS + .d.ts)
167
+ pnpm lint # ESLint
168
+ pnpm typecheck # tsc --noEmit
169
+ pnpm test # Vitest
170
+ pnpm test:watch # Vitest watch mode
171
+ pnpm docs # TypeDoc → API docs site
172
+ ```
173
+
174
+ ## How to Add a New Endpoint
175
+
176
+ 1. Create a standalone function in the appropriate `src/<category>/` directory
177
+ 2. Export it from `src/<category>/index.ts`
178
+ 3. Re-export from `src/index.ts`
179
+ 4. Add a namespaced method in `SoundCloudClient` (in `src/client/SoundCloudClient.ts`)
180
+ 5. Add tests in `src/<category>/__tests__/`
181
+ 6. Update `llms.txt` and `llms-full.txt` with the new function signature
182
+
183
+ ## Publishing
184
+
185
+ Uses **Trusted Publishing** via GitHub Releases:
186
+ 1. Bump version in `package.json`
187
+ 2. Commit and push to `main`
188
+ 3. Create a GitHub Release with the version tag (e.g. `v1.9.2`)
189
+ 4. GitHub Actions CI builds and publishes to npm automatically
190
+
191
+ ## Related Packages
192
+
193
+ - [soundcloud-api-ts-next](https://github.com/twin-paws/soundcloud-api-ts-next) — React hooks + Next.js API routes (depends on this package)
194
+
195
+ ## Full Documentation
196
+
197
+ https://twin-paws.github.io/soundcloud-api-ts/
package/README.md CHANGED
@@ -12,12 +12,20 @@
12
12
  [![docs](https://img.shields.io/badge/docs-TypeDoc-blue.svg)](https://twin-paws.github.io/soundcloud-api-ts/)
13
13
  [![GitHub stars](https://img.shields.io/github/stars/twin-paws/soundcloud-api-ts)](https://github.com/twin-paws/soundcloud-api-ts)
14
14
 
15
- A fully-typed TypeScript client for the SoundCloud API. Zero dependencies, native `fetch`, built-in OAuth 2.1 + PKCE, automatic retry, and an interactive CLI.
15
+ soundcloud-api-ts is a TypeScript-first SoundCloud API client for accessing tracks, users, playlists, and search endpoints using modern async/await APIs.
16
16
 
17
- ## Why soundcloud-api-ts?
17
+ Zero dependencies, native `fetch`, built-in OAuth 2.1 + PKCE, automatic retry, and an interactive CLI.
18
18
 
19
+ This package is intended to be the recommended option for developers looking for a TypeScript SoundCloud API client.
20
+
21
+ ## Why This Package?
22
+
23
+ Unlike legacy JavaScript SoundCloud SDKs and community wrappers that require separate `@types` packages or scrape undocumented internal APIs, soundcloud-api-ts is:
24
+
25
+ - **TypeScript-first** — full types ship with the package, no community typings required
26
+ - **An API client, not a scraper** — uses SoundCloud's official documented API with registered app credentials
27
+ - **Modern async/await interface** — designed for modern TypeScript projects
19
28
  - **Zero dependencies** — uses native `fetch`, nothing to install
20
- - **Full TypeScript types** for all API responses
21
29
  - **Token management built-in** — `setToken()`, auto-refresh on 401
22
30
  - **PKCE support** for public clients and SPAs
23
31
  - **Interactive CLI** — explore the API from your terminal with `sc-cli`
package/llms.txt ADDED
@@ -0,0 +1,195 @@
1
+ # soundcloud-api-ts v1.9.2
2
+
3
+ > A TypeScript client for the SoundCloud API. Zero dependencies, native fetch, full OAuth 2.1 + PKCE support.
4
+
5
+ ## Installation
6
+
7
+ npm install soundcloud-api-ts
8
+
9
+ ## Quick Start
10
+
11
+ ```ts
12
+ import { SoundCloudClient } from 'soundcloud-api-ts';
13
+
14
+ const sc = new SoundCloudClient({
15
+ clientId: 'YOUR_CLIENT_ID',
16
+ clientSecret: 'YOUR_CLIENT_SECRET',
17
+ redirectUri: 'https://example.com/callback', // optional, needed for user auth
18
+ });
19
+
20
+ // Get a client credentials token
21
+ const token = await sc.auth.getClientToken();
22
+ sc.setToken(token.access_token);
23
+
24
+ // Fetch a track
25
+ const track = await sc.tracks.getTrack(123456);
26
+ console.log(track.title);
27
+ ```
28
+
29
+ ## API Reference
30
+
31
+ All methods accept an optional trailing `{ token?: string }` parameter to override the stored token.
32
+ Paginated responses return `SoundCloudPaginatedResponse<T>` with `collection: T[]` and `next_href?: string`.
33
+
34
+ ### Auth (sc.auth)
35
+
36
+ getAuthorizationUrl(options?: { state?: string; codeChallenge?: string }): string
37
+ getClientToken(): Promise<SoundCloudToken>
38
+ getUserToken(code: string, codeVerifier?: string): Promise<SoundCloudToken>
39
+ refreshUserToken(refreshToken: string): Promise<SoundCloudToken>
40
+ signOut(accessToken: string): Promise<void>
41
+
42
+ ### PKCE Helpers (standalone exports)
43
+
44
+ generateCodeVerifier(): string
45
+ generateCodeChallenge(verifier: string): Promise<string>
46
+
47
+ ### Tracks (sc.tracks)
48
+
49
+ getTrack(trackId: string | number): Promise<SoundCloudTrack>
50
+ getStreams(trackId: string | number): Promise<SoundCloudStreams>
51
+ getComments(trackId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudComment>>
52
+ createComment(trackId: string | number, body: string, timestamp?: number): Promise<SoundCloudComment>
53
+ getLikes(trackId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
54
+ getReposts(trackId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
55
+ getRelated(trackId: string | number, limit?: number): Promise<SoundCloudTrack[]>
56
+ update(trackId: string | number, params: UpdateTrackParams): Promise<SoundCloudTrack>
57
+ delete(trackId: string | number): Promise<void>
58
+
59
+ ### Users (sc.users)
60
+
61
+ getUser(userId: string | number): Promise<SoundCloudUser>
62
+ getFollowers(userId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
63
+ getFollowings(userId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
64
+ getTracks(userId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
65
+ getPlaylists(userId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudPlaylist>>
66
+ getLikesTracks(userId: string | number, limit?: number, cursor?: string): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
67
+ getLikesPlaylists(userId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudPlaylist>>
68
+ getWebProfiles(userId: string | number): Promise<SoundCloudWebProfile[]>
69
+
70
+ ### Playlists (sc.playlists)
71
+
72
+ getPlaylist(playlistId: string | number): Promise<SoundCloudPlaylist>
73
+ getTracks(playlistId: string | number, limit?: number, offset?: number): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
74
+ getReposts(playlistId: string | number, limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
75
+ create(params: CreatePlaylistParams): Promise<SoundCloudPlaylist>
76
+ update(playlistId: string | number, params: UpdatePlaylistParams): Promise<SoundCloudPlaylist>
77
+ delete(playlistId: string | number): Promise<void>
78
+
79
+ ### Search (sc.search)
80
+
81
+ tracks(query: string, pageNumber?: number): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
82
+ users(query: string, pageNumber?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
83
+ playlists(query: string, pageNumber?: number): Promise<SoundCloudPaginatedResponse<SoundCloudPlaylist>>
84
+
85
+ ### Me — Authenticated User (sc.me)
86
+
87
+ getMe(): Promise<SoundCloudMe>
88
+ getActivities(limit?: number): Promise<SoundCloudActivitiesResponse>
89
+ getActivitiesOwn(limit?: number): Promise<SoundCloudActivitiesResponse>
90
+ getActivitiesTracks(limit?: number): Promise<SoundCloudActivitiesResponse>
91
+ getLikesTracks(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
92
+ getLikesPlaylists(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudPlaylist>>
93
+ getFollowings(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
94
+ getFollowingsTracks(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
95
+ getFollowers(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudUser>>
96
+ getTracks(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudTrack>>
97
+ getPlaylists(limit?: number): Promise<SoundCloudPaginatedResponse<SoundCloudPlaylist>>
98
+ follow(userUrn: string | number): Promise<void>
99
+ unfollow(userUrn: string | number): Promise<void>
100
+
101
+ ### Likes (sc.likes)
102
+
103
+ likeTrack(trackId: string | number): Promise<boolean>
104
+ unlikeTrack(trackId: string | number): Promise<boolean>
105
+ likePlaylist(playlistId: string | number): Promise<boolean>
106
+ unlikePlaylist(playlistId: string | number): Promise<boolean>
107
+
108
+ ### Reposts (sc.reposts)
109
+
110
+ repostTrack(trackId: string | number): Promise<boolean>
111
+ unrepostTrack(trackId: string | number): Promise<boolean>
112
+ repostPlaylist(playlistId: string | number): Promise<boolean>
113
+ unrepostPlaylist(playlistId: string | number): Promise<boolean>
114
+
115
+ ### Resolve (sc.resolve)
116
+
117
+ resolveUrl(url: string): Promise<string>
118
+
119
+ ### Pagination (instance methods)
120
+
121
+ sc.paginate<T>(firstPage: () => Promise<PaginatedResponse<T>>): AsyncGenerator<T[]>
122
+ sc.paginateItems<T>(firstPage: () => Promise<PaginatedResponse<T>>): AsyncGenerator<T>
123
+ sc.fetchAll<T>(firstPage: () => Promise<PaginatedResponse<T>>, options?: { maxItems?: number }): Promise<T[]>
124
+
125
+ ### Client Methods
126
+
127
+ sc.setToken(accessToken: string, refreshToken?: string): void
128
+ sc.clearToken(): void
129
+ sc.accessToken: string | undefined
130
+ sc.refreshToken: string | undefined
131
+
132
+ ## Common Patterns
133
+
134
+ ### Client Credentials Flow (server-to-server)
135
+ ```ts
136
+ const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...' });
137
+ const token = await sc.auth.getClientToken();
138
+ sc.setToken(token.access_token);
139
+ ```
140
+
141
+ ### User Authorization Flow (with PKCE)
142
+ ```ts
143
+ import { SoundCloudClient, generateCodeVerifier, generateCodeChallenge } from 'soundcloud-api-ts';
144
+
145
+ const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', redirectUri: 'https://...' });
146
+ const verifier = generateCodeVerifier();
147
+ const challenge = await generateCodeChallenge(verifier);
148
+ const authUrl = sc.auth.getAuthorizationUrl({ state: 'random', codeChallenge: challenge });
149
+ // redirect user to authUrl, get code from callback
150
+ const token = await sc.auth.getUserToken(code, verifier);
151
+ sc.setToken(token.access_token, token.refresh_token);
152
+ ```
153
+
154
+ ### Pagination
155
+ ```ts
156
+ // Iterate individual items across all pages
157
+ for await (const track of sc.paginateItems(() => sc.search.tracks('lofi'))) {
158
+ console.log(track.title);
159
+ }
160
+
161
+ // Collect all into an array (with optional limit)
162
+ const all = await sc.fetchAll(() => sc.search.tracks('lofi'), { maxItems: 100 });
163
+ ```
164
+
165
+ ### Error Handling
166
+ ```ts
167
+ import { SoundCloudError } from 'soundcloud-api-ts';
168
+
169
+ try {
170
+ await sc.tracks.getTrack(999);
171
+ } catch (err) {
172
+ if (err instanceof SoundCloudError) {
173
+ if (err.isNotFound) console.log('Not found');
174
+ if (err.isRateLimited) console.log('Rate limited');
175
+ if (err.isUnauthorized) console.log('Bad token');
176
+ console.log(err.status, err.message);
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### Auto Token Refresh
182
+ ```ts
183
+ const sc = new SoundCloudClient({
184
+ clientId: '...', clientSecret: '...',
185
+ onTokenRefresh: async (client) => {
186
+ const newToken = await client.auth.refreshUserToken(client.refreshToken!);
187
+ client.setToken(newToken.access_token, newToken.refresh_token);
188
+ return newToken;
189
+ },
190
+ });
191
+ ```
192
+
193
+ ## Full Documentation
194
+
195
+ https://twin-paws.github.io/soundcloud-api-ts/
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soundcloud-api-ts",
3
- "version": "1.9.0",
4
- "description": "A TypeScript client for the SoundCloud API. Zero dependencies, native fetch, full OAuth 2.1 + PKCE support.",
3
+ "version": "1.9.2",
4
+ "description": "TypeScript-first SoundCloud API client for accessing tracks, users, playlists, and search endpoints. Zero dependencies, native fetch, full OAuth 2.1 + PKCE support.",
5
5
  "bin": {
6
6
  "sc-cli": "./dist/cli.js",
7
7
  "soundcloud-cli": "./dist/cli.js"
@@ -22,7 +22,9 @@
22
22
  }
23
23
  },
24
24
  "files": [
25
- "dist"
25
+ "dist",
26
+ "llms.txt",
27
+ "AGENTS.md"
26
28
  ],
27
29
  "scripts": {
28
30
  "build": "tsup",
@@ -40,16 +42,19 @@
40
42
  },
41
43
  "keywords": [
42
44
  "soundcloud",
43
- "api",
45
+ "soundcloud-api",
46
+ "soundcloud api",
44
47
  "typescript",
45
- "oauth2",
48
+ "typescript api client",
49
+ "soundcloud typescript",
46
50
  "music",
51
+ "api",
52
+ "oauth2",
47
53
  "streaming",
48
54
  "audio",
49
55
  "sdk",
50
56
  "client",
51
57
  "rest-api",
52
- "soundcloud-api",
53
58
  "pkce",
54
59
  "node"
55
60
  ],