@manhgdev/soundcloud-package 0.1.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/.babelrc +12 -0
- package/.eslintrc.json +17 -0
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/bun.lock +914 -0
- package/dist/index.js +867 -0
- package/package.json +38 -0
- package/src/index.js +203 -0
- package/src/modules/discover.js +19 -0
- package/src/modules/media.js +66 -0
- package/src/modules/playlists.js +26 -0
- package/src/modules/search.js +52 -0
- package/src/modules/tracks.js +56 -0
- package/src/modules/users.js +58 -0
- package/tests/client-id.test.js +12 -0
- package/tests/discover.test.js +35 -0
- package/tests/find-ids.test.js +14 -0
- package/tests/media.test.js +27 -0
- package/tests/playlists.test.js +45 -0
- package/tests/search.test.js +70 -0
- package/tests/tracks.test.js +27 -0
- package/tests/users.test.js +49 -0
package/package.json
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
{
|
2
|
+
"name": "@manhgdev/soundcloud-package",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"description": "JavaScript wrapper for SoundCloud API",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"type": "module",
|
7
|
+
"exports": {
|
8
|
+
".": {
|
9
|
+
"import": "./dist/index.js"
|
10
|
+
}
|
11
|
+
},
|
12
|
+
"scripts": {
|
13
|
+
"build": "bun build ./src/index.js --outdir ./dist --target node",
|
14
|
+
"prod": "npm publish",
|
15
|
+
"test": "bun test",
|
16
|
+
"lint": "eslint src --ext .js",
|
17
|
+
"format": "prettier --write \"src/**/*.js\""
|
18
|
+
},
|
19
|
+
"keywords": [
|
20
|
+
"soundcloud",
|
21
|
+
"api",
|
22
|
+
"music",
|
23
|
+
"streaming",
|
24
|
+
"audio"
|
25
|
+
],
|
26
|
+
"author": "manhg dev",
|
27
|
+
"license": "MIT",
|
28
|
+
"dependencies": {
|
29
|
+
"query-string": "^8.1.0"
|
30
|
+
},
|
31
|
+
"devDependencies": {
|
32
|
+
"eslint": "^8.0.0",
|
33
|
+
"prettier": "^3.0.0"
|
34
|
+
},
|
35
|
+
"engines": {
|
36
|
+
"node": ">=18.0.0"
|
37
|
+
}
|
38
|
+
}
|
package/src/index.js
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
import queryString from "query-string";
|
2
|
+
|
3
|
+
import SearchModule from "./modules/search.js";
|
4
|
+
import TracksModule from "./modules/tracks.js";
|
5
|
+
import UsersModule from "./modules/users.js";
|
6
|
+
import PlaylistsModule from "./modules/playlists.js";
|
7
|
+
import MediaModule from "./modules/media.js";
|
8
|
+
import DiscoverModule from "./modules/discover.js";
|
9
|
+
|
10
|
+
class SoundCloudAPI {
|
11
|
+
constructor(options = {}) {
|
12
|
+
// Chỉ sử dụng client ID được cung cấp, không có giá trị mặc định
|
13
|
+
this.clientId = options.clientId;
|
14
|
+
this.baseURL = "https://api-v2.soundcloud.com";
|
15
|
+
this.appVersion = options.appVersion || "1753870647";
|
16
|
+
this.appLocale = options.appLocale || "en";
|
17
|
+
this.autoFetchClientId = options.autoFetchClientId !== false;
|
18
|
+
this.clientIdCache = {
|
19
|
+
value: this.clientId,
|
20
|
+
expirationTime: this.clientId ? Date.now() + 360000 : 0, // Nếu không có clientId, đặt expirationTime = 0 để buộc fetch
|
21
|
+
};
|
22
|
+
this.clientIdPromise = null; // Track ongoing client ID fetch requests
|
23
|
+
|
24
|
+
this.headers = {
|
25
|
+
"User-Agent":
|
26
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
27
|
+
Accept: "application/json, text/javascript, */*; q=0.01",
|
28
|
+
Origin: "https://soundcloud.com",
|
29
|
+
Referer: "https://soundcloud.com/",
|
30
|
+
"Accept-Language": "en,vi;q=0.9,en-US;q=0.8",
|
31
|
+
};
|
32
|
+
|
33
|
+
this.search = new SearchModule(this);
|
34
|
+
this.tracks = new TracksModule(this);
|
35
|
+
this.users = new UsersModule(this);
|
36
|
+
this.playlists = new PlaylistsModule(this);
|
37
|
+
this.media = new MediaModule(this);
|
38
|
+
this.discover = new DiscoverModule(this);
|
39
|
+
|
40
|
+
// Luôn fetch client ID khi autoFetchClientId là true
|
41
|
+
if (this.autoFetchClientId) {
|
42
|
+
// Không await ở đây để tránh làm constructor trở thành async
|
43
|
+
this._initClientId();
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
async _initClientId() {
|
48
|
+
try {
|
49
|
+
const newClientId = await this.getClientId(); // Use getClientId to avoid duplication
|
50
|
+
} catch (error) {
|
51
|
+
console.error(
|
52
|
+
"[SOUNDCLOUD] Error initializing client ID:",
|
53
|
+
error.message,
|
54
|
+
);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
async getClientId() {
|
59
|
+
// Nếu không có clientId hoặc cache đã hết hạn, fetch mới
|
60
|
+
if (
|
61
|
+
this.autoFetchClientId &&
|
62
|
+
(!this.clientId || Date.now() > this.clientIdCache.expirationTime)
|
63
|
+
) {
|
64
|
+
try {
|
65
|
+
// If there's already a fetch in progress, return that promise
|
66
|
+
if (this.clientIdPromise) {
|
67
|
+
return await this.clientIdPromise;
|
68
|
+
}
|
69
|
+
|
70
|
+
// Create a new promise for fetching the client ID
|
71
|
+
this.clientIdPromise = this.fetchClientIdFromWeb();
|
72
|
+
|
73
|
+
const newClientId = await this.clientIdPromise;
|
74
|
+
if (newClientId) {
|
75
|
+
this.clientId = newClientId;
|
76
|
+
this.clientIdCache = {
|
77
|
+
value: newClientId,
|
78
|
+
expirationTime: Date.now() + 180000, // 3 minutes
|
79
|
+
};
|
80
|
+
}
|
81
|
+
|
82
|
+
// Clear the promise after it's resolved
|
83
|
+
this.clientIdPromise = null;
|
84
|
+
} catch (error) {
|
85
|
+
// Clear the promise if there's an error
|
86
|
+
this.clientIdPromise = null;
|
87
|
+
console.error("[SOUNDCLOUD] Error fetching client ID:", error.message);
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
return this.clientId;
|
92
|
+
}
|
93
|
+
|
94
|
+
async fetchClientIdFromWeb() {
|
95
|
+
const webURL = "https://www.soundcloud.com/";
|
96
|
+
let script = "";
|
97
|
+
|
98
|
+
try {
|
99
|
+
console.log("[SOUNDCLOUD] Fetching client ID from web...");
|
100
|
+
// Fetch the main SoundCloud page
|
101
|
+
const response = await fetch(webURL, {
|
102
|
+
headers: this.headers,
|
103
|
+
});
|
104
|
+
|
105
|
+
if (!response.ok) {
|
106
|
+
throw new Error(`Failed to fetch SoundCloud page: ${response.status}`);
|
107
|
+
}
|
108
|
+
|
109
|
+
const html = await response.text();
|
110
|
+
|
111
|
+
// Extract script URLs
|
112
|
+
const scriptUrlRegex =
|
113
|
+
/(?!<script crossorigin src=")https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*\.js)(?=">)/g;
|
114
|
+
const urls = html.match(scriptUrlRegex) || [];
|
115
|
+
|
116
|
+
if (urls.length === 0) {
|
117
|
+
throw new Error("No script URLs found in SoundCloud page");
|
118
|
+
}
|
119
|
+
|
120
|
+
// Find the script containing the client ID
|
121
|
+
for (let i = urls.length - 1; i >= 0; i--) {
|
122
|
+
const scriptUrl = urls[i];
|
123
|
+
// console.log(`[SOUNDCLOUD] Checking script: ${scriptUrl}`);
|
124
|
+
|
125
|
+
const scriptResponse = await fetch(scriptUrl, {
|
126
|
+
headers: this.headers,
|
127
|
+
});
|
128
|
+
|
129
|
+
if (!scriptResponse.ok) continue;
|
130
|
+
|
131
|
+
script = await scriptResponse.text();
|
132
|
+
|
133
|
+
if (script.includes(',client_id:"')) {
|
134
|
+
const match = script.match(/,client_id:"(\w+)"/);
|
135
|
+
if (match && match[1]) {
|
136
|
+
const clientId = match[1];
|
137
|
+
// console.log(`[SOUNDCLOUD] Found client ID: ${clientId}`);
|
138
|
+
return clientId;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
// Nếu không tìm thấy client ID trong bất kỳ script nào
|
144
|
+
throw new Error("Client ID not found in any script");
|
145
|
+
} catch (error) {
|
146
|
+
console.error(
|
147
|
+
"[SOUNDCLOUD] Error in fetchClientIdFromWeb:",
|
148
|
+
error.message,
|
149
|
+
);
|
150
|
+
throw error; // Ném lại lỗi để caller xử lý
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
async request(endpoint, params = {}) {
|
155
|
+
// Get the latest client ID before making the request
|
156
|
+
const clientId = await this.getClientId();
|
157
|
+
|
158
|
+
if (!clientId) {
|
159
|
+
throw new Error(
|
160
|
+
"SoundCloud API Error: No client ID available. Please provide a client ID or enable autoFetchClientId.",
|
161
|
+
);
|
162
|
+
}
|
163
|
+
|
164
|
+
const defaultParams = {
|
165
|
+
client_id: clientId,
|
166
|
+
app_version: this.appVersion,
|
167
|
+
app_locale: this.appLocale,
|
168
|
+
};
|
169
|
+
|
170
|
+
const queryParams = queryString.stringify({
|
171
|
+
...defaultParams,
|
172
|
+
...params,
|
173
|
+
});
|
174
|
+
|
175
|
+
const url = `${this.baseURL}${endpoint}?${queryParams}`;
|
176
|
+
|
177
|
+
try {
|
178
|
+
const response = await fetch(url, {
|
179
|
+
method: "GET",
|
180
|
+
headers: this.headers,
|
181
|
+
});
|
182
|
+
|
183
|
+
if (!response.ok) {
|
184
|
+
const errorData = await response.json().catch(() => ({}));
|
185
|
+
throw new Error(
|
186
|
+
`SoundCloud API Error: ${response.status} - ${JSON.stringify(errorData)}`,
|
187
|
+
);
|
188
|
+
}
|
189
|
+
|
190
|
+
return await response.json();
|
191
|
+
} catch (error) {
|
192
|
+
if (error.name === "AbortError") {
|
193
|
+
throw new Error("SoundCloud API Error: Request was aborted");
|
194
|
+
} else if (error.name === "TypeError") {
|
195
|
+
throw new Error("SoundCloud API Error: Network error");
|
196
|
+
} else {
|
197
|
+
throw error;
|
198
|
+
}
|
199
|
+
}
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
export default SoundCloudAPI;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class DiscoverModule {
|
2
|
+
constructor(api) {
|
3
|
+
this.api = api;
|
4
|
+
}
|
5
|
+
|
6
|
+
getHomeContent() {
|
7
|
+
return this.api.request("/mixed-selections");
|
8
|
+
}
|
9
|
+
|
10
|
+
getRecentTracks(genre = "all genres") {
|
11
|
+
return this.api.request(`/recent-tracks/${encodeURIComponent(genre)}`);
|
12
|
+
}
|
13
|
+
|
14
|
+
getRecentTracksByCountry() {
|
15
|
+
return this.api.request("/recent-tracks/country");
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
export default DiscoverModule;
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class MediaModule {
|
2
|
+
constructor(api) {
|
3
|
+
this.api = api;
|
4
|
+
}
|
5
|
+
|
6
|
+
getStreamURL(mediaUrl, trackAuthorization, clientId) {
|
7
|
+
return `${mediaUrl}?client_id=${clientId}&track_authorization=${trackAuthorization}`;
|
8
|
+
}
|
9
|
+
|
10
|
+
async getPlaybackUrl(trackId) {
|
11
|
+
try {
|
12
|
+
return await this.getMediaUrl(trackId, "hls");
|
13
|
+
} catch (error) {
|
14
|
+
throw new Error(`Failed to get playback URL: ${error.message}`);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
async getDownloadUrl(trackId) {
|
19
|
+
try {
|
20
|
+
return await this.getMediaUrl(trackId, "progressive");
|
21
|
+
} catch (error) {
|
22
|
+
throw new Error(`Failed to get download URL: ${error.message}`);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
async getMediaUrl(trackId, protocol = "hls") {
|
27
|
+
try {
|
28
|
+
const track = await this.api.tracks.getMultiple([trackId]);
|
29
|
+
|
30
|
+
if (!track || !track.length || !track[0]) {
|
31
|
+
throw new Error("Track not found");
|
32
|
+
}
|
33
|
+
|
34
|
+
const trackData = track[0];
|
35
|
+
|
36
|
+
if (
|
37
|
+
!trackData.media ||
|
38
|
+
!trackData.media.transcodings ||
|
39
|
+
!trackData.media.transcodings.length
|
40
|
+
) {
|
41
|
+
throw new Error("No media transcodings available for this track");
|
42
|
+
}
|
43
|
+
|
44
|
+
const transcoding = trackData.media.transcodings.find(
|
45
|
+
(t) =>
|
46
|
+
t.format.protocol === protocol && t.format.mime_type === "audio/mpeg",
|
47
|
+
);
|
48
|
+
|
49
|
+
if (!transcoding) {
|
50
|
+
throw new Error(`No suitable ${protocol} media transcoding found`);
|
51
|
+
}
|
52
|
+
|
53
|
+
const mediaUrl = transcoding.url;
|
54
|
+
|
55
|
+
return this.getStreamURL(
|
56
|
+
mediaUrl,
|
57
|
+
trackData.track_authorization,
|
58
|
+
this.api.clientId,
|
59
|
+
);
|
60
|
+
} catch (error) {
|
61
|
+
throw new Error(`Failed to get media URL: ${error.message}`);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
export default MediaModule;
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class PlaylistsModule {
|
2
|
+
constructor(api) {
|
3
|
+
this.api = api;
|
4
|
+
}
|
5
|
+
|
6
|
+
getPlaylist(playlistId, options = {}) {
|
7
|
+
return this.api.request(`/playlists/${playlistId}`, options);
|
8
|
+
}
|
9
|
+
|
10
|
+
getLikers(playlistId, options = {}) {
|
11
|
+
return this.api.request(`/playlists/${playlistId}/likers`, options);
|
12
|
+
}
|
13
|
+
|
14
|
+
getReposters(playlistId, options = {}) {
|
15
|
+
return this.api.request(`/playlists/${playlistId}/reposters`, options);
|
16
|
+
}
|
17
|
+
|
18
|
+
getByGenre(genre, options = {}) {
|
19
|
+
return this.api.request("/playlists/discovery", {
|
20
|
+
tag: genre,
|
21
|
+
...options,
|
22
|
+
});
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export default PlaylistsModule;
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class SearchModule {
|
2
|
+
constructor(api) {
|
3
|
+
this.api = api;
|
4
|
+
}
|
5
|
+
|
6
|
+
all(query, options = {}) {
|
7
|
+
return this.api.request("/search", {
|
8
|
+
q: query,
|
9
|
+
...options,
|
10
|
+
});
|
11
|
+
}
|
12
|
+
|
13
|
+
tracks(query, options = {}) {
|
14
|
+
const params = {
|
15
|
+
q: query,
|
16
|
+
...options,
|
17
|
+
};
|
18
|
+
|
19
|
+
return this.api.request("/search/tracks", params);
|
20
|
+
}
|
21
|
+
|
22
|
+
users(query, options = {}) {
|
23
|
+
return this.api.request("/search/users", {
|
24
|
+
q: query,
|
25
|
+
...options,
|
26
|
+
});
|
27
|
+
}
|
28
|
+
|
29
|
+
albums(query, options = {}) {
|
30
|
+
return this.api.request("/search/albums", {
|
31
|
+
q: query,
|
32
|
+
...options,
|
33
|
+
});
|
34
|
+
}
|
35
|
+
|
36
|
+
playlists(query, options = {}) {
|
37
|
+
return this.api.request("/search/playlists_without_albums", {
|
38
|
+
q: query,
|
39
|
+
...options,
|
40
|
+
});
|
41
|
+
}
|
42
|
+
|
43
|
+
byGenre(genre, options = {}) {
|
44
|
+
return this.tracks("*", {
|
45
|
+
"filter.genre_or_tag": genre,
|
46
|
+
sort: "popular",
|
47
|
+
...options,
|
48
|
+
});
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
export default SearchModule;
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class TracksModule {
|
2
|
+
constructor(api) {
|
3
|
+
this.api = api;
|
4
|
+
}
|
5
|
+
|
6
|
+
getMultiple(ids) {
|
7
|
+
if (!Array.isArray(ids)) {
|
8
|
+
throw new Error("IDs must be an array");
|
9
|
+
}
|
10
|
+
|
11
|
+
return this.api.request("/tracks", {
|
12
|
+
ids: ids.join(","),
|
13
|
+
});
|
14
|
+
}
|
15
|
+
|
16
|
+
getComments(trackId, options = {}) {
|
17
|
+
return this.api.request(`/tracks/${trackId}/comments`, options);
|
18
|
+
}
|
19
|
+
|
20
|
+
getRelated(trackId, options = {}) {
|
21
|
+
return this.api.request(`/tracks/${trackId}/related`, options);
|
22
|
+
}
|
23
|
+
|
24
|
+
async getStreamUrl(trackId) {
|
25
|
+
try {
|
26
|
+
const trackInfo = await this.api.request(`/tracks/${trackId}`);
|
27
|
+
|
28
|
+
if (!trackInfo || !trackInfo.media || !trackInfo.media.transcodings) {
|
29
|
+
throw new Error("Track streaming information not available");
|
30
|
+
}
|
31
|
+
|
32
|
+
const hlsTranscoding = trackInfo.media.transcodings.find(
|
33
|
+
(t) =>
|
34
|
+
t.format.protocol === "hls" && t.format.mime_type === "audio/mpeg",
|
35
|
+
);
|
36
|
+
|
37
|
+
if (!hlsTranscoding) {
|
38
|
+
throw new Error("HLS streaming not available for this track");
|
39
|
+
}
|
40
|
+
|
41
|
+
const mediaUrl = hlsTranscoding.url;
|
42
|
+
const mediaTranscodingId = mediaUrl.split("/").pop();
|
43
|
+
|
44
|
+
const streamInfo = await this.api.request(
|
45
|
+
`/media/soundcloud:tracks:${trackId}/${mediaTranscodingId}/stream/hls`,
|
46
|
+
{ track_authorization: trackInfo.track_authorization },
|
47
|
+
);
|
48
|
+
|
49
|
+
return streamInfo.url;
|
50
|
+
} catch (error) {
|
51
|
+
throw new Error(`Failed to get stream URL: ${error.message}`);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
export default TracksModule;
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class UsersModule {
|
2
|
+
constructor(api) {
|
3
|
+
this.api = api;
|
4
|
+
}
|
5
|
+
|
6
|
+
getUser(userId) {
|
7
|
+
return this.api.request(`/users/${userId}`);
|
8
|
+
}
|
9
|
+
|
10
|
+
getSpotlight(userId) {
|
11
|
+
return this.api.request(`/users/${userId}/spotlight`);
|
12
|
+
}
|
13
|
+
|
14
|
+
getFeaturedProfiles(userId) {
|
15
|
+
return this.api.request(`/users/${userId}/featured-profiles`);
|
16
|
+
}
|
17
|
+
|
18
|
+
getLikes(userId, options = {}) {
|
19
|
+
return this.api.request(`/users/${userId}/likes`, options);
|
20
|
+
}
|
21
|
+
|
22
|
+
getFollowings(userId, options = {}) {
|
23
|
+
return this.api.request(`/users/${userId}/followings`, options);
|
24
|
+
}
|
25
|
+
|
26
|
+
getRelatedArtists(userId, options = {}) {
|
27
|
+
return this.api.request(`/users/${userId}/relatedartists`, options);
|
28
|
+
}
|
29
|
+
|
30
|
+
getComments(userId, options = {}) {
|
31
|
+
return this.api.request(`/users/${userId}/comments`, options);
|
32
|
+
}
|
33
|
+
|
34
|
+
getStream(userId, options = {}) {
|
35
|
+
return this.api.request(`/stream/users/${userId}`, options);
|
36
|
+
}
|
37
|
+
|
38
|
+
getTopTracks(userId, options = {}) {
|
39
|
+
return this.api.request(`/users/${userId}/toptracks`, options);
|
40
|
+
}
|
41
|
+
|
42
|
+
getTracks(userId, options = {}) {
|
43
|
+
return this.api.request(`/users/${userId}/tracks`, options);
|
44
|
+
}
|
45
|
+
|
46
|
+
getPlaylists(userId, options = {}) {
|
47
|
+
return this.api.request(
|
48
|
+
`/users/${userId}/playlists_without_albums`,
|
49
|
+
options,
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
getWebProfiles(userId) {
|
54
|
+
return this.api.request(`/users/soundcloud:users:${userId}/web-profiles`);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
export default UsersModule;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { test, expect } from "bun:test";
|
2
|
+
import SoundCloudAPI from '../src/index.js';
|
3
|
+
|
4
|
+
test('getClientId should return a client ID', async () => {
|
5
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
6
|
+
|
7
|
+
const clientId = await api.getClientId();
|
8
|
+
console.log("Final Client ID:", clientId);
|
9
|
+
|
10
|
+
expect(typeof clientId).toBe('string');
|
11
|
+
expect(clientId.length).toBeGreaterThan(0);
|
12
|
+
});
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { test, expect } from "bun:test";
|
2
|
+
import SoundCloudAPI from '../src/index.js';
|
3
|
+
|
4
|
+
test('discover.getHomeContent should fetch home content', async () => {
|
5
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
6
|
+
|
7
|
+
const result = await api.discover.getHomeContent();
|
8
|
+
console.log("Home Content Result:", JSON.stringify(result, null, 2).substring(0, 500) + "...");
|
9
|
+
|
10
|
+
expect(result).toBeDefined();
|
11
|
+
expect(result.collection).toBeDefined();
|
12
|
+
expect(Array.isArray(result.collection)).toBe(true);
|
13
|
+
});
|
14
|
+
|
15
|
+
test('discover.getRecentTracks should fetch recent tracks', async () => {
|
16
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
17
|
+
|
18
|
+
const result = await api.discover.getRecentTracks('electronic');
|
19
|
+
console.log("Recent Tracks Result:", JSON.stringify(result, null, 2).substring(0, 500) + "...");
|
20
|
+
|
21
|
+
expect(result).toBeDefined();
|
22
|
+
expect(result.collection).toBeDefined();
|
23
|
+
expect(Array.isArray(result.collection)).toBe(true);
|
24
|
+
});
|
25
|
+
|
26
|
+
test('discover.getRecentTracksByCountry should fetch recent tracks by country', async () => {
|
27
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
28
|
+
|
29
|
+
const result = await api.discover.getRecentTracksByCountry();
|
30
|
+
console.log("Recent Tracks By Country Result:", JSON.stringify(result, null, 2).substring(0, 500) + "...");
|
31
|
+
|
32
|
+
expect(result).toBeDefined();
|
33
|
+
expect(result.collection).toBeDefined();
|
34
|
+
expect(Array.isArray(result.collection)).toBe(true);
|
35
|
+
});
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { test, expect } from "bun:test";
|
2
|
+
import SoundCloudAPI from '../src/index.js';
|
3
|
+
|
4
|
+
test('find popular user and track IDs', async () => {
|
5
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
6
|
+
|
7
|
+
// Tìm user phổ biến
|
8
|
+
const userResult = await api.search.users('skrillex', { limit: 1 });
|
9
|
+
console.log("Found User:", JSON.stringify(userResult.collection[0], null, 2));
|
10
|
+
|
11
|
+
// Tìm track phổ biến
|
12
|
+
const trackResult = await api.search.tracks('skrillex bangarang', { limit: 1 });
|
13
|
+
console.log("Found Track:", JSON.stringify(trackResult.collection[0], null, 2));
|
14
|
+
});
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import { test, expect } from "bun:test";
|
2
|
+
import SoundCloudAPI from '../src/index.js';
|
3
|
+
|
4
|
+
// Sử dụng ID thật từ kết quả tìm kiếm
|
5
|
+
const TEST_TRACK_ID = 45719017; // Skrillex - Bangarang feat Sirah
|
6
|
+
|
7
|
+
test('media.getPlaybackUrl should fetch playback URL', async () => {
|
8
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
9
|
+
|
10
|
+
const result = await api.media.getMediaUrl(TEST_TRACK_ID, 'hls');
|
11
|
+
console.log("Playback URL Result:", result);
|
12
|
+
|
13
|
+
expect(result).toBeDefined();
|
14
|
+
expect(typeof result).toBe('string');
|
15
|
+
expect(result.startsWith('https://')).toBe(true);
|
16
|
+
});
|
17
|
+
|
18
|
+
test('media.getDownloadUrl should fetch download URL', async () => {
|
19
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
20
|
+
|
21
|
+
const result = await api.media.getMediaUrl(TEST_TRACK_ID, 'progressive');
|
22
|
+
console.log("Download URL Result:", result);
|
23
|
+
|
24
|
+
expect(result).toBeDefined();
|
25
|
+
expect(typeof result).toBe('string');
|
26
|
+
expect(result.startsWith('https://')).toBe(true);
|
27
|
+
});
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { test, expect } from "bun:test";
|
2
|
+
import SoundCloudAPI from '../src/index.js';
|
3
|
+
|
4
|
+
// Cần tìm một playlist ID thật
|
5
|
+
// Sử dụng kết quả từ search để tìm một playlist
|
6
|
+
test('find a playlist ID for testing', async () => {
|
7
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
8
|
+
|
9
|
+
const result = await api.search.playlists('skrillex', { limit: 1 });
|
10
|
+
console.log("Found Playlist:", JSON.stringify(result.collection[0], null, 2));
|
11
|
+
|
12
|
+
// Lưu playlist ID cho các test tiếp theo
|
13
|
+
const playlistId = result.collection[0].id;
|
14
|
+
|
15
|
+
expect(playlistId).toBeDefined();
|
16
|
+
expect(typeof playlistId).toBe('number');
|
17
|
+
|
18
|
+
return playlistId;
|
19
|
+
});
|
20
|
+
|
21
|
+
test('playlists.getPlaylist should fetch playlist information', async () => {
|
22
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
23
|
+
|
24
|
+
// Tìm một playlist ID
|
25
|
+
const searchResult = await api.search.playlists('skrillex', { limit: 1 });
|
26
|
+
const playlistId = searchResult.collection[0].id;
|
27
|
+
|
28
|
+
const result = await api.playlists.getPlaylist(playlistId);
|
29
|
+
console.log("Playlist Info Result:", JSON.stringify(result, null, 2).substring(0, 500) + "...");
|
30
|
+
|
31
|
+
expect(result).toBeDefined();
|
32
|
+
expect(result.id).toBeDefined();
|
33
|
+
expect(result.kind).toBe('playlist');
|
34
|
+
});
|
35
|
+
|
36
|
+
test('playlists.getByGenre should fetch playlists by genre', async () => {
|
37
|
+
const api = new SoundCloudAPI({ autoFetchClientId: true });
|
38
|
+
|
39
|
+
const result = await api.playlists.getByGenre('electronic', { limit: 5 });
|
40
|
+
console.log("Playlists By Genre Result:", JSON.stringify(result, null, 2).substring(0, 500) + "...");
|
41
|
+
|
42
|
+
expect(result).toBeDefined();
|
43
|
+
expect(result.collection).toBeDefined();
|
44
|
+
expect(Array.isArray(result.collection)).toBe(true);
|
45
|
+
});
|