soundcloud-api-ts-next 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 twin-paws
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # soundcloud-api-ts-next
2
+
3
+ React hooks and Next.js API route handlers for the SoundCloud API. Client secrets stay on the server.
4
+
5
+ Built on top of [`soundcloud-api-ts`](https://github.com/twin-paws/soundcloud-api-ts).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add soundcloud-api-ts-next soundcloud-api-ts
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Set up the API route (App Router)
16
+
17
+ ```ts
18
+ // app/api/soundcloud/[...route]/route.ts
19
+ import { createSoundCloudRoutes } from "soundcloud-api-ts-next/server";
20
+
21
+ const sc = createSoundCloudRoutes({
22
+ clientId: process.env.SC_CLIENT_ID!,
23
+ clientSecret: process.env.SC_CLIENT_SECRET!,
24
+ });
25
+
26
+ export const GET = sc.handler();
27
+ ```
28
+
29
+ ### 2. Wrap your app with the provider
30
+
31
+ ```tsx
32
+ // app/layout.tsx
33
+ import { SoundCloudProvider } from "soundcloud-api-ts-next";
34
+
35
+ export default function RootLayout({ children }) {
36
+ return (
37
+ <SoundCloudProvider apiPrefix="/api/soundcloud">
38
+ {children}
39
+ </SoundCloudProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ ### 3. Use hooks in your components
45
+
46
+ ```tsx
47
+ "use client";
48
+ import { useTrackSearch } from "soundcloud-api-ts-next";
49
+
50
+ export function Search() {
51
+ const { data, loading, error } = useTrackSearch("lofi beats", { limit: 10 });
52
+
53
+ if (loading) return <p>Loading...</p>;
54
+ if (error) return <p>Error: {error.message}</p>;
55
+
56
+ return (
57
+ <ul>
58
+ {data?.map((track) => (
59
+ <li key={track.id}>{track.title}</li>
60
+ ))}
61
+ </ul>
62
+ );
63
+ }
64
+ ```
65
+
66
+ ## Pages Router Setup
67
+
68
+ ```ts
69
+ // pages/api/soundcloud/[...route].ts
70
+ import { createSoundCloudRoutes } from "soundcloud-api-ts-next/server";
71
+
72
+ const sc = createSoundCloudRoutes({
73
+ clientId: process.env.SC_CLIENT_ID!,
74
+ clientSecret: process.env.SC_CLIENT_SECRET!,
75
+ });
76
+
77
+ export default sc.pagesHandler();
78
+ ```
79
+
80
+ ## Hooks
81
+
82
+ ### `useTrackSearch(query: string, options?: { limit?: number })`
83
+
84
+ Search for tracks. Returns `{ data: SoundCloudTrack[] | null, loading, error }`.
85
+
86
+ ### `useTrack(trackId: string | number)`
87
+
88
+ Fetch a single track by ID. Returns `{ data: SoundCloudTrack | null, loading, error }`.
89
+
90
+ ### `useUser(userId: string | number)`
91
+
92
+ Fetch a single user by ID. Returns `{ data: SoundCloudUser | null, loading, error }`.
93
+
94
+ ### `usePlayer(trackId: string | number)`
95
+
96
+ Manages an HTML5 Audio element for streaming. Returns:
97
+
98
+ ```ts
99
+ {
100
+ playing: boolean;
101
+ progress: number; // current time in seconds
102
+ duration: number; // total duration in seconds
103
+ play(): void;
104
+ pause(): void;
105
+ toggle(): void;
106
+ seek(time: number): void;
107
+ }
108
+ ```
109
+
110
+ ## API Routes
111
+
112
+ The server handler supports these routes:
113
+
114
+ | Route | Description |
115
+ |---|---|
116
+ | `/search/tracks?q=...&page=...` | Search tracks |
117
+ | `/tracks/:id` | Get track by ID |
118
+ | `/tracks/:id/stream` | Get stream URLs for a track |
119
+ | `/users/:id` | Get user by ID |
120
+ | `/users/:id/tracks` | Get user's tracks |
121
+
122
+ ## Provider
123
+
124
+ ```tsx
125
+ <SoundCloudProvider apiPrefix="/api/soundcloud">
126
+ {children}
127
+ </SoundCloudProvider>
128
+ ```
129
+
130
+ The `apiPrefix` prop configures where the hooks send requests. Default: `"/api/soundcloud"`.
131
+
132
+ ## License
133
+
134
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,179 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
+
3
+ // src/client/provider.tsx
4
+ var _react = require('react');
5
+ var _jsxruntime = require('react/jsx-runtime');
6
+ var SoundCloudContext = _react.createContext.call(void 0, {
7
+ apiPrefix: "/api/soundcloud"
8
+ });
9
+ function SoundCloudProvider({
10
+ apiPrefix = "/api/soundcloud",
11
+ children
12
+ }) {
13
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, SoundCloudContext.Provider, { value: { apiPrefix }, children });
14
+ }
15
+ function useSoundCloudContext() {
16
+ return _react.useContext.call(void 0, SoundCloudContext);
17
+ }
18
+
19
+ // src/client/hooks/useTrackSearch.ts
20
+
21
+ function useTrackSearch(query, options) {
22
+ const { apiPrefix } = useSoundCloudContext();
23
+ const [data, setData] = _react.useState.call(void 0, null);
24
+ const [loading, setLoading] = _react.useState.call(void 0, false);
25
+ const [error, setError] = _react.useState.call(void 0, null);
26
+ _react.useEffect.call(void 0, () => {
27
+ if (!query) {
28
+ setData(null);
29
+ return;
30
+ }
31
+ const controller = new AbortController();
32
+ setLoading(true);
33
+ setError(null);
34
+ const params = new URLSearchParams({ q: query });
35
+ if (_optionalChain([options, 'optionalAccess', _ => _.limit])) params.set("limit", String(options.limit));
36
+ fetch(`${apiPrefix}/search/tracks?${params}`, {
37
+ signal: controller.signal
38
+ }).then((res) => {
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
40
+ return res.json();
41
+ }).then((json) => setData(_nullishCoalesce(json.collection, () => ( json)))).catch((err) => {
42
+ if (err.name !== "AbortError") setError(err);
43
+ }).finally(() => setLoading(false));
44
+ return () => controller.abort();
45
+ }, [query, _optionalChain([options, 'optionalAccess', _2 => _2.limit]), apiPrefix]);
46
+ return { data, loading, error };
47
+ }
48
+
49
+ // src/client/hooks/useTrack.ts
50
+
51
+ function useTrack(trackId) {
52
+ const { apiPrefix } = useSoundCloudContext();
53
+ const [data, setData] = _react.useState.call(void 0, null);
54
+ const [loading, setLoading] = _react.useState.call(void 0, false);
55
+ const [error, setError] = _react.useState.call(void 0, null);
56
+ _react.useEffect.call(void 0, () => {
57
+ if (trackId == null) {
58
+ setData(null);
59
+ return;
60
+ }
61
+ const controller = new AbortController();
62
+ setLoading(true);
63
+ setError(null);
64
+ fetch(`${apiPrefix}/tracks/${trackId}`, { signal: controller.signal }).then((res) => {
65
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
66
+ return res.json();
67
+ }).then(setData).catch((err) => {
68
+ if (err.name !== "AbortError") setError(err);
69
+ }).finally(() => setLoading(false));
70
+ return () => controller.abort();
71
+ }, [trackId, apiPrefix]);
72
+ return { data, loading, error };
73
+ }
74
+
75
+ // src/client/hooks/useUser.ts
76
+
77
+ function useUser(userId) {
78
+ const { apiPrefix } = useSoundCloudContext();
79
+ const [data, setData] = _react.useState.call(void 0, null);
80
+ const [loading, setLoading] = _react.useState.call(void 0, false);
81
+ const [error, setError] = _react.useState.call(void 0, null);
82
+ _react.useEffect.call(void 0, () => {
83
+ if (userId == null) {
84
+ setData(null);
85
+ return;
86
+ }
87
+ const controller = new AbortController();
88
+ setLoading(true);
89
+ setError(null);
90
+ fetch(`${apiPrefix}/users/${userId}`, { signal: controller.signal }).then((res) => {
91
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
92
+ return res.json();
93
+ }).then(setData).catch((err) => {
94
+ if (err.name !== "AbortError") setError(err);
95
+ }).finally(() => setLoading(false));
96
+ return () => controller.abort();
97
+ }, [userId, apiPrefix]);
98
+ return { data, loading, error };
99
+ }
100
+
101
+ // src/client/hooks/usePlayer.ts
102
+
103
+ function usePlayer(trackId) {
104
+ const { apiPrefix } = useSoundCloudContext();
105
+ const audioRef = _react.useRef.call(void 0, null);
106
+ const [playing, setPlaying] = _react.useState.call(void 0, false);
107
+ const [progress, setProgress] = _react.useState.call(void 0, 0);
108
+ const [duration, setDuration] = _react.useState.call(void 0, 0);
109
+ _react.useEffect.call(void 0, () => {
110
+ if (trackId == null) return;
111
+ const controller = new AbortController();
112
+ let audio = null;
113
+ fetch(`${apiPrefix}/tracks/${trackId}/stream`, {
114
+ signal: controller.signal
115
+ }).then((res) => {
116
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
117
+ return res.json();
118
+ }).then((json) => {
119
+ const url = json.url || json.http_mp3_128_url;
120
+ if (!url) throw new Error("No stream URL returned");
121
+ audio = new Audio(url);
122
+ audioRef.current = audio;
123
+ audio.addEventListener("loadedmetadata", () => {
124
+ setDuration(audio.duration);
125
+ });
126
+ audio.addEventListener("timeupdate", () => {
127
+ setProgress(audio.currentTime);
128
+ });
129
+ audio.addEventListener("ended", () => {
130
+ setPlaying(false);
131
+ setProgress(0);
132
+ });
133
+ }).catch(() => {
134
+ });
135
+ return () => {
136
+ controller.abort();
137
+ if (audioRef.current) {
138
+ audioRef.current.pause();
139
+ audioRef.current.src = "";
140
+ audioRef.current = null;
141
+ }
142
+ setPlaying(false);
143
+ setProgress(0);
144
+ setDuration(0);
145
+ };
146
+ }, [trackId, apiPrefix]);
147
+ const play = _react.useCallback.call(void 0, () => {
148
+ _optionalChain([audioRef, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.play, 'call', _5 => _5()]);
149
+ setPlaying(true);
150
+ }, []);
151
+ const pause = _react.useCallback.call(void 0, () => {
152
+ _optionalChain([audioRef, 'access', _6 => _6.current, 'optionalAccess', _7 => _7.pause, 'call', _8 => _8()]);
153
+ setPlaying(false);
154
+ }, []);
155
+ const toggle = _react.useCallback.call(void 0, () => {
156
+ if (_optionalChain([audioRef, 'access', _9 => _9.current, 'optionalAccess', _10 => _10.paused])) {
157
+ audioRef.current.play();
158
+ setPlaying(true);
159
+ } else {
160
+ _optionalChain([audioRef, 'access', _11 => _11.current, 'optionalAccess', _12 => _12.pause, 'call', _13 => _13()]);
161
+ setPlaying(false);
162
+ }
163
+ }, []);
164
+ const seek = _react.useCallback.call(void 0, (time) => {
165
+ if (audioRef.current) {
166
+ audioRef.current.currentTime = time;
167
+ setProgress(time);
168
+ }
169
+ }, []);
170
+ return { playing, progress, duration, play, pause, toggle, seek };
171
+ }
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+ exports.SoundCloudProvider = SoundCloudProvider; exports.usePlayer = usePlayer; exports.useSoundCloudContext = useSoundCloudContext; exports.useTrack = useTrack; exports.useTrackSearch = useTrackSearch; exports.useUser = useUser;
@@ -0,0 +1,52 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { SoundCloudTrack, SoundCloudUser } from 'soundcloud-api-ts';
4
+ export { SoundCloudActivitiesResponse, SoundCloudActivity, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile } from 'soundcloud-api-ts';
5
+
6
+ interface SoundCloudContextValue {
7
+ apiPrefix: string;
8
+ }
9
+ interface SoundCloudProviderProps {
10
+ /** API route prefix (default: "/api/soundcloud") */
11
+ apiPrefix?: string;
12
+ children: ReactNode;
13
+ }
14
+ declare function SoundCloudProvider({ apiPrefix, children, }: SoundCloudProviderProps): react_jsx_runtime.JSX.Element;
15
+ declare function useSoundCloudContext(): SoundCloudContextValue;
16
+
17
+ /** Configuration for server-side route handlers. */
18
+ interface SoundCloudRoutesConfig {
19
+ /** OAuth client ID */
20
+ clientId: string;
21
+ /** OAuth client secret */
22
+ clientSecret: string;
23
+ }
24
+ /** Standard hook return shape. */
25
+ interface HookResult<T> {
26
+ data: T | null;
27
+ loading: boolean;
28
+ error: Error | null;
29
+ }
30
+ /** Player hook return shape. */
31
+ interface PlayerState {
32
+ playing: boolean;
33
+ progress: number;
34
+ duration: number;
35
+ play: () => void;
36
+ pause: () => void;
37
+ toggle: () => void;
38
+ seek: (time: number) => void;
39
+ }
40
+
41
+ interface UseTrackSearchOptions {
42
+ limit?: number;
43
+ }
44
+ declare function useTrackSearch(query: string, options?: UseTrackSearchOptions): HookResult<SoundCloudTrack[]>;
45
+
46
+ declare function useTrack(trackId: string | number | undefined): HookResult<SoundCloudTrack>;
47
+
48
+ declare function useUser(userId: string | number | undefined): HookResult<SoundCloudUser>;
49
+
50
+ declare function usePlayer(trackId: string | number | undefined): PlayerState;
51
+
52
+ export { type HookResult, type PlayerState, SoundCloudProvider, type SoundCloudProviderProps, type SoundCloudRoutesConfig, type UseTrackSearchOptions, usePlayer, useSoundCloudContext, useTrack, useTrackSearch, useUser };
@@ -0,0 +1,52 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { SoundCloudTrack, SoundCloudUser } from 'soundcloud-api-ts';
4
+ export { SoundCloudActivitiesResponse, SoundCloudActivity, SoundCloudComment, SoundCloudMe, SoundCloudPaginatedResponse, SoundCloudPlaylist, SoundCloudStreams, SoundCloudTrack, SoundCloudUser, SoundCloudWebProfile } from 'soundcloud-api-ts';
5
+
6
+ interface SoundCloudContextValue {
7
+ apiPrefix: string;
8
+ }
9
+ interface SoundCloudProviderProps {
10
+ /** API route prefix (default: "/api/soundcloud") */
11
+ apiPrefix?: string;
12
+ children: ReactNode;
13
+ }
14
+ declare function SoundCloudProvider({ apiPrefix, children, }: SoundCloudProviderProps): react_jsx_runtime.JSX.Element;
15
+ declare function useSoundCloudContext(): SoundCloudContextValue;
16
+
17
+ /** Configuration for server-side route handlers. */
18
+ interface SoundCloudRoutesConfig {
19
+ /** OAuth client ID */
20
+ clientId: string;
21
+ /** OAuth client secret */
22
+ clientSecret: string;
23
+ }
24
+ /** Standard hook return shape. */
25
+ interface HookResult<T> {
26
+ data: T | null;
27
+ loading: boolean;
28
+ error: Error | null;
29
+ }
30
+ /** Player hook return shape. */
31
+ interface PlayerState {
32
+ playing: boolean;
33
+ progress: number;
34
+ duration: number;
35
+ play: () => void;
36
+ pause: () => void;
37
+ toggle: () => void;
38
+ seek: (time: number) => void;
39
+ }
40
+
41
+ interface UseTrackSearchOptions {
42
+ limit?: number;
43
+ }
44
+ declare function useTrackSearch(query: string, options?: UseTrackSearchOptions): HookResult<SoundCloudTrack[]>;
45
+
46
+ declare function useTrack(trackId: string | number | undefined): HookResult<SoundCloudTrack>;
47
+
48
+ declare function useUser(userId: string | number | undefined): HookResult<SoundCloudUser>;
49
+
50
+ declare function usePlayer(trackId: string | number | undefined): PlayerState;
51
+
52
+ export { type HookResult, type PlayerState, SoundCloudProvider, type SoundCloudProviderProps, type SoundCloudRoutesConfig, type UseTrackSearchOptions, usePlayer, useSoundCloudContext, useTrack, useTrackSearch, useUser };
package/dist/index.mjs ADDED
@@ -0,0 +1,179 @@
1
+ "use client";
2
+
3
+ // src/client/provider.tsx
4
+ import { createContext, useContext } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ var SoundCloudContext = createContext({
7
+ apiPrefix: "/api/soundcloud"
8
+ });
9
+ function SoundCloudProvider({
10
+ apiPrefix = "/api/soundcloud",
11
+ children
12
+ }) {
13
+ return /* @__PURE__ */ jsx(SoundCloudContext.Provider, { value: { apiPrefix }, children });
14
+ }
15
+ function useSoundCloudContext() {
16
+ return useContext(SoundCloudContext);
17
+ }
18
+
19
+ // src/client/hooks/useTrackSearch.ts
20
+ import { useState, useEffect } from "react";
21
+ function useTrackSearch(query, options) {
22
+ const { apiPrefix } = useSoundCloudContext();
23
+ const [data, setData] = useState(null);
24
+ const [loading, setLoading] = useState(false);
25
+ const [error, setError] = useState(null);
26
+ useEffect(() => {
27
+ if (!query) {
28
+ setData(null);
29
+ return;
30
+ }
31
+ const controller = new AbortController();
32
+ setLoading(true);
33
+ setError(null);
34
+ const params = new URLSearchParams({ q: query });
35
+ if (options?.limit) params.set("limit", String(options.limit));
36
+ fetch(`${apiPrefix}/search/tracks?${params}`, {
37
+ signal: controller.signal
38
+ }).then((res) => {
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
40
+ return res.json();
41
+ }).then((json) => setData(json.collection ?? json)).catch((err) => {
42
+ if (err.name !== "AbortError") setError(err);
43
+ }).finally(() => setLoading(false));
44
+ return () => controller.abort();
45
+ }, [query, options?.limit, apiPrefix]);
46
+ return { data, loading, error };
47
+ }
48
+
49
+ // src/client/hooks/useTrack.ts
50
+ import { useState as useState2, useEffect as useEffect2 } from "react";
51
+ function useTrack(trackId) {
52
+ const { apiPrefix } = useSoundCloudContext();
53
+ const [data, setData] = useState2(null);
54
+ const [loading, setLoading] = useState2(false);
55
+ const [error, setError] = useState2(null);
56
+ useEffect2(() => {
57
+ if (trackId == null) {
58
+ setData(null);
59
+ return;
60
+ }
61
+ const controller = new AbortController();
62
+ setLoading(true);
63
+ setError(null);
64
+ fetch(`${apiPrefix}/tracks/${trackId}`, { signal: controller.signal }).then((res) => {
65
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
66
+ return res.json();
67
+ }).then(setData).catch((err) => {
68
+ if (err.name !== "AbortError") setError(err);
69
+ }).finally(() => setLoading(false));
70
+ return () => controller.abort();
71
+ }, [trackId, apiPrefix]);
72
+ return { data, loading, error };
73
+ }
74
+
75
+ // src/client/hooks/useUser.ts
76
+ import { useState as useState3, useEffect as useEffect3 } from "react";
77
+ function useUser(userId) {
78
+ const { apiPrefix } = useSoundCloudContext();
79
+ const [data, setData] = useState3(null);
80
+ const [loading, setLoading] = useState3(false);
81
+ const [error, setError] = useState3(null);
82
+ useEffect3(() => {
83
+ if (userId == null) {
84
+ setData(null);
85
+ return;
86
+ }
87
+ const controller = new AbortController();
88
+ setLoading(true);
89
+ setError(null);
90
+ fetch(`${apiPrefix}/users/${userId}`, { signal: controller.signal }).then((res) => {
91
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
92
+ return res.json();
93
+ }).then(setData).catch((err) => {
94
+ if (err.name !== "AbortError") setError(err);
95
+ }).finally(() => setLoading(false));
96
+ return () => controller.abort();
97
+ }, [userId, apiPrefix]);
98
+ return { data, loading, error };
99
+ }
100
+
101
+ // src/client/hooks/usePlayer.ts
102
+ import { useState as useState4, useEffect as useEffect4, useRef, useCallback } from "react";
103
+ function usePlayer(trackId) {
104
+ const { apiPrefix } = useSoundCloudContext();
105
+ const audioRef = useRef(null);
106
+ const [playing, setPlaying] = useState4(false);
107
+ const [progress, setProgress] = useState4(0);
108
+ const [duration, setDuration] = useState4(0);
109
+ useEffect4(() => {
110
+ if (trackId == null) return;
111
+ const controller = new AbortController();
112
+ let audio = null;
113
+ fetch(`${apiPrefix}/tracks/${trackId}/stream`, {
114
+ signal: controller.signal
115
+ }).then((res) => {
116
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
117
+ return res.json();
118
+ }).then((json) => {
119
+ const url = json.url || json.http_mp3_128_url;
120
+ if (!url) throw new Error("No stream URL returned");
121
+ audio = new Audio(url);
122
+ audioRef.current = audio;
123
+ audio.addEventListener("loadedmetadata", () => {
124
+ setDuration(audio.duration);
125
+ });
126
+ audio.addEventListener("timeupdate", () => {
127
+ setProgress(audio.currentTime);
128
+ });
129
+ audio.addEventListener("ended", () => {
130
+ setPlaying(false);
131
+ setProgress(0);
132
+ });
133
+ }).catch(() => {
134
+ });
135
+ return () => {
136
+ controller.abort();
137
+ if (audioRef.current) {
138
+ audioRef.current.pause();
139
+ audioRef.current.src = "";
140
+ audioRef.current = null;
141
+ }
142
+ setPlaying(false);
143
+ setProgress(0);
144
+ setDuration(0);
145
+ };
146
+ }, [trackId, apiPrefix]);
147
+ const play = useCallback(() => {
148
+ audioRef.current?.play();
149
+ setPlaying(true);
150
+ }, []);
151
+ const pause = useCallback(() => {
152
+ audioRef.current?.pause();
153
+ setPlaying(false);
154
+ }, []);
155
+ const toggle = useCallback(() => {
156
+ if (audioRef.current?.paused) {
157
+ audioRef.current.play();
158
+ setPlaying(true);
159
+ } else {
160
+ audioRef.current?.pause();
161
+ setPlaying(false);
162
+ }
163
+ }, []);
164
+ const seek = useCallback((time) => {
165
+ if (audioRef.current) {
166
+ audioRef.current.currentTime = time;
167
+ setProgress(time);
168
+ }
169
+ }, []);
170
+ return { playing, progress, duration, play, pause, toggle, seek };
171
+ }
172
+ export {
173
+ SoundCloudProvider,
174
+ usePlayer,
175
+ useSoundCloudContext,
176
+ useTrack,
177
+ useTrackSearch,
178
+ useUser
179
+ };
@@ -0,0 +1,134 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/server/routes.ts
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+ var _soundcloudapits = require('soundcloud-api-ts');
10
+ var ctx = {
11
+ config: { clientId: "", clientSecret: "" },
12
+ token: void 0,
13
+ tokenExpiry: 0
14
+ };
15
+ async function ensureToken() {
16
+ if (ctx.token && Date.now() < ctx.tokenExpiry) return ctx.token;
17
+ const result = await _soundcloudapits.getClientToken.call(void 0, ctx.config.clientId, ctx.config.clientSecret);
18
+ ctx.token = result.access_token;
19
+ ctx.tokenExpiry = Date.now() + (result.expires_in - 300) * 1e3;
20
+ return ctx.token;
21
+ }
22
+ function jsonResponse(data, status = 200) {
23
+ return new Response(JSON.stringify(data), {
24
+ status,
25
+ headers: { "Content-Type": "application/json" }
26
+ });
27
+ }
28
+ function errorResponse(message, status) {
29
+ return jsonResponse({ error: message }, status);
30
+ }
31
+ async function handleRoute(pathname, url) {
32
+ const token = await ensureToken();
33
+ if (pathname === "/search/tracks") {
34
+ const q = url.searchParams.get("q");
35
+ if (!q) return errorResponse("Missing query parameter 'q'", 400);
36
+ const page = url.searchParams.get("page");
37
+ const result = await _soundcloudapits.searchTracks.call(void 0, token, q, page ? parseInt(page, 10) : void 0);
38
+ return jsonResponse(result);
39
+ }
40
+ const streamMatch = pathname.match(/^\/tracks\/([^/]+)\/stream$/);
41
+ if (streamMatch) {
42
+ const streams = await _soundcloudapits.getTrackStreams.call(void 0, token, streamMatch[1]);
43
+ return jsonResponse(streams);
44
+ }
45
+ const trackMatch = pathname.match(/^\/tracks\/([^/]+)$/);
46
+ if (trackMatch) {
47
+ const track = await _soundcloudapits.getTrack.call(void 0, token, trackMatch[1]);
48
+ return jsonResponse(track);
49
+ }
50
+ const userTracksMatch = pathname.match(/^\/users\/([^/]+)\/tracks$/);
51
+ if (userTracksMatch) {
52
+ const limit = url.searchParams.get("limit");
53
+ const result = await _soundcloudapits.getUserTracks.call(void 0,
54
+ token,
55
+ userTracksMatch[1],
56
+ limit ? parseInt(limit, 10) : void 0
57
+ );
58
+ return jsonResponse(result);
59
+ }
60
+ const userMatch = pathname.match(/^\/users\/([^/]+)$/);
61
+ if (userMatch) {
62
+ const user = await _soundcloudapits.getUser.call(void 0, token, userMatch[1]);
63
+ return jsonResponse(user);
64
+ }
65
+ return errorResponse("Not found", 404);
66
+ }
67
+ function createSoundCloudRoutes(config) {
68
+ ctx.config = config;
69
+ ctx.token = void 0;
70
+ ctx.tokenExpiry = 0;
71
+ return {
72
+ /** Individual route handlers */
73
+ async searchTracks(q, page) {
74
+ const token = await ensureToken();
75
+ return _soundcloudapits.searchTracks.call(void 0, token, q, page);
76
+ },
77
+ async getTrack(trackId) {
78
+ const token = await ensureToken();
79
+ return _soundcloudapits.getTrack.call(void 0, token, trackId);
80
+ },
81
+ async getUser(userId) {
82
+ const token = await ensureToken();
83
+ return _soundcloudapits.getUser.call(void 0, token, userId);
84
+ },
85
+ async getUserTracks(userId, limit) {
86
+ const token = await ensureToken();
87
+ return _soundcloudapits.getUserTracks.call(void 0, token, userId, limit);
88
+ },
89
+ async getTrackStreams(trackId) {
90
+ const token = await ensureToken();
91
+ return _soundcloudapits.getTrackStreams.call(void 0, token, trackId);
92
+ },
93
+ /**
94
+ * App Router catch-all handler.
95
+ * Mount at `app/api/soundcloud/[...route]/route.ts`
96
+ */
97
+ handler() {
98
+ return async (request) => {
99
+ try {
100
+ const url = new URL(request.url);
101
+ const match = url.pathname.match(/\/api\/soundcloud(\/.*)/);
102
+ const route = match ? match[1] : url.pathname;
103
+ return await handleRoute(route, url);
104
+ } catch (err) {
105
+ const status = _nullishCoalesce(_optionalChain([err, 'optionalAccess', _ => _.statusCode]), () => ( 500));
106
+ return errorResponse(_nullishCoalesce(_optionalChain([err, 'optionalAccess', _2 => _2.message]), () => ( "Internal server error")), status);
107
+ }
108
+ };
109
+ },
110
+ /**
111
+ * Pages Router catch-all handler.
112
+ * Mount at `pages/api/soundcloud/[...route].ts`
113
+ */
114
+ pagesHandler() {
115
+ return async (req, res) => {
116
+ try {
117
+ const route = Array.isArray(req.query.route) ? "/" + req.query.route.join("/") : _nullishCoalesce(_optionalChain([req, 'access', _3 => _3.url, 'optionalAccess', _4 => _4.replace, 'call', _5 => _5(/^\/api\/soundcloud/, "")]), () => ( "/"));
118
+ const protocol = req.headers["x-forwarded-proto"] || "http";
119
+ const host = req.headers.host || "localhost";
120
+ const url = new URL(`${protocol}://${host}${req.url}`);
121
+ const response = await handleRoute(route, url);
122
+ const body = await response.json();
123
+ res.status(response.status).json(body);
124
+ } catch (err) {
125
+ const status = _nullishCoalesce(_optionalChain([err, 'optionalAccess', _6 => _6.statusCode]), () => ( 500));
126
+ res.status(status).json({ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _7 => _7.message]), () => ( "Internal server error")) });
127
+ }
128
+ };
129
+ }
130
+ };
131
+ }
132
+
133
+
134
+ exports.createSoundCloudRoutes = createSoundCloudRoutes;
@@ -0,0 +1,30 @@
1
+ import * as soundcloud_api_ts from 'soundcloud-api-ts';
2
+
3
+ /** Configuration for server-side route handlers. */
4
+ interface SoundCloudRoutesConfig {
5
+ /** OAuth client ID */
6
+ clientId: string;
7
+ /** OAuth client secret */
8
+ clientSecret: string;
9
+ }
10
+
11
+ declare function createSoundCloudRoutes(config: SoundCloudRoutesConfig): {
12
+ /** Individual route handlers */
13
+ searchTracks(q: string, page?: number): Promise<soundcloud_api_ts.SoundCloudPaginatedResponse<soundcloud_api_ts.SoundCloudTrack>>;
14
+ getTrack(trackId: string | number): Promise<soundcloud_api_ts.SoundCloudTrack>;
15
+ getUser(userId: string | number): Promise<soundcloud_api_ts.SoundCloudUser>;
16
+ getUserTracks(userId: string | number, limit?: number): Promise<soundcloud_api_ts.SoundCloudPaginatedResponse<soundcloud_api_ts.SoundCloudTrack>>;
17
+ getTrackStreams(trackId: string | number): Promise<soundcloud_api_ts.SoundCloudStreams>;
18
+ /**
19
+ * App Router catch-all handler.
20
+ * Mount at `app/api/soundcloud/[...route]/route.ts`
21
+ */
22
+ handler(): (request: Request) => Promise<Response>;
23
+ /**
24
+ * Pages Router catch-all handler.
25
+ * Mount at `pages/api/soundcloud/[...route].ts`
26
+ */
27
+ pagesHandler(): (req: any, res: any) => Promise<void>;
28
+ };
29
+
30
+ export { type SoundCloudRoutesConfig, createSoundCloudRoutes };
@@ -0,0 +1,30 @@
1
+ import * as soundcloud_api_ts from 'soundcloud-api-ts';
2
+
3
+ /** Configuration for server-side route handlers. */
4
+ interface SoundCloudRoutesConfig {
5
+ /** OAuth client ID */
6
+ clientId: string;
7
+ /** OAuth client secret */
8
+ clientSecret: string;
9
+ }
10
+
11
+ declare function createSoundCloudRoutes(config: SoundCloudRoutesConfig): {
12
+ /** Individual route handlers */
13
+ searchTracks(q: string, page?: number): Promise<soundcloud_api_ts.SoundCloudPaginatedResponse<soundcloud_api_ts.SoundCloudTrack>>;
14
+ getTrack(trackId: string | number): Promise<soundcloud_api_ts.SoundCloudTrack>;
15
+ getUser(userId: string | number): Promise<soundcloud_api_ts.SoundCloudUser>;
16
+ getUserTracks(userId: string | number, limit?: number): Promise<soundcloud_api_ts.SoundCloudPaginatedResponse<soundcloud_api_ts.SoundCloudTrack>>;
17
+ getTrackStreams(trackId: string | number): Promise<soundcloud_api_ts.SoundCloudStreams>;
18
+ /**
19
+ * App Router catch-all handler.
20
+ * Mount at `app/api/soundcloud/[...route]/route.ts`
21
+ */
22
+ handler(): (request: Request) => Promise<Response>;
23
+ /**
24
+ * Pages Router catch-all handler.
25
+ * Mount at `pages/api/soundcloud/[...route].ts`
26
+ */
27
+ pagesHandler(): (req: any, res: any) => Promise<void>;
28
+ };
29
+
30
+ export { type SoundCloudRoutesConfig, createSoundCloudRoutes };
@@ -0,0 +1,134 @@
1
+ // src/server/routes.ts
2
+ import {
3
+ getClientToken,
4
+ searchTracks,
5
+ getTrack,
6
+ getUser,
7
+ getUserTracks,
8
+ getTrackStreams
9
+ } from "soundcloud-api-ts";
10
+ var ctx = {
11
+ config: { clientId: "", clientSecret: "" },
12
+ token: void 0,
13
+ tokenExpiry: 0
14
+ };
15
+ async function ensureToken() {
16
+ if (ctx.token && Date.now() < ctx.tokenExpiry) return ctx.token;
17
+ const result = await getClientToken(ctx.config.clientId, ctx.config.clientSecret);
18
+ ctx.token = result.access_token;
19
+ ctx.tokenExpiry = Date.now() + (result.expires_in - 300) * 1e3;
20
+ return ctx.token;
21
+ }
22
+ function jsonResponse(data, status = 200) {
23
+ return new Response(JSON.stringify(data), {
24
+ status,
25
+ headers: { "Content-Type": "application/json" }
26
+ });
27
+ }
28
+ function errorResponse(message, status) {
29
+ return jsonResponse({ error: message }, status);
30
+ }
31
+ async function handleRoute(pathname, url) {
32
+ const token = await ensureToken();
33
+ if (pathname === "/search/tracks") {
34
+ const q = url.searchParams.get("q");
35
+ if (!q) return errorResponse("Missing query parameter 'q'", 400);
36
+ const page = url.searchParams.get("page");
37
+ const result = await searchTracks(token, q, page ? parseInt(page, 10) : void 0);
38
+ return jsonResponse(result);
39
+ }
40
+ const streamMatch = pathname.match(/^\/tracks\/([^/]+)\/stream$/);
41
+ if (streamMatch) {
42
+ const streams = await getTrackStreams(token, streamMatch[1]);
43
+ return jsonResponse(streams);
44
+ }
45
+ const trackMatch = pathname.match(/^\/tracks\/([^/]+)$/);
46
+ if (trackMatch) {
47
+ const track = await getTrack(token, trackMatch[1]);
48
+ return jsonResponse(track);
49
+ }
50
+ const userTracksMatch = pathname.match(/^\/users\/([^/]+)\/tracks$/);
51
+ if (userTracksMatch) {
52
+ const limit = url.searchParams.get("limit");
53
+ const result = await getUserTracks(
54
+ token,
55
+ userTracksMatch[1],
56
+ limit ? parseInt(limit, 10) : void 0
57
+ );
58
+ return jsonResponse(result);
59
+ }
60
+ const userMatch = pathname.match(/^\/users\/([^/]+)$/);
61
+ if (userMatch) {
62
+ const user = await getUser(token, userMatch[1]);
63
+ return jsonResponse(user);
64
+ }
65
+ return errorResponse("Not found", 404);
66
+ }
67
+ function createSoundCloudRoutes(config) {
68
+ ctx.config = config;
69
+ ctx.token = void 0;
70
+ ctx.tokenExpiry = 0;
71
+ return {
72
+ /** Individual route handlers */
73
+ async searchTracks(q, page) {
74
+ const token = await ensureToken();
75
+ return searchTracks(token, q, page);
76
+ },
77
+ async getTrack(trackId) {
78
+ const token = await ensureToken();
79
+ return getTrack(token, trackId);
80
+ },
81
+ async getUser(userId) {
82
+ const token = await ensureToken();
83
+ return getUser(token, userId);
84
+ },
85
+ async getUserTracks(userId, limit) {
86
+ const token = await ensureToken();
87
+ return getUserTracks(token, userId, limit);
88
+ },
89
+ async getTrackStreams(trackId) {
90
+ const token = await ensureToken();
91
+ return getTrackStreams(token, trackId);
92
+ },
93
+ /**
94
+ * App Router catch-all handler.
95
+ * Mount at `app/api/soundcloud/[...route]/route.ts`
96
+ */
97
+ handler() {
98
+ return async (request) => {
99
+ try {
100
+ const url = new URL(request.url);
101
+ const match = url.pathname.match(/\/api\/soundcloud(\/.*)/);
102
+ const route = match ? match[1] : url.pathname;
103
+ return await handleRoute(route, url);
104
+ } catch (err) {
105
+ const status = err?.statusCode ?? 500;
106
+ return errorResponse(err?.message ?? "Internal server error", status);
107
+ }
108
+ };
109
+ },
110
+ /**
111
+ * Pages Router catch-all handler.
112
+ * Mount at `pages/api/soundcloud/[...route].ts`
113
+ */
114
+ pagesHandler() {
115
+ return async (req, res) => {
116
+ try {
117
+ const route = Array.isArray(req.query.route) ? "/" + req.query.route.join("/") : req.url?.replace(/^\/api\/soundcloud/, "") ?? "/";
118
+ const protocol = req.headers["x-forwarded-proto"] || "http";
119
+ const host = req.headers.host || "localhost";
120
+ const url = new URL(`${protocol}://${host}${req.url}`);
121
+ const response = await handleRoute(route, url);
122
+ const body = await response.json();
123
+ res.status(response.status).json(body);
124
+ } catch (err) {
125
+ const status = err?.statusCode ?? 500;
126
+ res.status(status).json({ error: err?.message ?? "Internal server error" });
127
+ }
128
+ };
129
+ }
130
+ };
131
+ }
132
+ export {
133
+ createSoundCloudRoutes
134
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "soundcloud-api-ts-next",
3
+ "version": "1.0.0",
4
+ "description": "Next.js integration for soundcloud-api-ts — React hooks + secure API route handlers",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/twin-paws/soundcloud-api-ts-next"
9
+ },
10
+ "homepage": "https://github.com/twin-paws/soundcloud-api-ts-next#readme",
11
+ "keywords": [
12
+ "soundcloud",
13
+ "nextjs",
14
+ "react",
15
+ "hooks",
16
+ "soundcloud-api",
17
+ "typescript",
18
+ "soundcloud-next"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.cjs"
26
+ },
27
+ "./server": {
28
+ "types": "./dist/server.d.ts",
29
+ "import": "./dist/server.mjs",
30
+ "require": "./dist/server.cjs"
31
+ }
32
+ },
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.mjs",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "typecheck": "tsc --noEmit"
43
+ },
44
+ "dependencies": {
45
+ "soundcloud-api-ts": "^1.9.1"
46
+ },
47
+ "peerDependencies": {
48
+ "react": "^18.0.0 || ^19.0.0",
49
+ "next": "^13.0.0 || ^14.0.0 || ^15.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "next": {
53
+ "optional": false
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "typescript": "^5.7.0",
58
+ "tsup": "^8.0.0",
59
+ "@types/react": "^19.0.0",
60
+ "react": "^19.0.0",
61
+ "next": "^15.0.0"
62
+ }
63
+ }