soundcloud-api-ts-next 1.1.0 → 1.3.0
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.
- package/README.md +216 -0
- package/dist/index.cjs +493 -3
- package/dist/index.d.cts +135 -2
- package/dist/index.d.ts +135 -2
- package/dist/index.mjs +577 -87
- package/dist/server.cjs +207 -9
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.mjs +203 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,10 +24,13 @@ import { createSoundCloudRoutes } from "soundcloud-api-ts-next/server";
|
|
|
24
24
|
const sc = createSoundCloudRoutes({
|
|
25
25
|
clientId: process.env.SOUNDCLOUD_CLIENT_ID!,
|
|
26
26
|
clientSecret: process.env.SOUNDCLOUD_CLIENT_SECRET!,
|
|
27
|
+
redirectUri: process.env.SOUNDCLOUD_REDIRECT_URI, // Required for OAuth
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const handler = sc.handler();
|
|
30
31
|
export const GET = handler;
|
|
32
|
+
export const POST = handler;
|
|
33
|
+
export const DELETE = handler;
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
**Pages Router** (`pages/api/soundcloud/[...route].ts`):
|
|
@@ -38,6 +41,7 @@ import { createSoundCloudRoutes } from "soundcloud-api-ts-next/server";
|
|
|
38
41
|
const sc = createSoundCloudRoutes({
|
|
39
42
|
clientId: process.env.SOUNDCLOUD_CLIENT_ID!,
|
|
40
43
|
clientSecret: process.env.SOUNDCLOUD_CLIENT_SECRET!,
|
|
44
|
+
redirectUri: process.env.SOUNDCLOUD_REDIRECT_URI,
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
export default sc.pagesHandler();
|
|
@@ -59,6 +63,131 @@ export default function Layout({ children }) {
|
|
|
59
63
|
}
|
|
60
64
|
```
|
|
61
65
|
|
|
66
|
+
## Authentication (OAuth + PKCE)
|
|
67
|
+
|
|
68
|
+
soundcloud-api-ts-next includes a complete OAuth flow with PKCE for secure user authentication.
|
|
69
|
+
|
|
70
|
+
### Setup
|
|
71
|
+
|
|
72
|
+
1. Add `redirectUri` to your server config (see above)
|
|
73
|
+
2. Set your SoundCloud app's redirect URI to match (e.g., `http://localhost:3000/callback`)
|
|
74
|
+
|
|
75
|
+
### Login Flow
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useSCAuth } from "soundcloud-api-ts-next";
|
|
79
|
+
|
|
80
|
+
function LoginButton() {
|
|
81
|
+
const { isAuthenticated, user, login, logout, loading } = useSCAuth();
|
|
82
|
+
|
|
83
|
+
if (loading) return <p>Loading...</p>;
|
|
84
|
+
|
|
85
|
+
if (isAuthenticated) {
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<p>Logged in as {user?.username}</p>
|
|
89
|
+
<button onClick={logout}>Logout</button>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return <button onClick={login}>Login with SoundCloud</button>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Callback Page
|
|
99
|
+
|
|
100
|
+
Create a callback page that handles the OAuth redirect:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// app/callback/page.tsx
|
|
104
|
+
"use client";
|
|
105
|
+
|
|
106
|
+
import { useEffect } from "react";
|
|
107
|
+
import { useSearchParams, useRouter } from "next/navigation";
|
|
108
|
+
import { useSCAuth } from "soundcloud-api-ts-next";
|
|
109
|
+
|
|
110
|
+
export default function CallbackPage() {
|
|
111
|
+
const searchParams = useSearchParams();
|
|
112
|
+
const router = useRouter();
|
|
113
|
+
const { handleCallback } = useSCAuth();
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const code = searchParams.get("code");
|
|
117
|
+
const state = searchParams.get("state");
|
|
118
|
+
if (code && state) {
|
|
119
|
+
handleCallback(code, state).then(() => {
|
|
120
|
+
router.push("/");
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}, [searchParams]);
|
|
124
|
+
|
|
125
|
+
return <p>Authenticating...</p>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Authenticated User Hooks
|
|
130
|
+
|
|
131
|
+
These hooks fetch data for the currently logged-in user. They require authentication and automatically pass the access token.
|
|
132
|
+
|
|
133
|
+
| Hook | Returns | Description |
|
|
134
|
+
|------|---------|-------------|
|
|
135
|
+
| `useMe()` | `HookResult<SoundCloudUser>` | Current user's profile |
|
|
136
|
+
| `useMeTracks()` | `HookResult<SoundCloudTrack[]>` | Current user's tracks |
|
|
137
|
+
| `useMeLikes()` | `HookResult<SoundCloudTrack[]>` | Current user's liked tracks |
|
|
138
|
+
| `useMePlaylists()` | `HookResult<SoundCloudPlaylist[]>` | Current user's playlists |
|
|
139
|
+
| `useMeFollowings()` | `HookResult<SoundCloudUser[]>` | Who current user follows |
|
|
140
|
+
| `useMeFollowers()` | `HookResult<SoundCloudUser[]>` | Current user's followers |
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useMe, useMeTracks, useMeLikes } from "soundcloud-api-ts-next";
|
|
144
|
+
|
|
145
|
+
function MyProfile() {
|
|
146
|
+
const { data: me } = useMe();
|
|
147
|
+
const { data: tracks } = useMeTracks();
|
|
148
|
+
const { data: likes } = useMeLikes();
|
|
149
|
+
|
|
150
|
+
if (!me) return null;
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<h1>{me.username}</h1>
|
|
155
|
+
<p>{tracks?.length} tracks, {likes?.length} likes</p>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Action Hooks
|
|
162
|
+
|
|
163
|
+
Mutation hooks for user actions. All require authentication.
|
|
164
|
+
|
|
165
|
+
| Hook | Methods | Description |
|
|
166
|
+
|------|---------|-------------|
|
|
167
|
+
| `useFollow()` | `follow(userId)`, `unfollow(userId)` | Follow/unfollow a user |
|
|
168
|
+
| `useLike()` | `likeTrack(trackId)`, `unlikeTrack(trackId)` | Like/unlike a track |
|
|
169
|
+
| `useRepost()` | `repostTrack(trackId)`, `unrepostTrack(trackId)` | Repost/unrepost a track |
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { useLike, useFollow } from "soundcloud-api-ts-next";
|
|
173
|
+
|
|
174
|
+
function TrackActions({ trackId, artistId }) {
|
|
175
|
+
const { likeTrack, unlikeTrack, loading: likeLoading } = useLike();
|
|
176
|
+
const { follow, loading: followLoading } = useFollow();
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div>
|
|
180
|
+
<button onClick={() => likeTrack(trackId)} disabled={likeLoading}>
|
|
181
|
+
❤️ Like
|
|
182
|
+
</button>
|
|
183
|
+
<button onClick={() => follow(artistId)} disabled={followLoading}>
|
|
184
|
+
➕ Follow Artist
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
62
191
|
## Hooks
|
|
63
192
|
|
|
64
193
|
All hooks return `{ data, loading, error }`.
|
|
@@ -103,6 +232,39 @@ All hooks return `{ data, loading, error }`.
|
|
|
103
232
|
|
|
104
233
|
All routes are available via the catch-all handler and as individual methods on the routes object.
|
|
105
234
|
|
|
235
|
+
### Auth Routes
|
|
236
|
+
|
|
237
|
+
| Route | Method | Description |
|
|
238
|
+
|-------|--------|-------------|
|
|
239
|
+
| `/auth/login` | GET | Get SoundCloud OAuth URL (PKCE) |
|
|
240
|
+
| `/auth/callback?code=...&state=...` | GET | Exchange auth code for tokens |
|
|
241
|
+
| `/auth/refresh` | POST | Refresh access token |
|
|
242
|
+
| `/auth/logout` | POST | Sign out / revoke token |
|
|
243
|
+
|
|
244
|
+
### Me Routes (require `Authorization: Bearer <token>` header)
|
|
245
|
+
|
|
246
|
+
| Route | Method | Description |
|
|
247
|
+
|-------|--------|-------------|
|
|
248
|
+
| `/me` | GET | Current user profile |
|
|
249
|
+
| `/me/tracks` | GET | Current user's tracks |
|
|
250
|
+
| `/me/likes` | GET | Current user's liked tracks |
|
|
251
|
+
| `/me/playlists` | GET | Current user's playlists |
|
|
252
|
+
| `/me/followings` | GET | Who current user follows |
|
|
253
|
+
| `/me/followers` | GET | Current user's followers |
|
|
254
|
+
|
|
255
|
+
### Action Routes (require `Authorization: Bearer <token>` header)
|
|
256
|
+
|
|
257
|
+
| Route | Method | Description |
|
|
258
|
+
|-------|--------|-------------|
|
|
259
|
+
| `/me/follow/:userId` | POST | Follow a user |
|
|
260
|
+
| `/me/follow/:userId` | DELETE | Unfollow a user |
|
|
261
|
+
| `/tracks/:id/like` | POST | Like a track |
|
|
262
|
+
| `/tracks/:id/like` | DELETE | Unlike a track |
|
|
263
|
+
| `/tracks/:id/repost` | POST | Repost a track |
|
|
264
|
+
| `/tracks/:id/repost` | DELETE | Unrepost a track |
|
|
265
|
+
| `/playlists/:id/like` | POST/DELETE | Like/unlike a playlist |
|
|
266
|
+
| `/playlists/:id/repost` | POST/DELETE | Repost/unrepost a playlist |
|
|
267
|
+
|
|
106
268
|
### Search
|
|
107
269
|
|
|
108
270
|
| Route | Method | Description |
|
|
@@ -139,6 +301,57 @@ All routes are available via the catch-all handler and as individual methods on
|
|
|
139
301
|
| `GET /playlists/:id` | `getPlaylist(id)` | Get playlist details |
|
|
140
302
|
| `GET /playlists/:id/tracks` | `getPlaylistTracks(id)` | Get playlist tracks |
|
|
141
303
|
|
|
304
|
+
## Pagination / Infinite Scroll
|
|
305
|
+
|
|
306
|
+
All paginated endpoints have `useInfinite*` hooks that handle cursor-based pagination automatically:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { useInfiniteTrackSearch } from "soundcloud-api-ts-next";
|
|
310
|
+
|
|
311
|
+
function TrackList() {
|
|
312
|
+
const { data, loading, error, hasMore, loadMore } = useInfiniteTrackSearch("lofi");
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div>
|
|
316
|
+
{data.map((track) => (
|
|
317
|
+
<div key={track.id}>{track.title}</div>
|
|
318
|
+
))}
|
|
319
|
+
{loading && <p>Loading...</p>}
|
|
320
|
+
{error && <p>Error: {error.message}</p>}
|
|
321
|
+
{hasMore && <button onClick={loadMore}>Load More</button>}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Available infinite hooks
|
|
328
|
+
|
|
329
|
+
| Hook | Description |
|
|
330
|
+
| ---- | ----------- |
|
|
331
|
+
| `useInfiniteTrackSearch(query, options?)` | Paginated track search |
|
|
332
|
+
| `useInfiniteUserSearch(query, options?)` | Paginated user search |
|
|
333
|
+
| `useInfinitePlaylistSearch(query, options?)` | Paginated playlist search |
|
|
334
|
+
| `useInfiniteUserTracks(userId)` | User's tracks |
|
|
335
|
+
| `useInfiniteUserPlaylists(userId)` | User's playlists |
|
|
336
|
+
| `useInfiniteUserLikes(userId)` | User's liked tracks |
|
|
337
|
+
| `useInfiniteUserFollowers(userId)` | User's followers |
|
|
338
|
+
| `useInfiniteUserFollowings(userId)` | User's followings |
|
|
339
|
+
| `useInfiniteTrackComments(trackId)` | Track comments |
|
|
340
|
+
| `useInfinitePlaylistTracks(playlistId)` | Playlist tracks |
|
|
341
|
+
|
|
342
|
+
All hooks return `InfiniteResult<T>`:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
interface InfiniteResult<T> {
|
|
346
|
+
data: T[]; // accumulated items across all pages
|
|
347
|
+
loading: boolean;
|
|
348
|
+
error: Error | null;
|
|
349
|
+
hasMore: boolean; // true if more pages exist
|
|
350
|
+
loadMore: () => void;
|
|
351
|
+
reset: () => void; // clear and refetch from page 1
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
142
355
|
## Types
|
|
143
356
|
|
|
144
357
|
All SoundCloud types are re-exported from `soundcloud-api-ts`:
|
|
@@ -150,6 +363,9 @@ import type {
|
|
|
150
363
|
SoundCloudPlaylist,
|
|
151
364
|
SoundCloudComment,
|
|
152
365
|
SoundCloudStreams,
|
|
366
|
+
SoundCloudToken,
|
|
367
|
+
AuthState,
|
|
368
|
+
MutationResult,
|
|
153
369
|
} from "soundcloud-api-ts-next";
|
|
154
370
|
```
|
|
155
371
|
|