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 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
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, SoundCloudContext.Provider, { value: { apiPrefix }, children });
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 { SoundCloudTrack, SoundCloudUser, SoundCloudPlaylist, SoundCloudComment } from 'soundcloud-api-ts';
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
- export { type HookResult, type InfiniteResult, type PlayerState, SoundCloudProvider, type SoundCloudProviderProps, type SoundCloudRoutesConfig, type UseTrackSearchOptions, useInfinitePlaylistSearch, useInfinitePlaylistTracks, useInfiniteTrackComments, useInfiniteTrackSearch, useInfiniteUserFollowers, useInfiniteUserFollowings, useInfiniteUserLikes, useInfiniteUserPlaylists, useInfiniteUserSearch, useInfiniteUserTracks, usePlayer, usePlaylist, usePlaylistSearch, usePlaylistTracks, useRelatedTracks, useSoundCloudContext, useTrack, useTrackComments, useTrackLikes, useTrackSearch, useUser, useUserFollowers, useUserFollowings, useUserLikes, useUserPlaylists, useUserSearch, useUserTracks };
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 };