soundcloud-api-ts-next 1.2.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 +165 -0
- package/dist/index.cjs +328 -3
- package/dist/index.d.cts +94 -2
- package/dist/index.d.ts +94 -2
- package/dist/index.mjs +421 -96
- package/dist/server.cjs +200 -9
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.mjs +196 -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 |
|
|
@@ -201,6 +363,9 @@ import type {
|
|
|
201
363
|
SoundCloudPlaylist,
|
|
202
364
|
SoundCloudComment,
|
|
203
365
|
SoundCloudStreams,
|
|
366
|
+
SoundCloudToken,
|
|
367
|
+
AuthState,
|
|
368
|
+
MutationResult,
|
|
204
369
|
} from "soundcloud-api-ts-next";
|
|
205
370
|
```
|
|
206
371
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,13 +4,98 @@
|
|
|
4
4
|
var _react = require('react');
|
|
5
5
|
var _jsxruntime = require('react/jsx-runtime');
|
|
6
6
|
var SoundCloudContext = _react.createContext.call(void 0, {
|
|
7
|
-
apiPrefix: "/api/soundcloud"
|
|
7
|
+
apiPrefix: "/api/soundcloud",
|
|
8
|
+
user: null,
|
|
9
|
+
accessToken: null,
|
|
10
|
+
isAuthenticated: false,
|
|
11
|
+
authLoading: false,
|
|
12
|
+
login: () => {
|
|
13
|
+
},
|
|
14
|
+
logout: async () => {
|
|
15
|
+
},
|
|
16
|
+
_setAuth: () => {
|
|
17
|
+
}
|
|
8
18
|
});
|
|
9
19
|
function SoundCloudProvider({
|
|
10
20
|
apiPrefix = "/api/soundcloud",
|
|
11
21
|
children
|
|
12
22
|
}) {
|
|
13
|
-
|
|
23
|
+
const [accessToken, setAccessToken] = _react.useState.call(void 0, null);
|
|
24
|
+
const [refreshToken, setRefreshToken] = _react.useState.call(void 0, null);
|
|
25
|
+
const [expiresAt, setExpiresAt] = _react.useState.call(void 0, null);
|
|
26
|
+
const [user, setUser] = _react.useState.call(void 0, null);
|
|
27
|
+
const [authLoading, setAuthLoading] = _react.useState.call(void 0, false);
|
|
28
|
+
const _setAuth = _react.useCallback.call(void 0,
|
|
29
|
+
(auth) => {
|
|
30
|
+
setAccessToken(auth.accessToken);
|
|
31
|
+
setRefreshToken(auth.refreshToken);
|
|
32
|
+
setExpiresAt(Date.now() + auth.expiresIn * 1e3);
|
|
33
|
+
},
|
|
34
|
+
[]
|
|
35
|
+
);
|
|
36
|
+
_react.useEffect.call(void 0, () => {
|
|
37
|
+
if (!accessToken) {
|
|
38
|
+
setUser(null);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
setAuthLoading(true);
|
|
43
|
+
fetch(`${apiPrefix}/me`, {
|
|
44
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
45
|
+
}).then((res) => {
|
|
46
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
47
|
+
return res.json();
|
|
48
|
+
}).then((data) => {
|
|
49
|
+
if (!cancelled) setUser(data);
|
|
50
|
+
}).catch(() => {
|
|
51
|
+
if (!cancelled) setUser(null);
|
|
52
|
+
}).finally(() => {
|
|
53
|
+
if (!cancelled) setAuthLoading(false);
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
cancelled = true;
|
|
57
|
+
};
|
|
58
|
+
}, [accessToken, apiPrefix]);
|
|
59
|
+
const login = _react.useCallback.call(void 0, () => {
|
|
60
|
+
fetch(`${apiPrefix}/auth/login`).then((res) => res.json()).then((data) => {
|
|
61
|
+
if (data.url) {
|
|
62
|
+
window.location.href = data.url;
|
|
63
|
+
}
|
|
64
|
+
}).catch(console.error);
|
|
65
|
+
}, [apiPrefix]);
|
|
66
|
+
const logout = _react.useCallback.call(void 0, async () => {
|
|
67
|
+
try {
|
|
68
|
+
if (accessToken) {
|
|
69
|
+
await fetch(`${apiPrefix}/auth/logout`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ access_token: accessToken })
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
}
|
|
77
|
+
setAccessToken(null);
|
|
78
|
+
setRefreshToken(null);
|
|
79
|
+
setExpiresAt(null);
|
|
80
|
+
setUser(null);
|
|
81
|
+
}, [accessToken, apiPrefix]);
|
|
82
|
+
const isAuthenticated = accessToken !== null && user !== null;
|
|
83
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
84
|
+
SoundCloudContext.Provider,
|
|
85
|
+
{
|
|
86
|
+
value: {
|
|
87
|
+
apiPrefix,
|
|
88
|
+
user,
|
|
89
|
+
accessToken,
|
|
90
|
+
isAuthenticated,
|
|
91
|
+
authLoading,
|
|
92
|
+
login,
|
|
93
|
+
logout,
|
|
94
|
+
_setAuth
|
|
95
|
+
},
|
|
96
|
+
children
|
|
97
|
+
}
|
|
98
|
+
);
|
|
14
99
|
}
|
|
15
100
|
function useSoundCloudContext() {
|
|
16
101
|
return _react.useContext.call(void 0, SoundCloudContext);
|
|
@@ -639,6 +724,246 @@ function useInfinitePlaylistTracks(playlistId) {
|
|
|
639
724
|
return useInfinite(url, !!playlistId);
|
|
640
725
|
}
|
|
641
726
|
|
|
727
|
+
// src/client/hooks/useSCAuth.ts
|
|
728
|
+
function useSCAuth() {
|
|
729
|
+
const { user, isAuthenticated, authLoading, login, logout, _setAuth, apiPrefix } = useSoundCloudContext();
|
|
730
|
+
return {
|
|
731
|
+
user,
|
|
732
|
+
isAuthenticated,
|
|
733
|
+
loading: authLoading,
|
|
734
|
+
login,
|
|
735
|
+
logout,
|
|
736
|
+
/** Handle OAuth callback — exchange code for tokens */
|
|
737
|
+
async handleCallback(code, state) {
|
|
738
|
+
const res = await fetch(`${apiPrefix}/auth/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`);
|
|
739
|
+
if (!res.ok) throw new Error(`Auth callback failed: ${res.status}`);
|
|
740
|
+
const tokens = await res.json();
|
|
741
|
+
_setAuth({
|
|
742
|
+
accessToken: tokens.access_token,
|
|
743
|
+
refreshToken: tokens.refresh_token,
|
|
744
|
+
expiresIn: tokens.expires_in
|
|
745
|
+
});
|
|
746
|
+
return tokens;
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/client/hooks/useAuthFetch.ts
|
|
752
|
+
|
|
753
|
+
function useAuthFetch(path) {
|
|
754
|
+
const { apiPrefix, accessToken, isAuthenticated } = useSoundCloudContext();
|
|
755
|
+
const [data, setData] = _react.useState.call(void 0, null);
|
|
756
|
+
const [loading, setLoading] = _react.useState.call(void 0, false);
|
|
757
|
+
const [error, setError] = _react.useState.call(void 0, null);
|
|
758
|
+
_react.useEffect.call(void 0, () => {
|
|
759
|
+
if (!isAuthenticated || !accessToken) {
|
|
760
|
+
setData(null);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const controller = new AbortController();
|
|
764
|
+
setLoading(true);
|
|
765
|
+
setError(null);
|
|
766
|
+
fetch(`${apiPrefix}${path}`, {
|
|
767
|
+
signal: controller.signal,
|
|
768
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
769
|
+
}).then((res) => {
|
|
770
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
771
|
+
return res.json();
|
|
772
|
+
}).then(setData).catch((err) => {
|
|
773
|
+
if (err.name !== "AbortError") setError(err);
|
|
774
|
+
}).finally(() => setLoading(false));
|
|
775
|
+
return () => controller.abort();
|
|
776
|
+
}, [path, apiPrefix, accessToken, isAuthenticated]);
|
|
777
|
+
return { data, loading, error };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/client/hooks/useMe.ts
|
|
781
|
+
function useMe() {
|
|
782
|
+
return useAuthFetch("/me");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/client/hooks/useMeTracks.ts
|
|
786
|
+
function useMeTracks() {
|
|
787
|
+
return useAuthFetch("/me/tracks");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/client/hooks/useMeLikes.ts
|
|
791
|
+
function useMeLikes() {
|
|
792
|
+
return useAuthFetch("/me/likes");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/client/hooks/useMePlaylists.ts
|
|
796
|
+
function useMePlaylists() {
|
|
797
|
+
return useAuthFetch("/me/playlists");
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/client/hooks/useMeFollowings.ts
|
|
801
|
+
function useMeFollowings() {
|
|
802
|
+
return useAuthFetch("/me/followings");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/client/hooks/useMeFollowers.ts
|
|
806
|
+
function useMeFollowers() {
|
|
807
|
+
return useAuthFetch("/me/followers");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/client/hooks/useFollow.ts
|
|
811
|
+
|
|
812
|
+
function useFollow() {
|
|
813
|
+
const { apiPrefix, accessToken } = useSoundCloudContext();
|
|
814
|
+
const [loading, setLoading] = _react.useState.call(void 0, false);
|
|
815
|
+
const [error, setError] = _react.useState.call(void 0, null);
|
|
816
|
+
const follow = _react.useCallback.call(void 0,
|
|
817
|
+
async (userId) => {
|
|
818
|
+
if (!accessToken) throw new Error("Not authenticated");
|
|
819
|
+
setLoading(true);
|
|
820
|
+
setError(null);
|
|
821
|
+
try {
|
|
822
|
+
const res = await fetch(`${apiPrefix}/me/follow/${userId}`, {
|
|
823
|
+
method: "POST",
|
|
824
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
825
|
+
});
|
|
826
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
827
|
+
} catch (err) {
|
|
828
|
+
setError(err);
|
|
829
|
+
throw err;
|
|
830
|
+
} finally {
|
|
831
|
+
setLoading(false);
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
[apiPrefix, accessToken]
|
|
835
|
+
);
|
|
836
|
+
const unfollow = _react.useCallback.call(void 0,
|
|
837
|
+
async (userId) => {
|
|
838
|
+
if (!accessToken) throw new Error("Not authenticated");
|
|
839
|
+
setLoading(true);
|
|
840
|
+
setError(null);
|
|
841
|
+
try {
|
|
842
|
+
const res = await fetch(`${apiPrefix}/me/follow/${userId}`, {
|
|
843
|
+
method: "DELETE",
|
|
844
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
845
|
+
});
|
|
846
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
setError(err);
|
|
849
|
+
throw err;
|
|
850
|
+
} finally {
|
|
851
|
+
setLoading(false);
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
[apiPrefix, accessToken]
|
|
855
|
+
);
|
|
856
|
+
return { follow, unfollow, loading, error };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/client/hooks/useLike.ts
|
|
860
|
+
|
|
861
|
+
function useLike() {
|
|
862
|
+
const { apiPrefix, accessToken } = useSoundCloudContext();
|
|
863
|
+
const [loading, setLoading] = _react.useState.call(void 0, false);
|
|
864
|
+
const [error, setError] = _react.useState.call(void 0, null);
|
|
865
|
+
const likeTrack = _react.useCallback.call(void 0,
|
|
866
|
+
async (trackId) => {
|
|
867
|
+
if (!accessToken) throw new Error("Not authenticated");
|
|
868
|
+
setLoading(true);
|
|
869
|
+
setError(null);
|
|
870
|
+
try {
|
|
871
|
+
const res = await fetch(`${apiPrefix}/tracks/${trackId}/like`, {
|
|
872
|
+
method: "POST",
|
|
873
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
874
|
+
});
|
|
875
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
setError(err);
|
|
878
|
+
throw err;
|
|
879
|
+
} finally {
|
|
880
|
+
setLoading(false);
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
[apiPrefix, accessToken]
|
|
884
|
+
);
|
|
885
|
+
const unlikeTrack = _react.useCallback.call(void 0,
|
|
886
|
+
async (trackId) => {
|
|
887
|
+
if (!accessToken) throw new Error("Not authenticated");
|
|
888
|
+
setLoading(true);
|
|
889
|
+
setError(null);
|
|
890
|
+
try {
|
|
891
|
+
const res = await fetch(`${apiPrefix}/tracks/${trackId}/like`, {
|
|
892
|
+
method: "DELETE",
|
|
893
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
894
|
+
});
|
|
895
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
setError(err);
|
|
898
|
+
throw err;
|
|
899
|
+
} finally {
|
|
900
|
+
setLoading(false);
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
[apiPrefix, accessToken]
|
|
904
|
+
);
|
|
905
|
+
return { likeTrack, unlikeTrack, loading, error };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/client/hooks/useRepost.ts
|
|
909
|
+
|
|
910
|
+
function useRepost() {
|
|
911
|
+
const { apiPrefix, accessToken } = useSoundCloudContext();
|
|
912
|
+
const [loading, setLoading] = _react.useState.call(void 0, false);
|
|
913
|
+
const [error, setError] = _react.useState.call(void 0, null);
|
|
914
|
+
const repostTrack = _react.useCallback.call(void 0,
|
|
915
|
+
async (trackId) => {
|
|
916
|
+
if (!accessToken) throw new Error("Not authenticated");
|
|
917
|
+
setLoading(true);
|
|
918
|
+
setError(null);
|
|
919
|
+
try {
|
|
920
|
+
const res = await fetch(`${apiPrefix}/tracks/${trackId}/repost`, {
|
|
921
|
+
method: "POST",
|
|
922
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
923
|
+
});
|
|
924
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
setError(err);
|
|
927
|
+
throw err;
|
|
928
|
+
} finally {
|
|
929
|
+
setLoading(false);
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
[apiPrefix, accessToken]
|
|
933
|
+
);
|
|
934
|
+
const unrepostTrack = _react.useCallback.call(void 0,
|
|
935
|
+
async (trackId) => {
|
|
936
|
+
if (!accessToken) throw new Error("Not authenticated");
|
|
937
|
+
setLoading(true);
|
|
938
|
+
setError(null);
|
|
939
|
+
try {
|
|
940
|
+
const res = await fetch(`${apiPrefix}/tracks/${trackId}/repost`, {
|
|
941
|
+
method: "DELETE",
|
|
942
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
943
|
+
});
|
|
944
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
setError(err);
|
|
947
|
+
throw err;
|
|
948
|
+
} finally {
|
|
949
|
+
setLoading(false);
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
[apiPrefix, accessToken]
|
|
953
|
+
);
|
|
954
|
+
return { repostTrack, unrepostTrack, loading, error };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
|
|
642
967
|
|
|
643
968
|
|
|
644
969
|
|
|
@@ -667,4 +992,4 @@ function useInfinitePlaylistTracks(playlistId) {
|
|
|
667
992
|
|
|
668
993
|
|
|
669
994
|
|
|
670
|
-
exports.SoundCloudProvider = SoundCloudProvider; exports.useInfinitePlaylistSearch = useInfinitePlaylistSearch; exports.useInfinitePlaylistTracks = useInfinitePlaylistTracks; exports.useInfiniteTrackComments = useInfiniteTrackComments; exports.useInfiniteTrackSearch = useInfiniteTrackSearch; exports.useInfiniteUserFollowers = useInfiniteUserFollowers; exports.useInfiniteUserFollowings = useInfiniteUserFollowings; exports.useInfiniteUserLikes = useInfiniteUserLikes; exports.useInfiniteUserPlaylists = useInfiniteUserPlaylists; exports.useInfiniteUserSearch = useInfiniteUserSearch; exports.useInfiniteUserTracks = useInfiniteUserTracks; exports.usePlayer = usePlayer; exports.usePlaylist = usePlaylist; exports.usePlaylistSearch = usePlaylistSearch; exports.usePlaylistTracks = usePlaylistTracks; exports.useRelatedTracks = useRelatedTracks; exports.useSoundCloudContext = useSoundCloudContext; exports.useTrack = useTrack; exports.useTrackComments = useTrackComments; exports.useTrackLikes = useTrackLikes; exports.useTrackSearch = useTrackSearch; exports.useUser = useUser; exports.useUserFollowers = useUserFollowers; exports.useUserFollowings = useUserFollowings; exports.useUserLikes = useUserLikes; exports.useUserPlaylists = useUserPlaylists; exports.useUserSearch = useUserSearch; exports.useUserTracks = useUserTracks;
|
|
995
|
+
exports.SoundCloudProvider = SoundCloudProvider; exports.useFollow = useFollow; exports.useInfinitePlaylistSearch = useInfinitePlaylistSearch; exports.useInfinitePlaylistTracks = useInfinitePlaylistTracks; exports.useInfiniteTrackComments = useInfiniteTrackComments; exports.useInfiniteTrackSearch = useInfiniteTrackSearch; exports.useInfiniteUserFollowers = useInfiniteUserFollowers; exports.useInfiniteUserFollowings = useInfiniteUserFollowings; exports.useInfiniteUserLikes = useInfiniteUserLikes; exports.useInfiniteUserPlaylists = useInfiniteUserPlaylists; exports.useInfiniteUserSearch = useInfiniteUserSearch; exports.useInfiniteUserTracks = useInfiniteUserTracks; exports.useLike = useLike; exports.useMe = useMe; exports.useMeFollowers = useMeFollowers; exports.useMeFollowings = useMeFollowings; exports.useMeLikes = useMeLikes; exports.useMePlaylists = useMePlaylists; exports.useMeTracks = useMeTracks; exports.usePlayer = usePlayer; exports.usePlaylist = usePlaylist; exports.usePlaylistSearch = usePlaylistSearch; exports.usePlaylistTracks = usePlaylistTracks; exports.useRelatedTracks = useRelatedTracks; exports.useRepost = useRepost; exports.useSCAuth = useSCAuth; exports.useSoundCloudContext = useSoundCloudContext; exports.useTrack = useTrack; exports.useTrackComments = useTrackComments; exports.useTrackLikes = useTrackLikes; exports.useTrackSearch = useTrackSearch; exports.useUser = useUser; exports.useUserFollowers = useUserFollowers; exports.useUserFollowings = useUserFollowings; exports.useUserLikes = useUserLikes; exports.useUserPlaylists = useUserPlaylists; exports.useUserSearch = useUserSearch; exports.useUserTracks = useUserTracks;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import
|
|
3
|
+
import * as soundcloud_api_ts from 'soundcloud-api-ts';
|
|
4
|
+
import { SoundCloudUser, SoundCloudTrack, SoundCloudPlaylist, SoundCloudComment } from 'soundcloud-api-ts';
|
|
4
5
|
export { SoundCloudActivitiesResponse, SoundCloudActivity, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile } from 'soundcloud-api-ts';
|
|
5
6
|
|
|
6
7
|
interface SoundCloudContextValue {
|
|
7
8
|
apiPrefix: string;
|
|
9
|
+
user: SoundCloudUser | null;
|
|
10
|
+
accessToken: string | null;
|
|
11
|
+
isAuthenticated: boolean;
|
|
12
|
+
authLoading: boolean;
|
|
13
|
+
login: () => void;
|
|
14
|
+
logout: () => Promise<void>;
|
|
15
|
+
/** @internal Called by callback handler to set auth tokens */
|
|
16
|
+
_setAuth: (auth: {
|
|
17
|
+
accessToken: string;
|
|
18
|
+
refreshToken: string;
|
|
19
|
+
expiresIn: number;
|
|
20
|
+
}) => void;
|
|
8
21
|
}
|
|
9
22
|
interface SoundCloudProviderProps {
|
|
10
23
|
/** API route prefix (default: "/api/soundcloud") */
|
|
@@ -20,6 +33,24 @@ interface SoundCloudRoutesConfig {
|
|
|
20
33
|
clientId: string;
|
|
21
34
|
/** OAuth client secret */
|
|
22
35
|
clientSecret: string;
|
|
36
|
+
/** OAuth redirect URI (required for authentication features) */
|
|
37
|
+
redirectUri?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Token returned from SoundCloud OAuth. */
|
|
40
|
+
interface SoundCloudToken {
|
|
41
|
+
access_token: string;
|
|
42
|
+
refresh_token: string;
|
|
43
|
+
expires_in: number;
|
|
44
|
+
token_type: string;
|
|
45
|
+
scope: string;
|
|
46
|
+
}
|
|
47
|
+
/** Auth state for the client context. */
|
|
48
|
+
interface AuthState {
|
|
49
|
+
user: soundcloud_api_ts.SoundCloudUser | null;
|
|
50
|
+
accessToken: string | null;
|
|
51
|
+
refreshToken: string | null;
|
|
52
|
+
isAuthenticated: boolean;
|
|
53
|
+
expiresAt: number | null;
|
|
23
54
|
}
|
|
24
55
|
/** Standard hook return shape. */
|
|
25
56
|
interface HookResult<T> {
|
|
@@ -27,6 +58,12 @@ interface HookResult<T> {
|
|
|
27
58
|
loading: boolean;
|
|
28
59
|
error: Error | null;
|
|
29
60
|
}
|
|
61
|
+
/** Mutation hook return shape. */
|
|
62
|
+
interface MutationResult<TArgs extends any[] = []> {
|
|
63
|
+
execute: (...args: TArgs) => Promise<void>;
|
|
64
|
+
loading: boolean;
|
|
65
|
+
error: Error | null;
|
|
66
|
+
}
|
|
30
67
|
/** Infinite/paginated hook return shape. */
|
|
31
68
|
interface InfiniteResult<T> {
|
|
32
69
|
/** Accumulated items across all fetched pages. */
|
|
@@ -114,4 +151,59 @@ declare function useInfiniteTrackComments(trackId: string | number | null): Infi
|
|
|
114
151
|
|
|
115
152
|
declare function useInfinitePlaylistTracks(playlistId: string | number | null): InfiniteResult<SoundCloudTrack>;
|
|
116
153
|
|
|
117
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Hook for SoundCloud OAuth authentication state and actions.
|
|
156
|
+
*/
|
|
157
|
+
declare function useSCAuth(): {
|
|
158
|
+
user: soundcloud_api_ts.SoundCloudUser | null;
|
|
159
|
+
isAuthenticated: boolean;
|
|
160
|
+
loading: boolean;
|
|
161
|
+
login: () => void;
|
|
162
|
+
logout: () => Promise<void>;
|
|
163
|
+
/** Handle OAuth callback — exchange code for tokens */
|
|
164
|
+
handleCallback(code: string, state: string): Promise<any>;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Fetch the current authenticated user's profile. */
|
|
168
|
+
declare function useMe(): HookResult<SoundCloudUser>;
|
|
169
|
+
|
|
170
|
+
/** Fetch the current authenticated user's tracks. */
|
|
171
|
+
declare function useMeTracks(): HookResult<SoundCloudTrack[]>;
|
|
172
|
+
|
|
173
|
+
/** Fetch the current authenticated user's liked tracks. */
|
|
174
|
+
declare function useMeLikes(): HookResult<SoundCloudTrack[]>;
|
|
175
|
+
|
|
176
|
+
/** Fetch the current authenticated user's playlists. */
|
|
177
|
+
declare function useMePlaylists(): HookResult<SoundCloudPlaylist[]>;
|
|
178
|
+
|
|
179
|
+
/** Fetch who the current authenticated user follows. */
|
|
180
|
+
declare function useMeFollowings(): HookResult<SoundCloudUser[]>;
|
|
181
|
+
|
|
182
|
+
/** Fetch the current authenticated user's followers. */
|
|
183
|
+
declare function useMeFollowers(): HookResult<SoundCloudUser[]>;
|
|
184
|
+
|
|
185
|
+
/** Hook for follow/unfollow actions. Requires authentication. */
|
|
186
|
+
declare function useFollow(): {
|
|
187
|
+
follow: (userId: string | number) => Promise<void>;
|
|
188
|
+
unfollow: (userId: string | number) => Promise<void>;
|
|
189
|
+
loading: boolean;
|
|
190
|
+
error: Error | null;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Hook for like/unlike track actions. Requires authentication. */
|
|
194
|
+
declare function useLike(): {
|
|
195
|
+
likeTrack: (trackId: string | number) => Promise<void>;
|
|
196
|
+
unlikeTrack: (trackId: string | number) => Promise<void>;
|
|
197
|
+
loading: boolean;
|
|
198
|
+
error: Error | null;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** Hook for repost/unrepost track actions. Requires authentication. */
|
|
202
|
+
declare function useRepost(): {
|
|
203
|
+
repostTrack: (trackId: string | number) => Promise<void>;
|
|
204
|
+
unrepostTrack: (trackId: string | number) => Promise<void>;
|
|
205
|
+
loading: boolean;
|
|
206
|
+
error: Error | null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export { type AuthState, type HookResult, type InfiniteResult, type MutationResult, type PlayerState, type SoundCloudContextValue, SoundCloudProvider, type SoundCloudProviderProps, type SoundCloudRoutesConfig, type SoundCloudToken, type UseTrackSearchOptions, useFollow, useInfinitePlaylistSearch, useInfinitePlaylistTracks, useInfiniteTrackComments, useInfiniteTrackSearch, useInfiniteUserFollowers, useInfiniteUserFollowings, useInfiniteUserLikes, useInfiniteUserPlaylists, useInfiniteUserSearch, useInfiniteUserTracks, useLike, useMe, useMeFollowers, useMeFollowings, useMeLikes, useMePlaylists, useMeTracks, usePlayer, usePlaylist, usePlaylistSearch, usePlaylistTracks, useRelatedTracks, useRepost, useSCAuth, useSoundCloudContext, useTrack, useTrackComments, useTrackLikes, useTrackSearch, useUser, useUserFollowers, useUserFollowings, useUserLikes, useUserPlaylists, useUserSearch, useUserTracks };
|