@ziplayer/plugin 0.1.2 → 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.
- package/README.md +180 -180
- package/YTSR_README.md +310 -310
- package/dist/TTSPlugin.js +2 -0
- package/dist/TTSPlugin.js.map +1 -1
- package/dist/YTSRPlugin.d.ts.map +1 -1
- package/dist/YTSRPlugin.js +17 -1
- package/dist/YTSRPlugin.js.map +1 -1
- package/dist/YouTubePlugin.d.ts +15 -6
- package/dist/YouTubePlugin.d.ts.map +1 -1
- package/dist/YouTubePlugin.js +122 -47
- package/dist/YouTubePlugin.js.map +1 -1
- package/dist/utils/progress-bar.d.ts +8 -0
- package/dist/utils/progress-bar.d.ts.map +1 -0
- package/dist/utils/progress-bar.js +24 -0
- package/dist/utils/progress-bar.js.map +1 -0
- package/dist/utils/sabr-stream-factory.d.ts +25 -0
- package/dist/utils/sabr-stream-factory.d.ts.map +1 -0
- package/dist/utils/sabr-stream-factory.js +83 -0
- package/dist/utils/sabr-stream-factory.js.map +1 -0
- package/dist/utils/stream-converter.d.ts +10 -0
- package/dist/utils/stream-converter.d.ts.map +1 -0
- package/dist/utils/stream-converter.js +78 -0
- package/dist/utils/stream-converter.js.map +1 -0
- package/package.json +5 -3
- package/src/SpotifyPlugin.ts +312 -312
- package/src/TTSPlugin.ts +361 -361
- package/src/YTSRPlugin.ts +596 -583
- package/src/YouTubePlugin.ts +620 -528
- package/src/types/googlevideo.d.ts +45 -0
- package/src/utils/sabr-stream-factory.ts +96 -0
- package/src/utils/stream-converter.ts +87 -0
- package/tsconfig.json +1 -1
package/src/SpotifyPlugin.ts
CHANGED
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A minimal Spotify plugin for metadata extraction and display purposes.
|
|
5
|
-
*
|
|
6
|
-
* This plugin provides support for:
|
|
7
|
-
* - Spotify track URLs/URIs (spotify:track:...)
|
|
8
|
-
* - Spotify playlist URLs/URIs (spotify:playlist:...)
|
|
9
|
-
* - Spotify album URLs/URIs (spotify:album:...)
|
|
10
|
-
* - Metadata extraction using Spotify's public oEmbed endpoint
|
|
11
|
-
*
|
|
12
|
-
* **Important Notes:**
|
|
13
|
-
* - This plugin does NOT provide audio streams (player is expected to redirect/fallback upstream)
|
|
14
|
-
* - This plugin does NOT expand playlists/albums (no SDK; oEmbed doesn't enumerate items)
|
|
15
|
-
* - This plugin only provides display metadata for Spotify content
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
*
|
|
19
|
-
* const spotifyPlugin = new SpotifyPlugin();
|
|
20
|
-
*
|
|
21
|
-
* // Add to PlayerManager
|
|
22
|
-
* const manager = new PlayerManager({
|
|
23
|
-
* plugins: [spotifyPlugin]
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* // Get metadata for a Spotify track
|
|
27
|
-
* const result = await spotifyPlugin.search("spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "user123");
|
|
28
|
-
* console.log(result.tracks[0].metadata); // Contains Spotify metadata
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @since 1.1.0
|
|
32
|
-
*/
|
|
33
|
-
export class SpotifyPlugin extends BasePlugin {
|
|
34
|
-
name = "spotify";
|
|
35
|
-
version = "1.1.0";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Determines if this plugin can handle the given query.
|
|
39
|
-
*
|
|
40
|
-
* @param query - The search query or URL to check
|
|
41
|
-
* @returns `true` if the query is a Spotify URL/URI, `false` otherwise
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
*
|
|
45
|
-
* plugin.canHandle("spotify:track:4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
46
|
-
* plugin.canHandle("https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
47
|
-
* plugin.canHandle("youtube.com/watch?v=123"); // false
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
canHandle(query: string): boolean {
|
|
51
|
-
const q = query.toLowerCase().trim();
|
|
52
|
-
if (q.startsWith("spotify:")) return true;
|
|
53
|
-
try {
|
|
54
|
-
const u = new URL(q);
|
|
55
|
-
return u.hostname === "open.spotify.com";
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validates if a URL/URI is a valid Spotify URL/URI.
|
|
63
|
-
*
|
|
64
|
-
* @param url - The URL/URI to validate
|
|
65
|
-
* @returns `true` if the URL/URI is a valid Spotify URL/URI, `false` otherwise
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
*
|
|
69
|
-
* plugin.validate("spotify:track:4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
70
|
-
* plugin.validate("https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
71
|
-
* plugin.validate("https://youtube.com/watch?v=123"); // false
|
|
72
|
-
*
|
|
73
|
-
*/
|
|
74
|
-
validate(url: string): boolean {
|
|
75
|
-
if (url.startsWith("spotify:")) return true;
|
|
76
|
-
try {
|
|
77
|
-
const u = new URL(url);
|
|
78
|
-
return u.hostname === "open.spotify.com";
|
|
79
|
-
} catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Extracts metadata from Spotify URLs/URIs using the oEmbed API.
|
|
86
|
-
*
|
|
87
|
-
* This method handles Spotify track, playlist, and album URLs/URIs by fetching
|
|
88
|
-
* display metadata from Spotify's public oEmbed endpoint. It does not provide
|
|
89
|
-
* audio streams or expand playlists/albums.
|
|
90
|
-
*
|
|
91
|
-
* @param query - The Spotify URL/URI to extract metadata from
|
|
92
|
-
* @param requestedBy - The user ID who requested the extraction
|
|
93
|
-
* @returns A SearchResult containing a single track with metadata (no audio stream)
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
*
|
|
97
|
-
* // Extract track metadata
|
|
98
|
-
* const result = await plugin.search("spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "user123");
|
|
99
|
-
* console.log(result.tracks[0].metadata); // Contains Spotify metadata
|
|
100
|
-
*
|
|
101
|
-
* // Extract playlist metadata
|
|
102
|
-
* const playlistResult = await plugin.search("https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M", "user123");
|
|
103
|
-
* console.log(playlistResult.tracks[0].metadata.kind); // "playlist"
|
|
104
|
-
*
|
|
105
|
-
*/
|
|
106
|
-
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
107
|
-
if (!this.validate(query)) {
|
|
108
|
-
return { tracks: [] };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const kind = this.identifyKind(query);
|
|
112
|
-
|
|
113
|
-
if (kind === "track") {
|
|
114
|
-
const t = await this.buildTrackFromUrlOrUri(query, requestedBy);
|
|
115
|
-
return { tracks: t ? [t] : [] };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (kind === "playlist") {
|
|
119
|
-
const t = await this.buildHeaderItem(query, requestedBy, "playlist");
|
|
120
|
-
return { tracks: t ? [t] : [] };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (kind === "album") {
|
|
124
|
-
const t = await this.buildHeaderItem(query, requestedBy, "album");
|
|
125
|
-
return { tracks: t ? [t] : [] };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return { tracks: [] };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Extracts tracks from a Spotify playlist URL.
|
|
133
|
-
*
|
|
134
|
-
* **Note:** This method is not implemented as this plugin does not support
|
|
135
|
-
* playlist expansion. It always returns an empty array.
|
|
136
|
-
*
|
|
137
|
-
* @param _input - The Spotify playlist URL (unused)
|
|
138
|
-
* @param _requestedBy - The user ID who requested the extraction (unused)
|
|
139
|
-
* @returns An empty array (playlist expansion not supported)
|
|
140
|
-
*
|
|
141
|
-
* @example
|
|
142
|
-
*
|
|
143
|
-
* const tracks = await plugin.extractPlaylist("spotify:playlist:123", "user123");
|
|
144
|
-
* console.log(tracks); // [] - empty array
|
|
145
|
-
*
|
|
146
|
-
*/
|
|
147
|
-
async extractPlaylist(_input: string, _requestedBy: string): Promise<Track[]> {
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Extracts tracks from a Spotify album URL.
|
|
153
|
-
*
|
|
154
|
-
* **Note:** This method is not implemented as this plugin does not support
|
|
155
|
-
* album expansion. It always returns an empty array.
|
|
156
|
-
*
|
|
157
|
-
* @param _input - The Spotify album URL (unused)
|
|
158
|
-
* @param _requestedBy - The user ID who requested the extraction (unused)
|
|
159
|
-
* @returns An empty array (album expansion not supported)
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
*
|
|
163
|
-
* const tracks = await plugin.extractAlbum("spotify:album:123", "user123");
|
|
164
|
-
* console.log(tracks); // [] - empty array
|
|
165
|
-
*
|
|
166
|
-
*/
|
|
167
|
-
async extractAlbum(_input: string, _requestedBy: string): Promise<Track[]> {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Attempts to get an audio stream for a Spotify track.
|
|
173
|
-
*
|
|
174
|
-
* **Note:** This method always throws an error as this plugin does not support
|
|
175
|
-
* audio streaming. The player is expected to redirect to other plugins or
|
|
176
|
-
* use fallback mechanisms for actual audio playback.
|
|
177
|
-
*
|
|
178
|
-
* @param _track - The Track object (unused)
|
|
179
|
-
* @throws {Error} Always throws "Spotify streaming is not supported by this plugin"
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
*
|
|
183
|
-
* try {
|
|
184
|
-
* const stream = await plugin.getStream(track);
|
|
185
|
-
* } catch (error) {
|
|
186
|
-
* console.log(error.message); // "Spotify streaming is not supported by this plugin"
|
|
187
|
-
* }
|
|
188
|
-
*
|
|
189
|
-
*/
|
|
190
|
-
async getStream(_track: Track): Promise<StreamInfo> {
|
|
191
|
-
throw new Error("Spotify streaming is not supported by this plugin");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private identifyKind(input: string): "track" | "playlist" | "album" | "unknown" {
|
|
195
|
-
if (input.startsWith("spotify:")) {
|
|
196
|
-
if (input.includes(":track:")) return "track";
|
|
197
|
-
if (input.includes(":playlist:")) return "playlist";
|
|
198
|
-
if (input.includes(":album:")) return "album";
|
|
199
|
-
return "unknown";
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
const u = new URL(input);
|
|
203
|
-
const parts = u.pathname.split("/").filter(Boolean);
|
|
204
|
-
const kind = parts[0];
|
|
205
|
-
if (kind === "track") return "track";
|
|
206
|
-
if (kind === "playlist") return "playlist";
|
|
207
|
-
if (kind === "album") return "album";
|
|
208
|
-
return "unknown";
|
|
209
|
-
} catch {
|
|
210
|
-
return "unknown";
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private extractId(input: string): string | null {
|
|
215
|
-
if (!input) return null;
|
|
216
|
-
if (input.startsWith("spotify:")) {
|
|
217
|
-
const parts = input.split(":");
|
|
218
|
-
return parts[2] || null;
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const u = new URL(input);
|
|
222
|
-
const parts = u.pathname.split("/").filter(Boolean);
|
|
223
|
-
return parts[1] || null; // /track/<id>
|
|
224
|
-
} catch {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private async buildTrackFromUrlOrUri(input: string, requestedBy: string): Promise<Track | null> {
|
|
230
|
-
const id = this.extractId(input);
|
|
231
|
-
if (!id) return null;
|
|
232
|
-
|
|
233
|
-
const url = this.toShareUrl(input, "track", id);
|
|
234
|
-
const meta = await this.fetchOEmbed(url).catch(() => undefined);
|
|
235
|
-
const title = meta?.title || `Spotify Track ${id}`;
|
|
236
|
-
const thumbnail = meta?.thumbnail_url;
|
|
237
|
-
|
|
238
|
-
const track: Track = {
|
|
239
|
-
id,
|
|
240
|
-
title,
|
|
241
|
-
url,
|
|
242
|
-
duration: 0,
|
|
243
|
-
thumbnail,
|
|
244
|
-
requestedBy,
|
|
245
|
-
source: this.name,
|
|
246
|
-
metadata: {
|
|
247
|
-
author: meta?.author_name,
|
|
248
|
-
provider: meta?.provider_name,
|
|
249
|
-
spotify_id: id,
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
return track;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private async buildHeaderItem(input: string, requestedBy: string, kind: "playlist" | "album"): Promise<Track | null> {
|
|
256
|
-
const id = this.extractId(input);
|
|
257
|
-
if (!id) return null;
|
|
258
|
-
const url = this.toShareUrl(input, kind, id);
|
|
259
|
-
const meta = await this.fetchOEmbed(url).catch(() => undefined);
|
|
260
|
-
|
|
261
|
-
const title = meta?.title || `Spotify ${kind} ${id}`;
|
|
262
|
-
const thumbnail = meta?.thumbnail_url;
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
id,
|
|
266
|
-
title,
|
|
267
|
-
url,
|
|
268
|
-
duration: 0,
|
|
269
|
-
thumbnail,
|
|
270
|
-
requestedBy,
|
|
271
|
-
source: this.name,
|
|
272
|
-
metadata: {
|
|
273
|
-
author: meta?.author_name,
|
|
274
|
-
provider: meta?.provider_name,
|
|
275
|
-
spotify_id: id,
|
|
276
|
-
kind,
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private toShareUrl(input: string, expectedKind: string, id: string): string {
|
|
282
|
-
if (input.startsWith("spotify:")) {
|
|
283
|
-
return `https://open.spotify.com/${expectedKind}/${id}`;
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
const u = new URL(input);
|
|
287
|
-
const parts = u.pathname.split("/").filter(Boolean);
|
|
288
|
-
const kind = parts[0] || expectedKind;
|
|
289
|
-
const realId = parts[1] || id;
|
|
290
|
-
return `https://open.spotify.com/${kind}/${realId}`;
|
|
291
|
-
} catch {
|
|
292
|
-
return `https://open.spotify.com/${expectedKind}/${id}`;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private async fetchOEmbed(pageUrl: string): Promise<{
|
|
297
|
-
title?: string;
|
|
298
|
-
thumbnail_url?: string;
|
|
299
|
-
provider_name?: string;
|
|
300
|
-
author_name?: string;
|
|
301
|
-
}> {
|
|
302
|
-
const endpoint = `https://open.spotify.com/oembed?url=${encodeURIComponent(pageUrl)}`;
|
|
303
|
-
const res = await fetch(endpoint);
|
|
304
|
-
if (!res.ok) throw new Error(`oEmbed HTTP ${res.status}`);
|
|
305
|
-
return res.json() as Promise<{
|
|
306
|
-
title?: string;
|
|
307
|
-
thumbnail_url?: string;
|
|
308
|
-
provider_name?: string;
|
|
309
|
-
author_name?: string;
|
|
310
|
-
}>;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
1
|
+
import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A minimal Spotify plugin for metadata extraction and display purposes.
|
|
5
|
+
*
|
|
6
|
+
* This plugin provides support for:
|
|
7
|
+
* - Spotify track URLs/URIs (spotify:track:...)
|
|
8
|
+
* - Spotify playlist URLs/URIs (spotify:playlist:...)
|
|
9
|
+
* - Spotify album URLs/URIs (spotify:album:...)
|
|
10
|
+
* - Metadata extraction using Spotify's public oEmbed endpoint
|
|
11
|
+
*
|
|
12
|
+
* **Important Notes:**
|
|
13
|
+
* - This plugin does NOT provide audio streams (player is expected to redirect/fallback upstream)
|
|
14
|
+
* - This plugin does NOT expand playlists/albums (no SDK; oEmbed doesn't enumerate items)
|
|
15
|
+
* - This plugin only provides display metadata for Spotify content
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
*
|
|
19
|
+
* const spotifyPlugin = new SpotifyPlugin();
|
|
20
|
+
*
|
|
21
|
+
* // Add to PlayerManager
|
|
22
|
+
* const manager = new PlayerManager({
|
|
23
|
+
* plugins: [spotifyPlugin]
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Get metadata for a Spotify track
|
|
27
|
+
* const result = await spotifyPlugin.search("spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "user123");
|
|
28
|
+
* console.log(result.tracks[0].metadata); // Contains Spotify metadata
|
|
29
|
+
*
|
|
30
|
+
*
|
|
31
|
+
* @since 1.1.0
|
|
32
|
+
*/
|
|
33
|
+
export class SpotifyPlugin extends BasePlugin {
|
|
34
|
+
name = "spotify";
|
|
35
|
+
version = "1.1.0";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determines if this plugin can handle the given query.
|
|
39
|
+
*
|
|
40
|
+
* @param query - The search query or URL to check
|
|
41
|
+
* @returns `true` if the query is a Spotify URL/URI, `false` otherwise
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
*
|
|
45
|
+
* plugin.canHandle("spotify:track:4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
46
|
+
* plugin.canHandle("https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
47
|
+
* plugin.canHandle("youtube.com/watch?v=123"); // false
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
canHandle(query: string): boolean {
|
|
51
|
+
const q = query.toLowerCase().trim();
|
|
52
|
+
if (q.startsWith("spotify:")) return true;
|
|
53
|
+
try {
|
|
54
|
+
const u = new URL(q);
|
|
55
|
+
return u.hostname === "open.spotify.com";
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validates if a URL/URI is a valid Spotify URL/URI.
|
|
63
|
+
*
|
|
64
|
+
* @param url - The URL/URI to validate
|
|
65
|
+
* @returns `true` if the URL/URI is a valid Spotify URL/URI, `false` otherwise
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
*
|
|
69
|
+
* plugin.validate("spotify:track:4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
70
|
+
* plugin.validate("https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh"); // true
|
|
71
|
+
* plugin.validate("https://youtube.com/watch?v=123"); // false
|
|
72
|
+
*
|
|
73
|
+
*/
|
|
74
|
+
validate(url: string): boolean {
|
|
75
|
+
if (url.startsWith("spotify:")) return true;
|
|
76
|
+
try {
|
|
77
|
+
const u = new URL(url);
|
|
78
|
+
return u.hostname === "open.spotify.com";
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extracts metadata from Spotify URLs/URIs using the oEmbed API.
|
|
86
|
+
*
|
|
87
|
+
* This method handles Spotify track, playlist, and album URLs/URIs by fetching
|
|
88
|
+
* display metadata from Spotify's public oEmbed endpoint. It does not provide
|
|
89
|
+
* audio streams or expand playlists/albums.
|
|
90
|
+
*
|
|
91
|
+
* @param query - The Spotify URL/URI to extract metadata from
|
|
92
|
+
* @param requestedBy - The user ID who requested the extraction
|
|
93
|
+
* @returns A SearchResult containing a single track with metadata (no audio stream)
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
*
|
|
97
|
+
* // Extract track metadata
|
|
98
|
+
* const result = await plugin.search("spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "user123");
|
|
99
|
+
* console.log(result.tracks[0].metadata); // Contains Spotify metadata
|
|
100
|
+
*
|
|
101
|
+
* // Extract playlist metadata
|
|
102
|
+
* const playlistResult = await plugin.search("https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M", "user123");
|
|
103
|
+
* console.log(playlistResult.tracks[0].metadata.kind); // "playlist"
|
|
104
|
+
*
|
|
105
|
+
*/
|
|
106
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
107
|
+
if (!this.validate(query)) {
|
|
108
|
+
return { tracks: [] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const kind = this.identifyKind(query);
|
|
112
|
+
|
|
113
|
+
if (kind === "track") {
|
|
114
|
+
const t = await this.buildTrackFromUrlOrUri(query, requestedBy);
|
|
115
|
+
return { tracks: t ? [t] : [] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (kind === "playlist") {
|
|
119
|
+
const t = await this.buildHeaderItem(query, requestedBy, "playlist");
|
|
120
|
+
return { tracks: t ? [t] : [] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (kind === "album") {
|
|
124
|
+
const t = await this.buildHeaderItem(query, requestedBy, "album");
|
|
125
|
+
return { tracks: t ? [t] : [] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { tracks: [] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extracts tracks from a Spotify playlist URL.
|
|
133
|
+
*
|
|
134
|
+
* **Note:** This method is not implemented as this plugin does not support
|
|
135
|
+
* playlist expansion. It always returns an empty array.
|
|
136
|
+
*
|
|
137
|
+
* @param _input - The Spotify playlist URL (unused)
|
|
138
|
+
* @param _requestedBy - The user ID who requested the extraction (unused)
|
|
139
|
+
* @returns An empty array (playlist expansion not supported)
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
*
|
|
143
|
+
* const tracks = await plugin.extractPlaylist("spotify:playlist:123", "user123");
|
|
144
|
+
* console.log(tracks); // [] - empty array
|
|
145
|
+
*
|
|
146
|
+
*/
|
|
147
|
+
async extractPlaylist(_input: string, _requestedBy: string): Promise<Track[]> {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extracts tracks from a Spotify album URL.
|
|
153
|
+
*
|
|
154
|
+
* **Note:** This method is not implemented as this plugin does not support
|
|
155
|
+
* album expansion. It always returns an empty array.
|
|
156
|
+
*
|
|
157
|
+
* @param _input - The Spotify album URL (unused)
|
|
158
|
+
* @param _requestedBy - The user ID who requested the extraction (unused)
|
|
159
|
+
* @returns An empty array (album expansion not supported)
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
*
|
|
163
|
+
* const tracks = await plugin.extractAlbum("spotify:album:123", "user123");
|
|
164
|
+
* console.log(tracks); // [] - empty array
|
|
165
|
+
*
|
|
166
|
+
*/
|
|
167
|
+
async extractAlbum(_input: string, _requestedBy: string): Promise<Track[]> {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Attempts to get an audio stream for a Spotify track.
|
|
173
|
+
*
|
|
174
|
+
* **Note:** This method always throws an error as this plugin does not support
|
|
175
|
+
* audio streaming. The player is expected to redirect to other plugins or
|
|
176
|
+
* use fallback mechanisms for actual audio playback.
|
|
177
|
+
*
|
|
178
|
+
* @param _track - The Track object (unused)
|
|
179
|
+
* @throws {Error} Always throws "Spotify streaming is not supported by this plugin"
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
*
|
|
183
|
+
* try {
|
|
184
|
+
* const stream = await plugin.getStream(track);
|
|
185
|
+
* } catch (error) {
|
|
186
|
+
* console.log(error.message); // "Spotify streaming is not supported by this plugin"
|
|
187
|
+
* }
|
|
188
|
+
*
|
|
189
|
+
*/
|
|
190
|
+
async getStream(_track: Track): Promise<StreamInfo> {
|
|
191
|
+
throw new Error("Spotify streaming is not supported by this plugin");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private identifyKind(input: string): "track" | "playlist" | "album" | "unknown" {
|
|
195
|
+
if (input.startsWith("spotify:")) {
|
|
196
|
+
if (input.includes(":track:")) return "track";
|
|
197
|
+
if (input.includes(":playlist:")) return "playlist";
|
|
198
|
+
if (input.includes(":album:")) return "album";
|
|
199
|
+
return "unknown";
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const u = new URL(input);
|
|
203
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
204
|
+
const kind = parts[0];
|
|
205
|
+
if (kind === "track") return "track";
|
|
206
|
+
if (kind === "playlist") return "playlist";
|
|
207
|
+
if (kind === "album") return "album";
|
|
208
|
+
return "unknown";
|
|
209
|
+
} catch {
|
|
210
|
+
return "unknown";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private extractId(input: string): string | null {
|
|
215
|
+
if (!input) return null;
|
|
216
|
+
if (input.startsWith("spotify:")) {
|
|
217
|
+
const parts = input.split(":");
|
|
218
|
+
return parts[2] || null;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const u = new URL(input);
|
|
222
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
223
|
+
return parts[1] || null; // /track/<id>
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async buildTrackFromUrlOrUri(input: string, requestedBy: string): Promise<Track | null> {
|
|
230
|
+
const id = this.extractId(input);
|
|
231
|
+
if (!id) return null;
|
|
232
|
+
|
|
233
|
+
const url = this.toShareUrl(input, "track", id);
|
|
234
|
+
const meta = await this.fetchOEmbed(url).catch(() => undefined);
|
|
235
|
+
const title = meta?.title || `Spotify Track ${id}`;
|
|
236
|
+
const thumbnail = meta?.thumbnail_url;
|
|
237
|
+
|
|
238
|
+
const track: Track = {
|
|
239
|
+
id,
|
|
240
|
+
title,
|
|
241
|
+
url,
|
|
242
|
+
duration: 0,
|
|
243
|
+
thumbnail,
|
|
244
|
+
requestedBy,
|
|
245
|
+
source: this.name,
|
|
246
|
+
metadata: {
|
|
247
|
+
author: meta?.author_name,
|
|
248
|
+
provider: meta?.provider_name,
|
|
249
|
+
spotify_id: id,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
return track;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async buildHeaderItem(input: string, requestedBy: string, kind: "playlist" | "album"): Promise<Track | null> {
|
|
256
|
+
const id = this.extractId(input);
|
|
257
|
+
if (!id) return null;
|
|
258
|
+
const url = this.toShareUrl(input, kind, id);
|
|
259
|
+
const meta = await this.fetchOEmbed(url).catch(() => undefined);
|
|
260
|
+
|
|
261
|
+
const title = meta?.title || `Spotify ${kind} ${id}`;
|
|
262
|
+
const thumbnail = meta?.thumbnail_url;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
id,
|
|
266
|
+
title,
|
|
267
|
+
url,
|
|
268
|
+
duration: 0,
|
|
269
|
+
thumbnail,
|
|
270
|
+
requestedBy,
|
|
271
|
+
source: this.name,
|
|
272
|
+
metadata: {
|
|
273
|
+
author: meta?.author_name,
|
|
274
|
+
provider: meta?.provider_name,
|
|
275
|
+
spotify_id: id,
|
|
276
|
+
kind,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private toShareUrl(input: string, expectedKind: string, id: string): string {
|
|
282
|
+
if (input.startsWith("spotify:")) {
|
|
283
|
+
return `https://open.spotify.com/${expectedKind}/${id}`;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const u = new URL(input);
|
|
287
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
288
|
+
const kind = parts[0] || expectedKind;
|
|
289
|
+
const realId = parts[1] || id;
|
|
290
|
+
return `https://open.spotify.com/${kind}/${realId}`;
|
|
291
|
+
} catch {
|
|
292
|
+
return `https://open.spotify.com/${expectedKind}/${id}`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private async fetchOEmbed(pageUrl: string): Promise<{
|
|
297
|
+
title?: string;
|
|
298
|
+
thumbnail_url?: string;
|
|
299
|
+
provider_name?: string;
|
|
300
|
+
author_name?: string;
|
|
301
|
+
}> {
|
|
302
|
+
const endpoint = `https://open.spotify.com/oembed?url=${encodeURIComponent(pageUrl)}`;
|
|
303
|
+
const res = await fetch(endpoint);
|
|
304
|
+
if (!res.ok) throw new Error(`oEmbed HTTP ${res.status}`);
|
|
305
|
+
return res.json() as Promise<{
|
|
306
|
+
title?: string;
|
|
307
|
+
thumbnail_url?: string;
|
|
308
|
+
provider_name?: string;
|
|
309
|
+
author_name?: string;
|
|
310
|
+
}>;
|
|
311
|
+
}
|
|
312
|
+
}
|