@ziplayer/ytexecplug 0.0.1

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 ADDED
@@ -0,0 +1,216 @@
1
+ <img width="1175" height="305" alt="logo" src="https://raw.githubusercontent.com/ZiProject/ZiPlayer/refs/heads/main/publish/logo.png" />
2
+
3
+ # @ziplayer/plugin
4
+
5
+ Official plugin bundle for ZiPlayer. It ships a set of ready‑to‑use source plugins you can register on your `PlayerManager`:
6
+
7
+ - YouTubePlugin: search + stream YouTube videos and playlists
8
+ - SoundCloudPlugin: search + stream SoundCloud tracks and sets
9
+ - SpotifyPlugin: resolve tracks/albums/playlists, stream via fallbacks
10
+ - TTSPlugin: Text‑to‑Speech playback from simple `tts:` queries
11
+ - AttachmentsPlugin: handle Discord attachment URLs and direct audio file URLs
12
+
13
+ ZiPlayer is an audio player built on top of `@discordjs/voice` and `discord.js`. This package provides sources; the core player
14
+ lives in `ziplayer`.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @ziplayer/plugin ziplayer @discordjs/voice discord.js
20
+ ```
21
+
22
+ The TTS plugin uses a lightweight Google TTS wrapper and HTTP fetches:
23
+
24
+ ```bash
25
+ npm install @zibot/zitts axios
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```ts
31
+ import { PlayerManager } from "ziplayer";
32
+ import { YouTubePlugin, SoundCloudPlugin, SpotifyPlugin, TTSPlugin, AttachmentsPlugin } from "@ziplayer/plugin";
33
+ import { YTexec } from "@ziplayer/ytexecplug";
34
+
35
+ const ytbplg = new YouTubePlugin({ player: null });
36
+
37
+ ytbplg.getStream = new YTexec().getStream;
38
+
39
+ //create Player Manager
40
+ const manager = new PlayerManager({
41
+ plugins: [new TTSPlugin(), ytbplg, new SoundCloudPlugin(), new SpotifyPlugin(), new AttachmentsPlugin()],
42
+ extensions: [new lyricsExt(), new voiceExt(null, { client, minimalVoiceMessageDuration: 1 })],
43
+ });
44
+
45
+ // Create and connect a player (discord.js VoiceChannel instance)
46
+ const player = await manager.create(guildId, { userdata: { channel: textChannel } });
47
+ await player.connect(voiceChannel);
48
+
49
+ // Search & play
50
+ await player.play("never gonna give you up", requestedBy);
51
+
52
+ // Play a playlist URL directly
53
+ await player.play("https://www.youtube.com/playlist?list=...", requestedBy);
54
+
55
+ // Speak with TTS
56
+ await player.play("tts:en:Hello there!", requestedBy);
57
+
58
+ // Play Discord attachment
59
+ await player.play("https://cdn.discordapp.com/attachments/123/456/audio.mp3", requestedBy);
60
+
61
+ // Handle events via the manager
62
+ manager.on("trackStart", (plr, track) => {
63
+ plr.userdata?.channel?.send?.(`Now playing: ${track.title}`);
64
+ });
65
+ ```
66
+
67
+ ## Included Plugins
68
+
69
+ ### YouTubePlugin
70
+
71
+ - Resolves YouTube videos and playlists.
72
+ - Uses `youtubei.js` under the hood.
73
+
74
+ ```ts
75
+ import { YouTubePlugin } from "@ziplayer/plugin";
76
+ const youtube = new YouTubePlugin();
77
+ ```
78
+
79
+ ### SoundCloudPlugin
80
+
81
+ - Resolves tracks and sets. You may further tune streaming by combining with other plugins that provide fallbacks.
82
+
83
+ ```ts
84
+ import { SoundCloudPlugin } from "@ziplayer/plugin";
85
+ const sc = new SoundCloudPlugin();
86
+ ```
87
+
88
+ ### SpotifyPlugin
89
+
90
+ - Resolves track/album/playlist metadata from Spotify.
91
+ - Streaming typically uses fallback sources (e.g., YouTube) discovered by your plugin set.
92
+
93
+ ```ts
94
+ import { SpotifyPlugin } from "@ziplayer/plugin";
95
+ const sp = new SpotifyPlugin();
96
+ ```
97
+
98
+ ### TTSPlugin (Text‑to‑Speech)
99
+
100
+ - Plays spoken audio from text using a lightweight Google TTS wrapper.
101
+ - **Accurate duration analysis**: Generates sample audio to measure actual duration instead of estimating.
102
+ - Supported query formats:
103
+ - `tts: <text>`
104
+ - `tts:<lang>:<text>` (e.g., `tts:vi:xin chao`)
105
+ - `tts:<lang>:1:<text>` (set `slow = true`, `0` = normal)
106
+
107
+ ```ts
108
+ import { TTSPlugin } from "@ziplayer/plugin";
109
+ const tts = new TTSPlugin({ defaultLang: "en", slow: false });
110
+
111
+ // The plugin automatically analyzes TTS duration
112
+ const result = await tts.search("tts:en:Hello world", "user123");
113
+ console.log(`Duration: ${result.tracks[0].duration}s`); // Real duration from audio analysis
114
+ console.log(`Language: ${result.tracks[0].metadata.language}`); // "en"
115
+ console.log(`Slow mode: ${result.tracks[0].metadata.slowMode}`); // false
116
+
117
+ await player.play("tts:en:1:good morning", requestedBy);
118
+ ```
119
+
120
+ Note: Please comply with the service’s terms and provide your own quotas. The wrapper is intended for lightweight usage and may
121
+ change without notice.
122
+
123
+ Advanced: custom TTS provider
124
+
125
+ You can override audio generation by passing a `createStream` function. It receives the text and context and can return a Node
126
+ `Readable`, an HTTP(S) URL string, or a `Buffer`.
127
+
128
+ ```ts
129
+ const tts = new TTSPlugin({
130
+ defaultLang: "vi",
131
+ async createStream(text, ctx) {
132
+ // Example: integrate with Azure, CAMB.AI, etc.
133
+ // Return a URL and the plugin will stream it
134
+ const url = await myTTSService(text, { lang: ctx?.lang, slow: ctx?.slow });
135
+ return url; // or Readable / Buffer
136
+ },
137
+ });
138
+ ```
139
+
140
+ ### AttachmentsPlugin
141
+
142
+ - Handles Discord attachment URLs and direct audio file URLs.
143
+ - Supports various audio formats (mp3, wav, ogg, m4a, flac, etc.).
144
+ - **Audio metadata analysis**: Extracts duration, title, artist, album, bitrate, etc.
145
+ - Includes file size validation and proper error handling.
146
+ - Uses Range requests to efficiently analyze metadata without downloading entire files.
147
+
148
+ ```ts
149
+ import { AttachmentsPlugin } from "@ziplayer/plugin";
150
+ const attachments = new AttachmentsPlugin({
151
+ maxFileSize: 25 * 1024 * 1024, // 25MB
152
+ allowedExtensions: ["mp3", "wav", "ogg", "m4a", "flac"],
153
+ debug: true, // Enable to see metadata analysis process
154
+ });
155
+
156
+ // The plugin automatically analyzes audio metadata
157
+ const result = await attachments.search("https://cdn.discordapp.com/attachments/123/456/song.mp3", "user123");
158
+ console.log(`Duration: ${result.tracks[0].duration}s`); // Real duration from metadata
159
+ console.log(`Title: ${result.tracks[0].title}`); // May be extracted from metadata
160
+ console.log(`Artist: ${result.tracks[0].metadata.artist}`); // From metadata
161
+ ```
162
+
163
+ ## Writing Your Own Plugin
164
+
165
+ Plugins implement the `BasePlugin` contract from `ziplayer`:
166
+
167
+ ```ts
168
+ import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
169
+
170
+ export class MyPlugin extends BasePlugin {
171
+ name = "myplugin";
172
+ version = "1.0.0";
173
+
174
+ canHandle(query: string): boolean {
175
+ // Return true if this plugin can handle a given query/URL
176
+ return query.includes("mysite.com");
177
+ }
178
+
179
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
180
+ // Return one or more tracks for the query
181
+ return {
182
+ tracks: [
183
+ {
184
+ id: "abc",
185
+ title: "My Track",
186
+ url: "https://mysite.com/track/abc",
187
+ duration: 180,
188
+ requestedBy,
189
+ source: this.name,
190
+ },
191
+ ],
192
+ };
193
+ }
194
+
195
+ async getStream(track: Track): Promise<StreamInfo> {
196
+ // Return a Node Readable stream and an input type
197
+ return { stream, type: "arbitrary" };
198
+ }
199
+ }
200
+ ```
201
+
202
+ Tips
203
+
204
+ - Keep network calls bounded; ZiPlayer applies timeouts to extractor operations.
205
+ - For sources that require indirection (like Spotify), consider a `getFallback` strategy via other plugins.
206
+ - Use `track.metadata` for any source‑specific fields you want to carry along.
207
+
208
+ ## Requirements
209
+
210
+ - Node.js 18+
211
+ - `discord.js` 14 and `@discordjs/voice` 0.19+
212
+ - For TTS: `@zibot/zitts` and `axios`
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=YouTubePlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubePlugin.d.ts","sourceRoot":"","sources":["../src/YouTubePlugin.ts"],"names":[],"mappings":""}
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ // import { BasePlugin, Track, SearchResult, StreamInfo, Player } from "ziplayer";
3
+ // export interface PluginOptions {
4
+ // player: Player;
5
+ // debug?: boolean;
6
+ // }
7
+ // /**
8
+ // * A plugin for handling YouTube audio content including videos, playlists, and search functionality.
9
+ // *
10
+ // * This plugin provides comprehensive support for:
11
+ // * - YouTube video URLs (youtube.com, youtu.be, music.youtube.com)
12
+ // * - YouTube playlist URLs and dynamic mixes
13
+ // * - YouTube search queries
14
+ // * - Audio stream extraction from YouTube videos
15
+ // * - Related track recommendations
16
+ // *
17
+ // * @example
18
+ // * const youtubePlugin = new YouTubePlugin();
19
+ // *
20
+ // * // Add to PlayerManager
21
+ // * const manager = new PlayerManager({
22
+ // * plugins: [youtubePlugin]
23
+ // * });
24
+ // *
25
+ // * // Search for videos
26
+ // * const result = await youtubePlugin.search("Never Gonna Give You Up", "user123");
27
+ // *
28
+ // * // Get audio stream
29
+ // * const stream = await youtubePlugin.getStream(result.tracks[0]);
30
+ // *
31
+ // * @since 1.0.0
32
+ // */
33
+ // export class YouTubePlugin extends BasePlugin {
34
+ // name = "youtube";
35
+ // version = "1.0.0";
36
+ // private ready: Promise<void>;
37
+ // private player: Player | undefined;
38
+ // private options: PluginOptions;
39
+ // /**
40
+ // * Creates a new YouTubePlugin instance.
41
+ // *
42
+ // * The plugin will automatically initialize YouTube clients for both video playback
43
+ // * and search functionality. Initialization is asynchronous and handled internally.
44
+ // *
45
+ // * @example
46
+ // * const plugin = new YouTubePlugin();
47
+ // * // Plugin is ready to use after initialization completes
48
+ // */
49
+ // constructor(options: PluginOptions) {
50
+ // super();
51
+ // this.player = options?.player ?? undefined;
52
+ // this.options = options ?? {};
53
+ // this.ready = this.init();
54
+ // }
55
+ // private async init(): Promise<void> {}
56
+ // private debug(message?: any, ...optionalParams: any[]): void {
57
+ // if (this.options?.debug && this?.player && this.player?.listenerCount("debug") > 0) {
58
+ // this.player.emit("debug", `[YouTubePlugin] ${message}`, ...optionalParams);
59
+ // }
60
+ // }
61
+ // // Build a Track from various YouTube object shapes (search item, playlist item, watch_next feed, basic_info, info)
62
+ // private buildTrack(raw: any, requestedBy: string, extra?: { playlist?: string }): Track {
63
+ // const pickFirst = (...vals: any[]) => vals.find((v) => v !== undefined && v !== null && v !== "");
64
+ // // Try to resolve from multiple common shapes
65
+ // const id = pickFirst(
66
+ // raw?.id,
67
+ // raw?.video_id,
68
+ // raw?.videoId,
69
+ // raw?.content_id,
70
+ // raw?.identifier,
71
+ // raw?.basic_info?.id,
72
+ // raw?.basic_info?.video_id,
73
+ // raw?.basic_info?.videoId,
74
+ // raw?.basic_info?.content_id,
75
+ // );
76
+ // const title = pickFirst(
77
+ // raw?.metadata?.title?.text,
78
+ // raw?.title?.text,
79
+ // raw?.title,
80
+ // raw?.headline,
81
+ // raw?.basic_info?.title,
82
+ // "Unknown title",
83
+ // );
84
+ // const durationValue = pickFirst(
85
+ // raw?.length_seconds,
86
+ // raw?.duration?.seconds,
87
+ // raw?.duration?.text,
88
+ // raw?.duration,
89
+ // raw?.length_text,
90
+ // raw?.basic_info?.duration,
91
+ // );
92
+ // const duration = Number(toSeconds(durationValue)) || 0;
93
+ // const thumb = pickFirst(
94
+ // raw?.thumbnails?.[0]?.url,
95
+ // raw?.thumbnail?.[0]?.url,
96
+ // raw?.thumbnail?.url,
97
+ // raw?.thumbnail?.thumbnails?.[0]?.url,
98
+ // raw?.content_image?.image?.[0]?.url,
99
+ // raw?.basic_info?.thumbnail?.[0]?.url,
100
+ // raw?.basic_info?.thumbnail?.[raw?.basic_info?.thumbnail?.length - 1]?.url,
101
+ // raw?.thumbnails?.[raw?.thumbnails?.length - 1]?.url,
102
+ // );
103
+ // const author = pickFirst(raw?.author?.name, raw?.author, raw?.channel?.name, raw?.owner?.name, raw?.basic_info?.author);
104
+ // const views = pickFirst(
105
+ // raw?.view_count,
106
+ // raw?.views,
107
+ // raw?.short_view_count,
108
+ // raw?.stats?.view_count,
109
+ // raw?.basic_info?.view_count,
110
+ // );
111
+ // const url = pickFirst(raw?.url, id ? `https://www.youtube.com/watch?v=${id}` : undefined);
112
+ // this.debug("Track build:", {
113
+ // id: String(id),
114
+ // title: String(title),
115
+ // url: String(url),
116
+ // duration,
117
+ // thumbnail: thumb,
118
+ // requestedBy,
119
+ // source: this.name,
120
+ // });
121
+ // return {
122
+ // id: String(id),
123
+ // title: String(title),
124
+ // url: String(url),
125
+ // duration,
126
+ // thumbnail: thumb,
127
+ // requestedBy,
128
+ // source: this.name,
129
+ // metadata: {
130
+ // author,
131
+ // views,
132
+ // ...(extra?.playlist ? { playlist: extra.playlist } : {}),
133
+ // },
134
+ // } as Track;
135
+ // }
136
+ // /**
137
+ // * Determines if this plugin can handle the given query.
138
+ // *
139
+ // * @param query - The search query or URL to check
140
+ // * @returns `true` if the plugin can handle the query, `false` otherwise
141
+ // *
142
+ // * @example
143
+ // * plugin.canHandle("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
144
+ // * plugin.canHandle("Never Gonna Give You Up"); // true
145
+ // * plugin.canHandle("spotify:track:123"); // false
146
+ // */
147
+ // canHandle(query: string): boolean {
148
+ // const q = (query || "").trim().toLowerCase();
149
+ // const isUrl = q.startsWith("http://") || q.startsWith("https://");
150
+ // if (isUrl) {
151
+ // try {
152
+ // const parsed = new URL(query);
153
+ // const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
154
+ // return allowedHosts.includes(parsed.hostname.toLowerCase());
155
+ // } catch (e) {
156
+ // return false;
157
+ // }
158
+ // }
159
+ // // Avoid intercepting explicit patterns for other extractors
160
+ // if (q.startsWith("tts:") || q.startsWith("say ")) return false;
161
+ // if (q.startsWith("spotify:") || q.includes("open.spotify.com")) return false;
162
+ // if (q.includes("soundcloud")) return false;
163
+ // // Treat remaining non-URL free text as YouTube-searchable
164
+ // return true;
165
+ // }
166
+ // /**
167
+ // * Validates if a URL is a valid YouTube URL.
168
+ // *
169
+ // * @param url - The URL to validate
170
+ // * @returns `true` if the URL is a valid YouTube URL, `false` otherwise
171
+ // *
172
+ // * @example
173
+ // * plugin.validate("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
174
+ // * plugin.validate("https://youtu.be/dQw4w9WgXcQ"); // true
175
+ // * plugin.validate("https://spotify.com/track/123"); // false
176
+ // */
177
+ // validate(url: string): boolean {
178
+ // try {
179
+ // const parsed = new URL(url);
180
+ // const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be", "m.youtube.com"];
181
+ // return allowedHosts.includes(parsed.hostname.toLowerCase());
182
+ // } catch (e) {
183
+ // return false;
184
+ // }
185
+ // }
186
+ // /**
187
+ // * Retrieves the audio stream for a YouTube track using sabr download.
188
+ // *
189
+ // * This method extracts the audio stream from a YouTube video using the sabr download
190
+ // * method which provides better quality and more reliable streaming.
191
+ // *
192
+ // * @param track - The Track object to get the stream for
193
+ // * @returns A StreamInfo object containing the audio stream and metadata
194
+ // * @throws {Error} If the track ID is invalid or stream extraction fails
195
+ // *
196
+ // * @example
197
+ // * const track = { id: "dQw4w9WgXcQ", title: "Never Gonna Give You Up", ... };
198
+ // * const streamInfo = await plugin.getStream(track);
199
+ // * console.log(streamInfo.type); // "arbitrary"
200
+ // * console.log(streamInfo.stream); // Readable stream
201
+ // */
202
+ // async getStream(track: Track): Promise<StreamInfo> {
203
+ // await this.ready;
204
+ // const id = this.extractVideoId(track.url) || track.id;
205
+ // if (!id) throw new Error("Invalid track id");
206
+ // try {
207
+ // this.debug("🚀 Attempting sabr download for video ID:", id);
208
+ // // Use sabr download for better quality and reliability
209
+ // const { streamResults } = await createSabrStream(id, DEFAULT_SABR_OPTIONS);
210
+ // const { audioStream, selectedFormats, videoTitle } = streamResults;
211
+ // this.debug("✅ Sabr download successful, converting Web Stream to Node.js Stream");
212
+ // // Convert Web Stream to Node.js Readable Stream
213
+ // const nodeStream = webStreamToNodeStream(audioStream);
214
+ // this.debug("✅ Stream conversion complete, returning Node.js stream");
215
+ // // Return the converted Node.js stream
216
+ // return {
217
+ // stream: nodeStream,
218
+ // type: "arbitrary",
219
+ // metadata: {
220
+ // ...track.metadata,
221
+ // itag: selectedFormats.audioFormat.itag,
222
+ // mime: selectedFormats.audioFormat.mimeType,
223
+ // },
224
+ // };
225
+ // } catch (e: any) {
226
+ // this.debug("⚠️ Sabr download failed, falling back to youtubei.js:", e.message);
227
+ // // Fallback to original youtubei.js method if sabr download fails
228
+ // try {
229
+ // const stream: any = await (this.client as any).download(id, {
230
+ // type: "audio",
231
+ // quality: "best",
232
+ // });
233
+ // // Check if it's a Web Stream and convert it
234
+ // this.debug("🔍 Checking stream type:", typeof stream, stream?.constructor?.name);
235
+ // if (stream && typeof stream.getReader === "function") {
236
+ // this.debug("🔄 Converting Web Stream to Node.js Stream");
237
+ // const nodeStream = webStreamToNodeStream(stream);
238
+ // this.debug("✅ Stream converted successfully");
239
+ // return {
240
+ // stream: nodeStream,
241
+ // type: "arbitrary",
242
+ // metadata: track.metadata,
243
+ // };
244
+ // } else {
245
+ // this.debug("⚠️ Stream is not a Web Stream or is null");
246
+ // }
247
+ // return {
248
+ // stream,
249
+ // type: "arbitrary",
250
+ // metadata: track.metadata,
251
+ // };
252
+ // } catch (fallbackError: any) {
253
+ // try {
254
+ // const info: any = await (this.client as any).getBasicInfo(id);
255
+ // // Prefer m4a audio-only formats first
256
+ // let format: any = info?.chooseFormat?.({
257
+ // type: "audio",
258
+ // quality: "best",
259
+ // });
260
+ // if (!format && info?.formats?.length) {
261
+ // const audioOnly = info.formats.filter((f: any) => f.mime_type?.includes("audio"));
262
+ // audioOnly.sort((a: any, b: any) => (b.bitrate || 0) - (a.bitrate || 0));
263
+ // format = audioOnly[0];
264
+ // }
265
+ // if (!format) throw new Error("No audio format available");
266
+ // let url: string | undefined = undefined;
267
+ // if (typeof format.decipher === "function") {
268
+ // url = format.decipher((this.client as any).session.player);
269
+ // }
270
+ // if (!url) url = format.url;
271
+ // if (!url) throw new Error("No valid URL to decipher");
272
+ // const res = await fetch(url);
273
+ // if (!res.ok || !res.body) {
274
+ // throw new Error(`HTTP ${res.status}`);
275
+ // }
276
+ // // Convert Web Stream to Node.js Stream
277
+ // this.debug("🔄 Converting fetch response Web Stream to Node.js Stream");
278
+ // const nodeStream = webStreamToNodeStream(res.body);
279
+ // return {
280
+ // stream: nodeStream,
281
+ // type: "arbitrary",
282
+ // metadata: {
283
+ // ...track.metadata,
284
+ // itag: format.itag,
285
+ // mime: format.mime_type,
286
+ // },
287
+ // };
288
+ // } catch (inner: any) {
289
+ // throw new Error(`Failed to get YouTube stream: ${inner?.message || inner}`);
290
+ // }
291
+ // }
292
+ // }
293
+ // }
294
+ // async getFallback(track: Track): Promise<StreamInfo> {
295
+ // try {
296
+ // const result = await this.search(track.title, track.requestedBy);
297
+ // const first = result.tracks[0];
298
+ // this.debug("Fallback track:", first);
299
+ // if (!first) throw new Error("No fallback track found");
300
+ // return await this.getStream(first);
301
+ // } catch (e: any) {
302
+ // throw new Error(`YouTube fallback search failed: ${e?.message || e}`);
303
+ // }
304
+ // }
305
+ // private extractVideoId(input: string): string | null {
306
+ // try {
307
+ // const u = new URL(input);
308
+ // const allowedShortHosts = ["youtu.be"];
309
+ // const allowedLongHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "m.youtube.com"];
310
+ // if (allowedShortHosts.includes(u.hostname)) {
311
+ // return u.pathname.split("/").filter(Boolean)[0] || null;
312
+ // }
313
+ // if (allowedLongHosts.includes(u.hostname)) {
314
+ // // watch?v=, shorts/, embed/
315
+ // if (u.searchParams.get("v")) return u.searchParams.get("v");
316
+ // const path = u.pathname;
317
+ // if (path.startsWith("/shorts/")) return path.replace("/shorts/", "");
318
+ // if (path.startsWith("/embed/")) return path.replace("/embed/", "");
319
+ // }
320
+ // return null;
321
+ // } catch {
322
+ // return null;
323
+ // }
324
+ // }
325
+ // }
326
+ // function toSeconds(d: any): number | undefined {
327
+ // if (typeof d === "number") return d;
328
+ // if (typeof d === "string") {
329
+ // // mm:ss or hh:mm:ss
330
+ // const parts = d.split(":").map(Number);
331
+ // if (parts.some((n) => Number.isNaN(n))) return undefined;
332
+ // if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
333
+ // if (parts.length === 2) return parts[0] * 60 + parts[1];
334
+ // const asNum = Number(d);
335
+ // return Number.isFinite(asNum) ? asNum : undefined;
336
+ // }
337
+ // if (d && typeof d === "object") {
338
+ // if (typeof (d as any).seconds === "number") return (d as any).seconds;
339
+ // if (typeof (d as any).milliseconds === "number") return Math.floor((d as any).milliseconds / 1000);
340
+ // }
341
+ // return undefined;
342
+ // }
343
+ //# sourceMappingURL=YouTubePlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubePlugin.js","sourceRoot":"","sources":["../src/YouTubePlugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { BasePlugin } from "ziplayer";
2
+ import { Track, SearchResult, StreamInfo } from "ziplayer";
3
+ import { Readable } from "stream";
4
+ /**
5
+ * Converts a Web ReadableStream to a Node.js Readable stream
6
+ */
7
+ export declare function webStreamToNodeStream(webStream: ReadableStream): Readable;
8
+ export declare class YTexec extends BasePlugin {
9
+ name: string;
10
+ version: string;
11
+ canHandle(query: string): boolean;
12
+ search(query: string, requestedBy: string): Promise<SearchResult>;
13
+ getStream(track: Track): Promise<StreamInfo>;
14
+ }
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGlC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,cAAc,GAAG,QAAQ,CA8BzE;AAoBD,qBAAa,MAAO,SAAQ,UAAU;IACrC,IAAI,SAAY;IAChB,OAAO,SAAW;IAElB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAe3B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIjE,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC;CAqBlD"}
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.YTexec = void 0;
7
+ exports.webStreamToNodeStream = webStreamToNodeStream;
8
+ const ziplayer_1 = require("ziplayer");
9
+ const stream_1 = require("stream");
10
+ const youtube_dl_exec_1 = __importDefault(require("youtube-dl-exec"));
11
+ /**
12
+ * Converts a Web ReadableStream to a Node.js Readable stream
13
+ */
14
+ function webStreamToNodeStream(webStream) {
15
+ const nodeStream = new stream_1.Readable({
16
+ read() {
17
+ // This will be handled by the Web Stream reader
18
+ },
19
+ });
20
+ // Create a reader from the Web Stream
21
+ const reader = webStream.getReader();
22
+ // Read chunks and push to Node.js stream
23
+ const pump = async () => {
24
+ try {
25
+ while (true) {
26
+ const { done, value } = await reader.read();
27
+ if (done) {
28
+ nodeStream.push(null); // End the stream
29
+ break;
30
+ }
31
+ nodeStream.push(Buffer.from(value));
32
+ }
33
+ }
34
+ catch (error) {
35
+ nodeStream.destroy(error);
36
+ }
37
+ };
38
+ // Start pumping data
39
+ pump();
40
+ return nodeStream;
41
+ }
42
+ async function getYoutubeStream(url) {
43
+ const info = await (0, youtube_dl_exec_1.default)(url, {
44
+ dumpSingleJson: true,
45
+ noCheckCertificates: true,
46
+ noWarnings: true,
47
+ preferFreeFormats: true,
48
+ format: "bestaudio/best",
49
+ addHeader: ["referer:youtube.com", "user-agent:googlebot"],
50
+ });
51
+ const videourl = typeof info === "object" ? info?.url : info;
52
+ if (!videourl) {
53
+ return null;
54
+ }
55
+ return videourl;
56
+ }
57
+ class YTexec extends ziplayer_1.BasePlugin {
58
+ constructor() {
59
+ super(...arguments);
60
+ this.name = "YTexec";
61
+ this.version = "1.0.0";
62
+ }
63
+ canHandle(query) {
64
+ const q = (query || "").trim().toLowerCase();
65
+ const isUrl = q.startsWith("http://") || q.startsWith("https://");
66
+ if (isUrl) {
67
+ try {
68
+ const parsed = new URL(query);
69
+ const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
70
+ return allowedHosts.includes(parsed.hostname.toLowerCase());
71
+ }
72
+ catch (e) {
73
+ return false;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ async search(query, requestedBy) {
79
+ return { tracks: [] };
80
+ }
81
+ async getStream(track) {
82
+ try {
83
+ const youtubeUrl = await getYoutubeStream(track.url);
84
+ if (!youtubeUrl) {
85
+ throw new Error("Failed to get YouTube stream URL");
86
+ }
87
+ const response = await fetch(youtubeUrl);
88
+ if (!response.ok || !response.body) {
89
+ throw new Error("Failed to fetch YouTube stream");
90
+ }
91
+ const stream = webStreamToNodeStream(response.body);
92
+ return {
93
+ stream,
94
+ type: "arbitrary",
95
+ metadata: track.metadata,
96
+ };
97
+ }
98
+ catch (error) {
99
+ throw new Error(`Failed to get YouTube stream: ${error}`);
100
+ }
101
+ }
102
+ }
103
+ exports.YTexec = YTexec;
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@ziplayer/ytexecplug",
3
+ "version": "0.0.1",
4
+ "description": "A modular Discord voice player with plugin system",
5
+ "keywords": [
6
+ "ZiPlayer",
7
+ "@ziplayer/plugin",
8
+ "discord",
9
+ "music",
10
+ "player",
11
+ "voice"
12
+ ],
13
+ "homepage": "https://player.ziji.world",
14
+ "bugs": {
15
+ "url": "https://github.com/ZiProject/ZiPlayer/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ZiProject/ZiPlayer.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Ziji",
23
+ "main": "dist/index.js",
24
+ "types": "dist/index.d.ts",
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "dev": "tsc --watch",
28
+ "prepare": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "youtube-dl-exec": "^3.0.30",
32
+ "ziplayer": "^0.2.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "typescript": "^5.0.0"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { BasePlugin } from "ziplayer";
2
+ import { Track, SearchResult, StreamInfo } from "ziplayer";
3
+ import { Readable } from "stream";
4
+ import youtubedl from "youtube-dl-exec";
5
+
6
+ /**
7
+ * Converts a Web ReadableStream to a Node.js Readable stream
8
+ */
9
+ export function webStreamToNodeStream(webStream: ReadableStream): Readable {
10
+ const nodeStream = new Readable({
11
+ read() {
12
+ // This will be handled by the Web Stream reader
13
+ },
14
+ });
15
+
16
+ // Create a reader from the Web Stream
17
+ const reader = webStream.getReader();
18
+
19
+ // Read chunks and push to Node.js stream
20
+ const pump = async () => {
21
+ try {
22
+ while (true) {
23
+ const { done, value } = await reader.read();
24
+ if (done) {
25
+ nodeStream.push(null); // End the stream
26
+ break;
27
+ }
28
+ nodeStream.push(Buffer.from(value));
29
+ }
30
+ } catch (error) {
31
+ nodeStream.destroy(error as Error);
32
+ }
33
+ };
34
+
35
+ // Start pumping data
36
+ pump();
37
+
38
+ return nodeStream;
39
+ }
40
+
41
+ async function getYoutubeStream(url: string): Promise<string | null> {
42
+ const info = await youtubedl(url, {
43
+ dumpSingleJson: true,
44
+ noCheckCertificates: true,
45
+ noWarnings: true,
46
+ preferFreeFormats: true,
47
+ format: "bestaudio/best",
48
+ addHeader: ["referer:youtube.com", "user-agent:googlebot"],
49
+ });
50
+
51
+ const videourl = typeof info === "object" ? (info as any)?.url : info;
52
+ if (!videourl) {
53
+ return null;
54
+ }
55
+
56
+ return videourl;
57
+ }
58
+
59
+ export class YTexec extends BasePlugin {
60
+ name = "YTexec";
61
+ version = "1.0.0";
62
+
63
+ canHandle(query: string): boolean {
64
+ const q = (query || "").trim().toLowerCase();
65
+ const isUrl = q.startsWith("http://") || q.startsWith("https://");
66
+ if (isUrl) {
67
+ try {
68
+ const parsed = new URL(query);
69
+ const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
70
+ return allowedHosts.includes(parsed.hostname.toLowerCase());
71
+ } catch (e) {
72
+ return false;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+
78
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
79
+ return { tracks: [] };
80
+ }
81
+
82
+ async getStream(track: Track): Promise<StreamInfo> {
83
+ try {
84
+ const youtubeUrl = await getYoutubeStream(track.url);
85
+ if (!youtubeUrl) {
86
+ throw new Error("Failed to get YouTube stream URL");
87
+ }
88
+ const response = await fetch(youtubeUrl);
89
+ if (!response.ok || !response.body) {
90
+ throw new Error("Failed to fetch YouTube stream");
91
+ }
92
+ const stream = webStreamToNodeStream(response.body as ReadableStream) as unknown as Readable;
93
+
94
+ return {
95
+ stream,
96
+ type: "arbitrary",
97
+ metadata: track.metadata,
98
+ };
99
+ } catch (error) {
100
+ throw new Error(`Failed to get YouTube stream: ${error}`);
101
+ }
102
+ }
103
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "moduleResolution": "node",
16
+ "allowSyntheticDefaultImports": true,
17
+ "experimentalDecorators": true,
18
+ "emitDecoratorMetadata": true,
19
+ "resolveJsonModule": true
20
+ },
21
+ "include": ["src/*", "src/types/*"],
22
+ "exclude": ["node_modules", "dist", "examples"]
23
+ }