@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,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
+ }