@ziplayer/plugin 0.1.33 → 0.1.40

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.
@@ -1,368 +1,368 @@
1
- import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
2
-
3
- const SoundCloud = require("@zibot/scdl");
4
- import { URL } from "url";
5
-
6
- const ALLOWED_SOUNDCLOUD_HOSTS = ["soundcloud.com", "www.soundcloud.com", "m.soundcloud.com"];
7
-
8
- function isValidSoundCloudHost(maybeUrl: string): boolean {
9
- try {
10
- const parsed = new URL(maybeUrl);
11
- return ALLOWED_SOUNDCLOUD_HOSTS.includes(parsed.hostname);
12
- } catch {
13
- // Not a valid URL, not handled as host-based
14
- return false;
15
- }
16
- }
17
- /**
18
- * A plugin for handling SoundCloud audio content including tracks, playlists, and search functionality.
19
- *
20
- * This plugin provides comprehensive support for:
21
- * - SoundCloud track URLs (soundcloud.com)
22
- * - SoundCloud playlist URLs
23
- * - SoundCloud search queries
24
- * - Audio stream extraction from SoundCloud tracks
25
- * - Related track recommendations
26
- *
27
- * @example
28
- *
29
- * const soundcloudPlugin = new SoundCloudPlugin();
30
- *
31
- * // Add to PlayerManager
32
- * const manager = new PlayerManager({
33
- * plugins: [soundcloudPlugin]
34
- * });
35
- *
36
- * // Search for tracks
37
- * const result = await soundcloudPlugin.search("chill music", "user123");
38
- *
39
- * // Get audio stream
40
- * const stream = await soundcloudPlugin.getStream(result.tracks[0]);
41
- *
42
- * @since 1.0.0
43
- */
44
- export class SoundCloudPlugin extends BasePlugin {
45
- name = "soundcloud";
46
- version = "1.0.0";
47
- private client: any;
48
- private ready: Promise<void>;
49
-
50
- /**
51
- * Creates a new SoundCloudPlugin instance.
52
- *
53
- * The plugin will automatically initialize the SoundCloud client for track
54
- * and playlist operations. Initialization is asynchronous and handled internally.
55
- *
56
- * @example
57
- * const plugin = new SoundCloudPlugin();
58
- * // Plugin is ready to use after initialization completes
59
- */
60
- constructor() {
61
- super();
62
- this.ready = this.init();
63
- }
64
-
65
- private async init(): Promise<void> {
66
- this.client = new SoundCloud({ init: false });
67
- await this.client.init();
68
- }
69
-
70
- /**
71
- * Determines if this plugin can handle the given query.
72
- *
73
- * @param query - The search query or URL to check
74
- * @returns `true` if the plugin can handle the query, `false` otherwise
75
- *
76
- * @example
77
- * plugin.canHandle("https://soundcloud.com/artist/track"); // true
78
- * plugin.canHandle("chill music"); // true
79
- * plugin.canHandle("spotify:track:123"); // false
80
- */
81
- canHandle(query: string): boolean {
82
- const q = (query || "").trim().toLowerCase();
83
- const isUrl = q.startsWith("http://") || q.startsWith("https://");
84
- if (isUrl) {
85
- return isValidSoundCloudHost(query);
86
- }
87
-
88
- // Avoid intercepting explicit patterns for other extractors
89
- if (q.startsWith("tts:") || q.startsWith("say ")) return false;
90
- if (q.startsWith("spotify:") || q.includes("open.spotify.com")) return false;
91
- if (q.includes("youtube")) return false;
92
-
93
- // Treat remaining non-URL free text as searchable
94
- return true;
95
- }
96
-
97
- /**
98
- * Validates if a URL is a valid SoundCloud URL.
99
- *
100
- * @param url - The URL to validate
101
- * @returns `true` if the URL is a valid SoundCloud URL, `false` otherwise
102
- *
103
- * @example
104
- * plugin.validate("https://soundcloud.com/artist/track"); // true
105
- * plugin.validate("https://www.soundcloud.com/artist/track"); // true
106
- * plugin.validate("https://youtube.com/watch?v=123"); // false
107
- */
108
- validate(url: string): boolean {
109
- return isValidSoundCloudHost(url);
110
- }
111
-
112
- /**
113
- * Searches for SoundCloud content based on the given query.
114
- *
115
- * This method handles both URL-based queries (direct track/playlist links) and
116
- * text-based search queries. For URLs, it will extract track or playlist information.
117
- * For text queries, it will perform a SoundCloud search and return up to 10 results.
118
- *
119
- * @param query - The search query (URL or text)
120
- * @param requestedBy - The user ID who requested the search
121
- * @returns A SearchResult containing tracks and optional playlist information
122
- *
123
- * @example
124
- * // Search by URL
125
- * const result = await plugin.search("https://soundcloud.com/artist/track", "user123");
126
- *
127
- * // Search by text
128
- * const searchResult = await plugin.search("chill music", "user123");
129
- * console.log(searchResult.tracks); // Array of Track objects
130
- */
131
- async search(query: string, requestedBy: string): Promise<SearchResult> {
132
- await this.ready;
133
-
134
- // If the query is a URL but not a SoundCloud URL, do not handle it here
135
- // This prevents hijacking e.g. YouTube/Spotify links as free-text searches.
136
- try {
137
- const q = (query || "").trim().toLowerCase();
138
- const isUrl = q.startsWith("http://") || q.startsWith("https://");
139
- if (isUrl && !this.validate(query)) {
140
- return { tracks: [] };
141
- }
142
- } catch {}
143
-
144
- try {
145
- if (isValidSoundCloudHost(query)) {
146
- try {
147
- const info = await this.client.getTrackDetails(query);
148
- const track: Track = {
149
- id: info.id.toString(),
150
- title: info.title,
151
- url: info.permalink_url || query,
152
- duration: info.duration,
153
- thumbnail: info.artwork_url,
154
- requestedBy,
155
- source: this.name,
156
- metadata: {
157
- author: info.user?.username,
158
- plays: info.playback_count,
159
- },
160
- };
161
- return { tracks: [track] };
162
- } catch {
163
- const playlist = await this.client.getPlaylistDetails(query);
164
- const tracks: Track[] = playlist.tracks.map((t: any) => ({
165
- id: t.id.toString(),
166
- title: t.title,
167
- url: t.permalink_url,
168
- duration: t.duration,
169
- thumbnail: t.artwork_url || playlist.artwork_url,
170
- requestedBy,
171
- source: this.name,
172
- metadata: {
173
- author: t.user?.username,
174
- plays: t.playback_count,
175
- playlist: playlist.id?.toString(),
176
- },
177
- }));
178
-
179
- return {
180
- tracks,
181
- playlist: {
182
- name: playlist.title,
183
- url: playlist.permalink_url || query,
184
- thumbnail: playlist.artwork_url,
185
- },
186
- };
187
- }
188
- }
189
-
190
- const results = await this.client.searchTracks({ query, limit: 15 });
191
- const tracks: Track[] = results.slice(0, 10).map((track: any) => ({
192
- id: track.id.toString(),
193
- title: track.title,
194
- url: track.permalink_url,
195
- duration: track.duration,
196
- thumbnail: track.artwork_url,
197
- requestedBy,
198
- source: this.name,
199
- metadata: {
200
- author: track.user?.username,
201
- plays: track.playback_count,
202
- },
203
- }));
204
-
205
- return { tracks };
206
- } catch (error: any) {
207
- throw new Error(`SoundCloud search failed: ${error?.message}`);
208
- }
209
- }
210
-
211
- /**
212
- * Retrieves the audio stream for a SoundCloud track.
213
- *
214
- * This method downloads the audio stream from SoundCloud using the track's URL.
215
- * It handles the SoundCloud-specific download process and returns the stream
216
- * in a format compatible with the player.
217
- *
218
- * @param track - The Track object to get the stream for
219
- * @returns A StreamInfo object containing the audio stream and metadata
220
- * @throws {Error} If the track URL is invalid or stream download fails
221
- *
222
- * @example
223
- * const track = { id: "123", title: "Track Title", url: "https://soundcloud.com/artist/track", ... };
224
- * const streamInfo = await plugin.getStream(track);
225
- * console.log(streamInfo.type); // "arbitrary"
226
- * console.log(streamInfo.stream); // Readable stream
227
- */
228
- async getStream(track: Track): Promise<StreamInfo> {
229
- await this.ready;
230
-
231
- try {
232
- const stream = await this.client.downloadTrack(track.url);
233
- if (!stream) {
234
- throw new Error("SoundCloud download returned null");
235
- }
236
-
237
- return {
238
- stream,
239
- type: "arbitrary",
240
- metadata: track.metadata,
241
- };
242
- } catch (error: any) {
243
- throw new Error(`Failed to get SoundCloud stream: ${error.message}`);
244
- }
245
- }
246
-
247
- /**
248
- * Gets related tracks for a given SoundCloud track.
249
- *
250
- * This method fetches related tracks from SoundCloud's recommendation system
251
- * based on the provided track URL or ID. It can filter out tracks that are
252
- * already in the history to avoid duplicates.
253
- *
254
- * @param trackURL - The SoundCloud track URL or ID to get related tracks for
255
- * @param opts - Options for filtering and limiting results
256
- * @param opts.limit - Maximum number of related tracks to return (default: 1)
257
- * @param opts.offset - Number of tracks to skip from the beginning (default: 0)
258
- * @param opts.history - Array of tracks to exclude from results
259
- * @returns An array of related Track objects
260
- *
261
- * @example
262
- * const related = await plugin.getRelatedTracks(
263
- * "https://soundcloud.com/artist/track",
264
- * { limit: 3, history: [currentTrack] }
265
- * );
266
- * console.log(`Found ${related.length} related tracks`);
267
- */
268
- async getRelatedTracks(
269
- trackURL: string | number,
270
- opts: { limit?: number; offset?: number; history?: Track[] } = {},
271
- ): Promise<Track[]> {
272
- await this.ready;
273
- try {
274
- const tracks = await this.client.getRelatedTracks(trackURL, {
275
- limit: 30,
276
- filter: "tracks",
277
- });
278
-
279
- if (!tracks || !tracks?.length) {
280
- return [];
281
- }
282
- const relatedfilter = tracks.filter((tr: any) => !(opts?.history ?? []).some((t) => t.url === tr.permalink_url));
283
-
284
- const related = relatedfilter.slice(0, opts.limit || 1);
285
-
286
- return related.map((t: any) => ({
287
- id: t.id.toString(),
288
- title: t.title,
289
- url: t.permalink_url,
290
- duration: t.duration,
291
- thumbnail: t.artwork_url,
292
- requestedBy: "auto",
293
- source: this.name,
294
- metadata: {
295
- author: t.user?.username,
296
- plays: t.playback_count,
297
- },
298
- }));
299
- } catch {
300
- return [];
301
- }
302
- }
303
-
304
- /**
305
- * Provides a fallback stream by searching for the track title.
306
- *
307
- * This method is used when the primary stream extraction fails. It performs
308
- * a search using the track's title and attempts to get a stream from the
309
- * first search result.
310
- *
311
- * @param track - The Track object to get a fallback stream for
312
- * @returns A StreamInfo object containing the fallback audio stream
313
- * @throws {Error} If no fallback track is found or stream extraction fails
314
- *
315
- * @example
316
- * try {
317
- * const stream = await plugin.getStream(track);
318
- * } catch (error) {
319
- * // Try fallback
320
- * const fallbackStream = await plugin.getFallback(track);
321
- * }
322
- */
323
- async getFallback(track: Track): Promise<StreamInfo> {
324
- const trackfall = await this.search(track.title, track.requestedBy);
325
- const fallbackTrack = trackfall.tracks?.[0];
326
- if (!fallbackTrack) {
327
- throw new Error(`No fallback track found for ${track.title}`);
328
- }
329
- return await this.getStream(fallbackTrack);
330
- }
331
-
332
- /**
333
- * Extracts tracks from a SoundCloud playlist URL.
334
- *
335
- * @param url - The SoundCloud playlist URL
336
- * @param requestedBy - The user ID who requested the extraction
337
- * @returns An array of Track objects from the playlist
338
- *
339
- * @example
340
- * const tracks = await plugin.extractPlaylist(
341
- * "https://soundcloud.com/artist/sets/playlist-name",
342
- * "user123"
343
- * );
344
- * console.log(`Found ${tracks.length} tracks in playlist`);
345
- */
346
- async extractPlaylist(url: string, requestedBy: string): Promise<Track[]> {
347
- await this.ready;
348
- try {
349
- const playlist = await this.client.getPlaylistDetails(url);
350
- return playlist.tracks.map((t: any) => ({
351
- id: t.id.toString(),
352
- title: t.title,
353
- url: t.permalink_url,
354
- duration: t.duration,
355
- thumbnail: t.artwork_url || playlist.artwork_url,
356
- requestedBy,
357
- source: this.name,
358
- metadata: {
359
- author: t.user?.username,
360
- plays: t.playback_count,
361
- playlist: playlist.id?.toString(),
362
- },
363
- }));
364
- } catch {
365
- return [];
366
- }
367
- }
368
- }
1
+ import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
2
+
3
+ const SoundCloud = require("@zibot/scdl");
4
+ import { URL } from "url";
5
+
6
+ const ALLOWED_SOUNDCLOUD_HOSTS = ["soundcloud.com", "www.soundcloud.com", "m.soundcloud.com"];
7
+
8
+ function isValidSoundCloudHost(maybeUrl: string): boolean {
9
+ try {
10
+ const parsed = new URL(maybeUrl);
11
+ return ALLOWED_SOUNDCLOUD_HOSTS.includes(parsed.hostname);
12
+ } catch {
13
+ // Not a valid URL, not handled as host-based
14
+ return false;
15
+ }
16
+ }
17
+ /**
18
+ * A plugin for handling SoundCloud audio content including tracks, playlists, and search functionality.
19
+ *
20
+ * This plugin provides comprehensive support for:
21
+ * - SoundCloud track URLs (soundcloud.com)
22
+ * - SoundCloud playlist URLs
23
+ * - SoundCloud search queries
24
+ * - Audio stream extraction from SoundCloud tracks
25
+ * - Related track recommendations
26
+ *
27
+ * @example
28
+ *
29
+ * const soundcloudPlugin = new SoundCloudPlugin();
30
+ *
31
+ * // Add to PlayerManager
32
+ * const manager = new PlayerManager({
33
+ * plugins: [soundcloudPlugin]
34
+ * });
35
+ *
36
+ * // Search for tracks
37
+ * const result = await soundcloudPlugin.search("chill music", "user123");
38
+ *
39
+ * // Get audio stream
40
+ * const stream = await soundcloudPlugin.getStream(result.tracks[0]);
41
+ *
42
+ * @since 1.0.0
43
+ */
44
+ export class SoundCloudPlugin extends BasePlugin {
45
+ name = "soundcloud";
46
+ version = "1.0.0";
47
+ private client: any;
48
+ private ready: Promise<void>;
49
+
50
+ /**
51
+ * Creates a new SoundCloudPlugin instance.
52
+ *
53
+ * The plugin will automatically initialize the SoundCloud client for track
54
+ * and playlist operations. Initialization is asynchronous and handled internally.
55
+ *
56
+ * @example
57
+ * const plugin = new SoundCloudPlugin();
58
+ * // Plugin is ready to use after initialization completes
59
+ */
60
+ constructor() {
61
+ super();
62
+ this.ready = this.init();
63
+ }
64
+
65
+ private async init(): Promise<void> {
66
+ this.client = new SoundCloud({ init: false });
67
+ await this.client.init();
68
+ }
69
+
70
+ /**
71
+ * Determines if this plugin can handle the given query.
72
+ *
73
+ * @param query - The search query or URL to check
74
+ * @returns `true` if the plugin can handle the query, `false` otherwise
75
+ *
76
+ * @example
77
+ * plugin.canHandle("https://soundcloud.com/artist/track"); // true
78
+ * plugin.canHandle("chill music"); // true
79
+ * plugin.canHandle("spotify:track:123"); // false
80
+ */
81
+ canHandle(query: string): boolean {
82
+ const q = (query || "").trim().toLowerCase();
83
+ const isUrl = q.startsWith("http://") || q.startsWith("https://");
84
+ if (isUrl) {
85
+ return isValidSoundCloudHost(query);
86
+ }
87
+
88
+ // Avoid intercepting explicit patterns for other extractors
89
+ if (q.startsWith("tts:") || q.startsWith("say ")) return false;
90
+ if (q.startsWith("spotify:") || q.includes("open.spotify.com")) return false;
91
+ if (q.includes("youtube")) return false;
92
+
93
+ // Treat remaining non-URL free text as searchable
94
+ return true;
95
+ }
96
+
97
+ /**
98
+ * Validates if a URL is a valid SoundCloud URL.
99
+ *
100
+ * @param url - The URL to validate
101
+ * @returns `true` if the URL is a valid SoundCloud URL, `false` otherwise
102
+ *
103
+ * @example
104
+ * plugin.validate("https://soundcloud.com/artist/track"); // true
105
+ * plugin.validate("https://www.soundcloud.com/artist/track"); // true
106
+ * plugin.validate("https://youtube.com/watch?v=123"); // false
107
+ */
108
+ validate(url: string): boolean {
109
+ return isValidSoundCloudHost(url);
110
+ }
111
+
112
+ /**
113
+ * Searches for SoundCloud content based on the given query.
114
+ *
115
+ * This method handles both URL-based queries (direct track/playlist links) and
116
+ * text-based search queries. For URLs, it will extract track or playlist information.
117
+ * For text queries, it will perform a SoundCloud search and return up to 10 results.
118
+ *
119
+ * @param query - The search query (URL or text)
120
+ * @param requestedBy - The user ID who requested the search
121
+ * @returns A SearchResult containing tracks and optional playlist information
122
+ *
123
+ * @example
124
+ * // Search by URL
125
+ * const result = await plugin.search("https://soundcloud.com/artist/track", "user123");
126
+ *
127
+ * // Search by text
128
+ * const searchResult = await plugin.search("chill music", "user123");
129
+ * console.log(searchResult.tracks); // Array of Track objects
130
+ */
131
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
132
+ await this.ready;
133
+
134
+ // If the query is a URL but not a SoundCloud URL, do not handle it here
135
+ // This prevents hijacking e.g. YouTube/Spotify links as free-text searches.
136
+ try {
137
+ const q = (query || "").trim().toLowerCase();
138
+ const isUrl = q.startsWith("http://") || q.startsWith("https://");
139
+ if (isUrl && !this.validate(query)) {
140
+ return { tracks: [] };
141
+ }
142
+ } catch {}
143
+
144
+ try {
145
+ if (isValidSoundCloudHost(query)) {
146
+ try {
147
+ const info = await this.client.getTrackDetails(query);
148
+ const track: Track = {
149
+ id: info.id.toString(),
150
+ title: info.title,
151
+ url: info.permalink_url || query,
152
+ duration: info.duration,
153
+ thumbnail: info.artwork_url,
154
+ requestedBy,
155
+ source: this.name,
156
+ metadata: {
157
+ author: info.user?.username,
158
+ plays: info.playback_count,
159
+ },
160
+ };
161
+ return { tracks: [track] };
162
+ } catch {
163
+ const playlist = await this.client.getPlaylistDetails(query);
164
+ const tracks: Track[] = playlist.tracks.map((t: any) => ({
165
+ id: t.id.toString(),
166
+ title: t.title,
167
+ url: t.permalink_url,
168
+ duration: t.duration,
169
+ thumbnail: t.artwork_url || playlist.artwork_url,
170
+ requestedBy,
171
+ source: this.name,
172
+ metadata: {
173
+ author: t.user?.username,
174
+ plays: t.playback_count,
175
+ playlist: playlist.id?.toString(),
176
+ },
177
+ }));
178
+
179
+ return {
180
+ tracks,
181
+ playlist: {
182
+ name: playlist.title,
183
+ url: playlist.permalink_url || query,
184
+ thumbnail: playlist.artwork_url,
185
+ },
186
+ };
187
+ }
188
+ }
189
+
190
+ const results = await this.client.searchTracks({ query, limit: 15 });
191
+ const tracks: Track[] = results.slice(0, 10).map((track: any) => ({
192
+ id: track.id.toString(),
193
+ title: track.title,
194
+ url: track.permalink_url,
195
+ duration: track.duration,
196
+ thumbnail: track.artwork_url,
197
+ requestedBy,
198
+ source: this.name,
199
+ metadata: {
200
+ author: track.user?.username,
201
+ plays: track.playback_count,
202
+ },
203
+ }));
204
+
205
+ return { tracks };
206
+ } catch (error: any) {
207
+ throw new Error(`SoundCloud search failed: ${error?.message}`);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Retrieves the audio stream for a SoundCloud track.
213
+ *
214
+ * This method downloads the audio stream from SoundCloud using the track's URL.
215
+ * It handles the SoundCloud-specific download process and returns the stream
216
+ * in a format compatible with the player.
217
+ *
218
+ * @param track - The Track object to get the stream for
219
+ * @returns A StreamInfo object containing the audio stream and metadata
220
+ * @throws {Error} If the track URL is invalid or stream download fails
221
+ *
222
+ * @example
223
+ * const track = { id: "123", title: "Track Title", url: "https://soundcloud.com/artist/track", ... };
224
+ * const streamInfo = await plugin.getStream(track);
225
+ * console.log(streamInfo.type); // "arbitrary"
226
+ * console.log(streamInfo.stream); // Readable stream
227
+ */
228
+ async getStream(track: Track): Promise<StreamInfo> {
229
+ await this.ready;
230
+
231
+ try {
232
+ const stream = await this.client.downloadTrack(track.url);
233
+ if (!stream) {
234
+ throw new Error("SoundCloud download returned null");
235
+ }
236
+
237
+ return {
238
+ stream,
239
+ type: "arbitrary",
240
+ metadata: track.metadata,
241
+ };
242
+ } catch (error: any) {
243
+ throw new Error(`Failed to get SoundCloud stream: ${error.message}`);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Gets related tracks for a given SoundCloud track.
249
+ *
250
+ * This method fetches related tracks from SoundCloud's recommendation system
251
+ * based on the provided track URL or ID. It can filter out tracks that are
252
+ * already in the history to avoid duplicates.
253
+ *
254
+ * @param trackURL - The SoundCloud track URL or ID to get related tracks for
255
+ * @param opts - Options for filtering and limiting results
256
+ * @param opts.limit - Maximum number of related tracks to return (default: 1)
257
+ * @param opts.offset - Number of tracks to skip from the beginning (default: 0)
258
+ * @param opts.history - Array of tracks to exclude from results
259
+ * @returns An array of related Track objects
260
+ *
261
+ * @example
262
+ * const related = await plugin.getRelatedTracks(
263
+ * "https://soundcloud.com/artist/track",
264
+ * { limit: 3, history: [currentTrack] }
265
+ * );
266
+ * console.log(`Found ${related.length} related tracks`);
267
+ */
268
+ async getRelatedTracks(
269
+ trackURL: string | number,
270
+ opts: { limit?: number; offset?: number; history?: Track[] } = {},
271
+ ): Promise<Track[]> {
272
+ await this.ready;
273
+ try {
274
+ const tracks = await this.client.getRelatedTracks(trackURL, {
275
+ limit: 30,
276
+ filter: "tracks",
277
+ });
278
+
279
+ if (!tracks || !tracks?.length) {
280
+ return [];
281
+ }
282
+ const relatedfilter = tracks.filter((tr: any) => !(opts?.history ?? []).some((t) => t.url === tr.permalink_url));
283
+
284
+ const related = relatedfilter.slice(0, opts.limit || 1);
285
+
286
+ return related.map((t: any) => ({
287
+ id: t.id.toString(),
288
+ title: t.title,
289
+ url: t.permalink_url,
290
+ duration: t.duration,
291
+ thumbnail: t.artwork_url,
292
+ requestedBy: "auto",
293
+ source: this.name,
294
+ metadata: {
295
+ author: t.user?.username,
296
+ plays: t.playback_count,
297
+ },
298
+ }));
299
+ } catch {
300
+ return [];
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Provides a fallback stream by searching for the track title.
306
+ *
307
+ * This method is used when the primary stream extraction fails. It performs
308
+ * a search using the track's title and attempts to get a stream from the
309
+ * first search result.
310
+ *
311
+ * @param track - The Track object to get a fallback stream for
312
+ * @returns A StreamInfo object containing the fallback audio stream
313
+ * @throws {Error} If no fallback track is found or stream extraction fails
314
+ *
315
+ * @example
316
+ * try {
317
+ * const stream = await plugin.getStream(track);
318
+ * } catch (error) {
319
+ * // Try fallback
320
+ * const fallbackStream = await plugin.getFallback(track);
321
+ * }
322
+ */
323
+ async getFallback(track: Track): Promise<StreamInfo> {
324
+ const trackfall = await this.search(track.title, track.requestedBy);
325
+ const fallbackTrack = trackfall.tracks?.[0];
326
+ if (!fallbackTrack) {
327
+ throw new Error(`No fallback track found for ${track.title}`);
328
+ }
329
+ return await this.getStream(fallbackTrack);
330
+ }
331
+
332
+ /**
333
+ * Extracts tracks from a SoundCloud playlist URL.
334
+ *
335
+ * @param url - The SoundCloud playlist URL
336
+ * @param requestedBy - The user ID who requested the extraction
337
+ * @returns An array of Track objects from the playlist
338
+ *
339
+ * @example
340
+ * const tracks = await plugin.extractPlaylist(
341
+ * "https://soundcloud.com/artist/sets/playlist-name",
342
+ * "user123"
343
+ * );
344
+ * console.log(`Found ${tracks.length} tracks in playlist`);
345
+ */
346
+ async extractPlaylist(url: string, requestedBy: string): Promise<Track[]> {
347
+ await this.ready;
348
+ try {
349
+ const playlist = await this.client.getPlaylistDetails(url);
350
+ return playlist.tracks.map((t: any) => ({
351
+ id: t.id.toString(),
352
+ title: t.title,
353
+ url: t.permalink_url,
354
+ duration: t.duration,
355
+ thumbnail: t.artwork_url || playlist.artwork_url,
356
+ requestedBy,
357
+ source: this.name,
358
+ metadata: {
359
+ author: t.user?.username,
360
+ plays: t.playback_count,
361
+ playlist: playlist.id?.toString(),
362
+ },
363
+ }));
364
+ } catch {
365
+ return [];
366
+ }
367
+ }
368
+ }