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 +21 -0
- package/README.md +134 -0
- package/dist/index.cjs +179 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.mjs +179 -0
- package/dist/server.cjs +134 -0
- package/dist/server.d.cts +30 -0
- package/dist/server.d.ts +30 -0
- package/dist/server.mjs +134 -0
- package/package.json +63 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/server.cjs
ADDED
|
@@ -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 };
|
package/dist/server.d.ts
ADDED
|
@@ -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 };
|
package/dist/server.mjs
ADDED
|
@@ -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
|
+
}
|