@ziplayer/plugin 0.1.51 → 0.2.1-dev-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/src/index.ts CHANGED
@@ -85,23 +85,6 @@ export * from "./SpotifyPlugin";
85
85
  */
86
86
  export * from "./TTSPlugin";
87
87
 
88
- /**
89
- * YTSR plugin for advanced YouTube search without streaming.
90
- *
91
- * Provides comprehensive YouTube search functionality including:
92
- * - Advanced video search with filters (duration, upload date, sort by)
93
- * - Playlist and channel search
94
- * - Multiple search types (video, playlist, channel, all)
95
- * - Metadata extraction without streaming
96
- * - Support for YouTube URLs
97
- *
98
- * @example
99
- * const ytsrPlugin = new YTSRPlugin();
100
- * const result = await ytsrPlugin.search("Never Gonna Give You Up", "user123");
101
- * const playlistResult = await ytsrPlugin.searchPlaylist("chill music", "user123");
102
- */
103
- export * from "./YTSRPlugin";
104
-
105
88
  /**
106
89
  * Attachments plugin for handling Discord attachment URLs and audio files.
107
90
  *
@@ -1,96 +1,282 @@
1
- import { createWriteStream } from "fs";
2
- import { join } from "path";
3
- import { tmpdir } from "os";
4
-
5
- // Import type declarations
6
- import type { SabrPlaybackOptions, StreamResult } from "../types/googlevideo";
7
-
8
- // Re-export types for external use
9
- export type { StreamResult, SabrPlaybackOptions };
10
-
11
- export interface OutputStream {
12
- stream: NodeJS.WritableStream;
13
- filePath: string;
14
- }
15
-
16
- /**
17
- * Creates a sabr stream for YouTube video download
18
- */
19
- export async function createSabrStream(videoId: string, options: SabrPlaybackOptions): Promise<{ streamResults: StreamResult }> {
20
- try {
21
- // Dynamic import to avoid build-time errors
22
- const sabrModule = require("googlevideo/sabr-stream") as any;
23
- const createSabrStreamImpl = sabrModule.createSabrStream;
24
-
25
- const streamResults = await createSabrStreamImpl(videoId, options);
26
-
27
- return { streamResults };
28
- } catch (error) {
29
- // Fallback implementation if sabr download is not available
30
- throw new Error(`Sabr download not available: ${error}`);
31
- }
32
- }
33
-
34
- /**
35
- * Creates an output stream for writing downloaded content
36
- */
37
- export function createOutputStream(videoTitle: string, mimeType: string): OutputStream {
38
- const sanitizedTitle = videoTitle.replace(/[<>:"/\\|?*]/g, "_").substring(0, 100);
39
- const extension = getExtensionFromMimeType(mimeType);
40
- const fileName = `${sanitizedTitle}.${extension}`;
41
- const filePath = join(tmpdir(), fileName);
42
-
43
- const stream = createWriteStream(filePath);
44
-
45
- return {
46
- stream,
47
- filePath,
48
- };
49
- }
50
-
51
- /**
52
- * Creates a stream sink for piping data with progress tracking
53
- */
54
- export function createStreamSink(format: any, outputStream: NodeJS.WritableStream, progressBar: any) {
55
- return new WritableStream({
56
- start() {
57
- // Initialize progress tracking
58
- },
59
- write(chunk) {
60
- outputStream.write(chunk);
61
- if (progressBar) {
62
- progressBar.increment(chunk.length);
63
- }
64
- },
65
- close() {
66
- outputStream.end();
67
- },
68
- });
69
- }
70
-
71
- /**
72
- * Gets file extension from MIME type
73
- */
74
- function getExtensionFromMimeType(mimeType: string): string {
75
- const mimeMap: { [key: string]: string } = {
76
- "audio/mp4": "m4a",
77
- "audio/webm": "webm",
78
- "audio/ogg": "ogg",
79
- "video/mp4": "mp4",
80
- "video/webm": "webm",
81
- "video/ogg": "ogv",
82
- };
83
-
84
- return mimeMap[mimeType] || "bin";
85
- }
86
-
87
- /**
88
- * Default sabr playback options
89
- */
90
- export const DEFAULT_SABR_OPTIONS: SabrPlaybackOptions = {
91
- preferWebM: true,
92
- preferOpus: true,
93
- videoQuality: "720p",
94
- audioQuality: "AUDIO_QUALITY_MEDIUM",
95
- enabledTrackTypes: "VIDEO_AND_AUDIO",
96
- };
1
+ import { createWriteStream } from "fs";
2
+ import { join } from "path";
3
+ import { tmpdir } from "os";
4
+ import { Readable } from "stream";
5
+ import { Constants, YTNodes, Platform } from "youtubei.js";
6
+ import type Innertube from "youtubei.js";
7
+
8
+ import { SabrStream } from "googlevideo/sabr-stream";
9
+ import { buildSabrFormat } from "googlevideo/utils";
10
+
11
+ import { BG } from "bgutils-js";
12
+ import { JSDOM } from "jsdom";
13
+ import { webStreamToNodeStream } from "./stream-converter";
14
+
15
+ export interface OutputStream {
16
+ stream: NodeJS.WritableStream;
17
+ filePath: string;
18
+ }
19
+
20
+ export interface SabrAudioResult {
21
+ title: string;
22
+ stream: Readable;
23
+ format: {
24
+ mimeType: string;
25
+ itag: number;
26
+ contentLength: number;
27
+ };
28
+ }
29
+
30
+ export interface SabrPlaybackOptions {
31
+ preferWebM?: boolean;
32
+ preferOpus?: boolean;
33
+ videoQuality?: string;
34
+ audioQuality?: string;
35
+ enabledTrackTypes?: any;
36
+ }
37
+ /**
38
+ * Generates a web PoToken for YouTube authentication
39
+ * This is required for accessing restricted video content
40
+ */
41
+ async function generateWebPoToken(contentBinding: string): Promise<{
42
+ visitorData: string;
43
+ placeholderPoToken: string;
44
+ poToken: string;
45
+ }> {
46
+ try {
47
+ const requestKey = "O43z0dpjhgX20SCx4KAo";
48
+
49
+ if (!contentBinding) throw new Error("Could not get visitor data");
50
+
51
+ const dom = new JSDOM();
52
+
53
+ Object.assign(globalThis, {
54
+ window: dom.window,
55
+ document: dom.window.document,
56
+ });
57
+
58
+ const bgConfig = {
59
+ fetch: (input: any, init: any) => fetch(input, init),
60
+ globalObj: globalThis,
61
+ identifier: contentBinding,
62
+ requestKey,
63
+ };
64
+
65
+ const bgChallenge = await BG.Challenge.create(bgConfig);
66
+
67
+ if (!bgChallenge) throw new Error("Could not get challenge");
68
+
69
+ const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
70
+
71
+ if (interpreterJavascript) {
72
+ new Function(interpreterJavascript)();
73
+ } else throw new Error("Could not load VM");
74
+
75
+ const poTokenResult = await BG.PoToken.generate({
76
+ program: bgChallenge.program,
77
+ globalName: bgChallenge.globalName,
78
+ bgConfig,
79
+ });
80
+
81
+ const placeholderPoToken = BG.PoToken.generatePlaceholder(contentBinding);
82
+
83
+ return {
84
+ visitorData: contentBinding,
85
+ placeholderPoToken,
86
+ poToken: poTokenResult.poToken,
87
+ };
88
+ } catch (error) {
89
+ console.warn("PoToken generation failed, continuing without it:", error);
90
+ return {
91
+ visitorData: contentBinding,
92
+ placeholderPoToken: "",
93
+ poToken: "",
94
+ };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Makes a proper player request to YouTube API
100
+ */
101
+ async function makePlayerRequest(innertube: Innertube, videoId: string, reloadPlaybackContext?: any): Promise<any> {
102
+ const watchEndpoint = new YTNodes.NavigationEndpoint({
103
+ watchEndpoint: { videoId },
104
+ });
105
+
106
+ const extraArgs: any = {
107
+ playbackContext: {
108
+ adPlaybackContext: { pyv: true },
109
+ contentPlaybackContext: {
110
+ vis: 0,
111
+ splay: false,
112
+ lactMilliseconds: "-1",
113
+ signatureTimestamp: innertube.session.player?.signature_timestamp,
114
+ },
115
+ },
116
+ contentCheckOk: true,
117
+ racyCheckOk: true,
118
+ };
119
+
120
+ if (reloadPlaybackContext) {
121
+ extraArgs.playbackContext.reloadPlaybackContext = reloadPlaybackContext;
122
+ }
123
+
124
+ return watchEndpoint.call(innertube.actions, {
125
+ ...extraArgs,
126
+ parse: true,
127
+ });
128
+ }
129
+
130
+ /**
131
+ * YouTube VM shim
132
+ * This allows the SABR stream to execute YouTube's custom JavaScript for deciphering signatures and generating tokens
133
+ */
134
+ Platform.shim.eval = async (data, env) => {
135
+ const properties = [];
136
+
137
+ if (env.n) properties.push(`n: exportedVars.nFunction("${env.n}")`);
138
+ if (env.sig) properties.push(`sig: exportedVars.sigFunction("${env.sig}")`);
139
+
140
+ const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
141
+ return new Function(code)();
142
+ };
143
+
144
+ /**
145
+ * Creates a SABR audio stream for YouTube video download
146
+ * This provides better quality and more reliable streaming than standard methods
147
+ */
148
+ export async function createSabrStream(
149
+ videoId: string,
150
+ innertube: Innertube,
151
+ options?: SabrPlaybackOptions,
152
+ ): Promise<SabrAudioResult> {
153
+ try {
154
+ // Generate PoToken for authentication
155
+ const webPo = await generateWebPoToken(videoId);
156
+
157
+ // Make initial player request
158
+ const player = await makePlayerRequest(innertube, videoId);
159
+
160
+ const title = player.video_details?.title || "unknown";
161
+
162
+ const serverAbrStreamingUrl = await innertube.session.player?.decipher(player.streaming_data?.server_abr_streaming_url);
163
+
164
+ const ustreamerConfig =
165
+ player.player_config?.media_common_config.media_ustreamer_request_config?.video_playback_ustreamer_config;
166
+
167
+ if (!serverAbrStreamingUrl || !ustreamerConfig) {
168
+ throw new Error("Missing SABR streaming config");
169
+ }
170
+
171
+ const sabrFormats = player.streaming_data?.adaptive_formats.map((f: any) => buildSabrFormat(f)) || [];
172
+
173
+ const sabr = new SabrStream({
174
+ formats: sabrFormats,
175
+ serverAbrStreamingUrl,
176
+ videoPlaybackUstreamerConfig: ustreamerConfig,
177
+ poToken: webPo.poToken,
178
+ clientInfo: {
179
+ clientName: parseInt(
180
+ Constants.CLIENT_NAME_IDS[innertube.session.context.client.clientName as keyof typeof Constants.CLIENT_NAME_IDS],
181
+ ),
182
+ clientVersion: innertube.session.context.client.clientVersion,
183
+ },
184
+ });
185
+
186
+ // Handle player response reload events
187
+ sabr.on("reloadPlayerResponse", async (ctx: any) => {
188
+ try {
189
+ const pr = await makePlayerRequest(innertube, videoId, ctx);
190
+
191
+ const url = await innertube.session.player?.decipher(pr.streaming_data?.server_abr_streaming_url);
192
+
193
+ const config = pr.player_config?.media_common_config.media_ustreamer_request_config?.video_playback_ustreamer_config;
194
+
195
+ if (url && config) {
196
+ sabr.setStreamingURL(url);
197
+ sabr.setUstreamerConfig(config);
198
+ }
199
+ } catch (error) {
200
+ console.error("Failed to reload player response:", error);
201
+ }
202
+ });
203
+
204
+ // Start the stream with audio preference
205
+ const { audioStream, selectedFormats } = await sabr.start({
206
+ // audioQuality: options?.audioQuality || "high",
207
+ });
208
+
209
+ // Convert Web Stream to Node.js Readable stream
210
+ const nodeStream = webStreamToNodeStream(audioStream);
211
+
212
+ return {
213
+ title,
214
+ stream: nodeStream,
215
+ format: {
216
+ mimeType: selectedFormats.audioFormat.mimeType || "audio/webm",
217
+ itag: selectedFormats.audioFormat.itag || 0,
218
+ contentLength: selectedFormats.audioFormat.contentLength || 0,
219
+ },
220
+ };
221
+ } catch (error) {
222
+ throw new Error(`SABR stream creation failed: ${error instanceof Error ? error.message : String(error)}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Creates an output stream for writing downloaded content
228
+ */
229
+ export function createOutputStream(videoTitle: string, mimeType: string): OutputStream {
230
+ const sanitizedTitle = videoTitle.replace(/[<>:"/\\|?*]/g, "_").substring(0, 100);
231
+ const extension = getExtensionFromMimeType(mimeType);
232
+ const fileName = `${sanitizedTitle}.${extension}`;
233
+ const filePath = join(tmpdir(), fileName);
234
+
235
+ const stream = createWriteStream(filePath);
236
+
237
+ return {
238
+ stream,
239
+ filePath,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Sanitizes a filename by removing invalid characters
245
+ */
246
+ export function sanitizeFileName(name: string): string {
247
+ return name.replace(/[^\w\d]+/g, "_").slice(0, 128);
248
+ }
249
+
250
+ /**
251
+ * Converts bytes to megabytes
252
+ */
253
+ export function bytesToMB(bytes: number): string {
254
+ return (bytes / 1024 / 1024).toFixed(2);
255
+ }
256
+
257
+ /**
258
+ * Gets file extension from MIME type
259
+ */
260
+ function getExtensionFromMimeType(mimeType: string): string {
261
+ const mimeMap: { [key: string]: string } = {
262
+ "audio/mp4": "m4a",
263
+ "audio/webm": "webm",
264
+ "audio/ogg": "ogg",
265
+ "video/mp4": "mp4",
266
+ "video/webm": "webm",
267
+ "video/ogg": "ogv",
268
+ };
269
+
270
+ return mimeMap[mimeType] || "bin";
271
+ }
272
+
273
+ /**
274
+ * Default sabr playback options
275
+ */
276
+ export const DEFAULT_SABR_OPTIONS: SabrPlaybackOptions = {
277
+ preferWebM: true,
278
+ preferOpus: true,
279
+ videoQuality: "720p",
280
+ audioQuality: "high",
281
+ enabledTrackTypes: "VIDEO_AND_AUDIO",
282
+ };
@@ -2,78 +2,109 @@ import { Readable } from "stream";
2
2
 
3
3
  /**
4
4
  * Converts a Web ReadableStream to a Node.js Readable stream
5
+ * with proper cleanup handling to prevent "Controller is already closed" errors
5
6
  */
6
7
  export function webStreamToNodeStream(webStream: ReadableStream): Readable {
8
+ let reader: ReadableStreamDefaultReader | null = null;
9
+ let pumpActive = true;
10
+ let abortController: AbortController | null = null;
11
+
7
12
  const nodeStream = new Readable({
8
13
  read() {
9
14
  // This will be handled by the Web Stream reader
10
15
  },
11
- });
12
-
13
- // Create a reader from the Web Stream
14
- const reader = webStream.getReader();
16
+ destroy(error: Error | null, callback: (error?: Error | null) => void) {
17
+ // Gracefully stop the pump when stream is destroyed
18
+ pumpActive = false;
15
19
 
16
- // Read chunks and push to Node.js stream
17
- const pump = async () => {
18
- try {
19
- while (true) {
20
- const { done, value } = await reader.read();
21
- if (done) {
22
- nodeStream.push(null); // End the stream
23
- break;
24
- }
25
- nodeStream.push(Buffer.from(value));
20
+ // Cancel reader if active
21
+ if (reader) {
22
+ try {
23
+ // reader.cancel() will abort the fetch
24
+ reader.cancel().catch(() => {
25
+ // Ignore cancel errors
26
+ });
27
+ reader = null;
28
+ } catch {}
26
29
  }
27
- } catch (error) {
28
- nodeStream.destroy(error as Error);
29
- }
30
- };
31
-
32
- // Start pumping data
33
- pump();
34
30
 
35
- return nodeStream;
36
- }
31
+ // Abort any pending operations
32
+ if (abortController) {
33
+ try {
34
+ abortController.abort();
35
+ } catch {}
36
+ abortController = null;
37
+ }
37
38
 
38
- /**
39
- * Converts a Web ReadableStream to a Node.js Readable stream with progress tracking
40
- */
41
- export function webStreamToNodeStreamWithProgress(
42
- webStream: ReadableStream,
43
- progressCallback?: (bytesRead: number) => void,
44
- ): Readable {
45
- const nodeStream = new Readable({
46
- read() {
47
- // This will be handled by the Web Stream reader
39
+ callback(error);
48
40
  },
49
41
  });
50
42
 
51
- let bytesRead = 0;
52
- const reader = webStream.getReader();
43
+ // Create abort controller for graceful shutdown
44
+ abortController = new AbortController();
45
+
46
+ // Create a reader from the Web Stream
47
+ reader = webStream.getReader();
53
48
 
49
+ // Read chunks and push to Node.js stream
54
50
  const pump = async () => {
55
51
  try {
56
- while (true) {
52
+ while (pumpActive && reader) {
57
53
  const { done, value } = await reader.read();
54
+
55
+ // Check if pump was stopped during read
56
+ if (!pumpActive || !reader) {
57
+ break;
58
+ }
59
+
58
60
  if (done) {
59
61
  nodeStream.push(null); // End the stream
60
62
  break;
61
63
  }
62
64
 
63
- const buffer = Buffer.from(value);
64
- nodeStream.push(buffer);
65
-
66
- bytesRead += buffer.length;
67
- if (progressCallback) {
68
- progressCallback(bytesRead);
65
+ if (value && pumpActive) {
66
+ nodeStream.push(Buffer.from(value));
69
67
  }
70
68
  }
71
69
  } catch (error) {
72
- nodeStream.destroy(error as Error);
70
+ // Only destroy if pump is still active and stream exists
71
+ if (pumpActive) {
72
+ const errorMsg = error instanceof Error ? error.message : String(error);
73
+
74
+ // Ignore "Controller is already closed" and stream cancelled errors
75
+ if (
76
+ errorMsg.includes("Controller is already closed") ||
77
+ errorMsg.includes("already been cancelled") ||
78
+ errorMsg.includes("stream closed") ||
79
+ errorMsg.includes("aborted")
80
+ ) {
81
+ // Stream was destroyed externally, just end cleanly
82
+ nodeStream.push(null);
83
+ } else {
84
+ // Real error, report it
85
+ nodeStream.destroy(error as Error);
86
+ }
87
+ }
88
+ } finally {
89
+ // Cleanup reader when pump ends
90
+ try {
91
+ if (reader) {
92
+ await reader.cancel();
93
+ reader = null;
94
+ }
95
+ } catch {}
96
+
97
+ pumpActive = false;
73
98
  }
74
99
  };
75
100
 
76
- pump();
101
+ // Start pumping data
102
+ pump().catch((error) => {
103
+ // Catch any unhandled promise rejection
104
+ if (pumpActive) {
105
+ console.error("[stream-converter] Pump error:", error);
106
+ }
107
+ });
77
108
 
78
109
  return nodeStream;
79
110
  }
package/tsconfig.json CHANGED
@@ -16,8 +16,12 @@
16
16
  "allowSyntheticDefaultImports": true,
17
17
  "experimentalDecorators": true,
18
18
  "emitDecoratorMetadata": true,
19
- "resolveJsonModule": true
19
+ "resolveJsonModule": true,
20
+ "paths": {
21
+ "googlevideo/sabr-stream": ["./node_modules/googlevideo/dist/src/exports/sabr-stream"],
22
+ "googlevideo/utils": ["./node_modules/googlevideo/dist/src/exports/utils"]
23
+ }
20
24
  },
21
- "include": ["src/*", "src/types/*"],
25
+ "include": ["src/*"],
22
26
  "exclude": ["node_modules", "dist", "examples"]
23
27
  }
@@ -1,8 +0,0 @@
1
- interface MultiBar {
2
- create(total: number, startValue: number, options: any): any;
3
- stop(): void;
4
- }
5
- export declare function createMultiProgressBar(): MultiBar;
6
- export declare function setupProgressBar(multiBar: MultiBar, filename: string, total: number): any;
7
- export {};
8
- //# sourceMappingURL=progress-bar.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"progress-bar.d.ts","sourceRoot":"","sources":["../../src/utils/progress-bar.ts"],"names":[],"mappings":"AACA,UAAU,QAAQ;IACjB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,GAAG,CAAC;IAC7D,IAAI,IAAI,IAAI,CAAC;CACb;AAOD,wBAAgB,sBAAsB,IAAI,QAAQ,CAcjD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAEnF"}
@@ -1,24 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createMultiProgressBar = createMultiProgressBar;
4
- exports.setupProgressBar = setupProgressBar;
5
- // Mock implementation for now - will be replaced with actual cli-progress when available
6
- function createMultiProgressBar() {
7
- return {
8
- create(total, startValue, options) {
9
- return {
10
- increment(value) {
11
- // Mock progress bar implementation
12
- console.log(`Progress: ${value}/${total}`);
13
- },
14
- };
15
- },
16
- stop() {
17
- console.log("Progress completed");
18
- },
19
- };
20
- }
21
- function setupProgressBar(multiBar, filename, total) {
22
- return multiBar.create(total, 0, { filename });
23
- }
24
- //# sourceMappingURL=progress-bar.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"progress-bar.js","sourceRoot":"","sources":["../../src/utils/progress-bar.ts"],"names":[],"mappings":";;AAWA,wDAcC;AAED,4CAEC;AAnBD,yFAAyF;AACzF,SAAgB,sBAAsB;IACrC,OAAO;QACN,MAAM,CAAC,KAAa,EAAE,UAAkB,EAAE,OAAY;YACrD,OAAO;gBACN,SAAS,CAAC,KAAa;oBACtB,mCAAmC;oBACnC,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,IAAI,KAAK,EAAE,CAAC,CAAC;gBAC5C,CAAC;aACD,CAAC;QACH,CAAC;QACD,IAAI;YACH,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QACnC,CAAC;KACD,CAAC;AACH,CAAC;AAED,SAAgB,gBAAgB,CAAC,QAAkB,EAAE,QAAgB,EAAE,KAAa;IACnF,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;AAChD,CAAC"}