@ziplayer/plugin 0.1.41 → 0.1.50

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.
@@ -0,0 +1,523 @@
1
+ import { BasePlugin, Track, SearchResult, StreamInfo } from "ziplayer";
2
+ import { Readable } from "stream";
3
+ import axios from "axios";
4
+ import { parseBuffer } from "music-metadata";
5
+
6
+ /**
7
+ * Configuration options for the AttachmentsPlugin.
8
+ */
9
+ export interface AttachmentsPluginOptions {
10
+ /** Maximum file size in bytes (default: 25MB) */
11
+ maxFileSize?: number;
12
+ /** Allowed audio file extensions */
13
+ allowedExtensions?: string[];
14
+ /** Whether to enable debug logging */
15
+ debug?: boolean;
16
+ }
17
+
18
+ /**
19
+ * A plugin for handling Discord attachment URLs and local audio files.
20
+ *
21
+ * This plugin provides support for:
22
+ * - Discord attachment URLs (cdn.discordapp.com, media.discordapp.net)
23
+ * - Direct audio file URLs
24
+ * - Local file paths (if accessible)
25
+ * - Various audio formats (mp3, wav, ogg, m4a, flac, etc.)
26
+ * - File size validation
27
+ * - Audio metadata analysis (duration, title, artist, album, etc.)
28
+ * - Stream extraction from URLs
29
+ *
30
+ * @example
31
+ * const attachmentsPlugin = new AttachmentsPlugin({
32
+ * maxFileSize: 25 * 1024 * 1024, // 25MB
33
+ * allowedExtensions: ['mp3', 'wav', 'ogg', 'm4a', 'flac']
34
+ * });
35
+ *
36
+ * // Add to PlayerManager
37
+ * const manager = new PlayerManager({
38
+ * plugins: [attachmentsPlugin]
39
+ * });
40
+ *
41
+ * // Search for attachment content
42
+ * const result = await attachmentsPlugin.search(
43
+ * "https://cdn.discordapp.com/attachments/123/456/audio.mp3",
44
+ * "user123"
45
+ * );
46
+ * const stream = await attachmentsPlugin.getStream(result.tracks[0]);
47
+ *
48
+ * @since 1.0.0
49
+ */
50
+ export class AttachmentsPlugin extends BasePlugin {
51
+ name = "attachments";
52
+ version = "1.0.0";
53
+
54
+ private opts: AttachmentsPluginOptions;
55
+ private readonly defaultAllowedExtensions = ["mp3", "wav", "ogg", "m4a", "flac", "aac", "wma", "opus", "webm"];
56
+
57
+ /**
58
+ * Creates a new AttachmentsPlugin instance.
59
+ *
60
+ * @param opts - Configuration options for the attachments plugin
61
+ * @param opts.maxFileSize - Maximum file size in bytes (default: 25MB)
62
+ * @param opts.allowedExtensions - Allowed audio file extensions (default: common audio formats)
63
+ * @param opts.debug - Whether to enable debug logging (default: false)
64
+ *
65
+ * @example
66
+ * // Basic attachments plugin
67
+ * const attachmentsPlugin = new AttachmentsPlugin();
68
+ *
69
+ * // Custom configuration
70
+ * const customPlugin = new AttachmentsPlugin({
71
+ * maxFileSize: 50 * 1024 * 1024, // 50MB
72
+ * allowedExtensions: ['mp3', 'wav', 'ogg'],
73
+ * debug: true
74
+ * });
75
+ */
76
+ constructor(opts?: AttachmentsPluginOptions) {
77
+ super();
78
+ this.opts = {
79
+ maxFileSize: opts?.maxFileSize || 25 * 1024 * 1024, // 25MB default
80
+ allowedExtensions: opts?.allowedExtensions || this.defaultAllowedExtensions,
81
+ debug: opts?.debug || false,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Determines if this plugin can handle the given query.
87
+ *
88
+ * @param query - The URL or file path to check
89
+ * @returns `true` if the query is a Discord attachment URL or audio file URL, `false` otherwise
90
+ *
91
+ * @example
92
+ * plugin.canHandle("https://cdn.discordapp.com/attachments/123/456/audio.mp3"); // true
93
+ * plugin.canHandle("https://example.com/song.wav"); // true
94
+ * plugin.canHandle("youtube.com/watch?v=123"); // false
95
+ */
96
+ canHandle(query: string): boolean {
97
+ if (!query) return false;
98
+
99
+ const q = query.trim();
100
+
101
+ // Check if it's a URL
102
+ if (q.startsWith("http://") || q.startsWith("https://")) {
103
+ try {
104
+ const url = new URL(q);
105
+
106
+ // Discord attachment URLs
107
+ if (url.hostname === "cdn.discordapp.com" || url.hostname === "media.discordapp.net") {
108
+ return this.isAudioFile(q);
109
+ }
110
+
111
+ // Any other URL - check if it's an audio file
112
+ return this.isAudioFile(q);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ // Check if it's a local file path (basic check)
119
+ if (q.includes("/") || q.includes("\\")) {
120
+ return this.isAudioFile(q);
121
+ }
122
+
123
+ return false;
124
+ }
125
+
126
+ /**
127
+ * Validates if a URL is a valid Discord attachment URL or audio file URL.
128
+ *
129
+ * @param url - The URL to validate
130
+ * @returns `true` if the URL is valid and points to an audio file, `false` otherwise
131
+ *
132
+ * @example
133
+ * plugin.validate("https://cdn.discordapp.com/attachments/123/456/audio.mp3"); // true
134
+ * plugin.validate("https://example.com/song.wav"); // true
135
+ * plugin.validate("https://example.com/image.jpg"); // false
136
+ */
137
+ validate(url: string): boolean {
138
+ return this.canHandle(url) && this.isAudioFile(url);
139
+ }
140
+
141
+ /**
142
+ * Creates a track from an attachment URL or file path.
143
+ *
144
+ * This method handles both Discord attachment URLs and direct audio file URLs.
145
+ * It extracts metadata from the URL and creates a track that can be played.
146
+ *
147
+ * @param query - The attachment URL or file path
148
+ * @param requestedBy - The user ID who requested the track
149
+ * @returns A SearchResult containing a single track
150
+ *
151
+ * @example
152
+ * // Discord attachment
153
+ * const result = await plugin.search(
154
+ * "https://cdn.discordapp.com/attachments/123/456/audio.mp3",
155
+ * "user123"
156
+ * );
157
+ *
158
+ * // Direct audio file URL
159
+ * const result2 = await plugin.search(
160
+ * "https://example.com/song.wav",
161
+ * "user123"
162
+ * );
163
+ */
164
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
165
+ if (!this.canHandle(query)) {
166
+ return { tracks: [] };
167
+ }
168
+
169
+ try {
170
+ const filename = this.extractFilename(query);
171
+ const fileExtension = this.getFileExtension(filename);
172
+ let title = filename || `Audio File (${fileExtension})`;
173
+
174
+ // Get file size if it's a URL
175
+ let fileSize = 0;
176
+ let duration = 0;
177
+ let audioMetadata: any = {};
178
+
179
+ if (query.startsWith("http://") || query.startsWith("https://")) {
180
+ try {
181
+ const headResponse = await axios.head(query, { timeout: 5000 });
182
+ const contentLength = headResponse.headers["content-length"];
183
+ if (contentLength) {
184
+ fileSize = parseInt(contentLength, 10);
185
+
186
+ // Check file size limit
187
+ if (fileSize > this.opts.maxFileSize!) {
188
+ throw new Error(`File too large: ${this.formatBytes(fileSize)} (max: ${this.formatBytes(this.opts.maxFileSize!)})`);
189
+ }
190
+ }
191
+ } catch (error) {
192
+ this.debug("Could not get file size:", error);
193
+ }
194
+
195
+ // Analyze audio metadata to get duration and other info
196
+ try {
197
+ const analysisResult = await this.analyzeAudioMetadata(query);
198
+ duration = analysisResult.duration;
199
+ audioMetadata = analysisResult.metadata || {};
200
+
201
+ // Use metadata title if available
202
+ if (audioMetadata.title && audioMetadata.title.trim()) {
203
+ const artist = audioMetadata.artist ? ` - ${audioMetadata.artist}` : "";
204
+ const album = audioMetadata.album ? ` (${audioMetadata.album})` : "";
205
+ const finalTitle = `${audioMetadata.title}${artist}${album}`;
206
+ if (finalTitle.trim()) {
207
+ title = finalTitle;
208
+ }
209
+ }
210
+ } catch (error) {
211
+ this.debug("Could not analyze audio metadata:", error);
212
+ }
213
+ }
214
+
215
+ const track: Track = {
216
+ id: this.generateTrackId(query),
217
+ title,
218
+ url: query,
219
+ duration,
220
+ requestedBy,
221
+ source: this.name,
222
+ metadata: {
223
+ filename,
224
+ extension: fileExtension,
225
+ fileSize,
226
+ isDiscordAttachment: this.isDiscordAttachment(query),
227
+ ...audioMetadata, // Include all audio metadata
228
+ },
229
+ };
230
+
231
+ return { tracks: [track] };
232
+ } catch (error) {
233
+ this.debug("Error creating track:", error);
234
+ return { tracks: [] };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Retrieves the audio stream from an attachment URL or file path.
240
+ *
241
+ * This method downloads the audio file from the URL and returns it as a stream.
242
+ * It handles various audio formats and provides proper error handling.
243
+ *
244
+ * @param track - The Track object to get the stream for
245
+ * @returns A StreamInfo object containing the audio stream
246
+ * @throws {Error} If the URL is invalid, file is too large, or download fails
247
+ *
248
+ * @example
249
+ * const track = {
250
+ * id: "attachment-123",
251
+ * title: "audio.mp3",
252
+ * url: "https://cdn.discordapp.com/attachments/123/456/audio.mp3",
253
+ * ...
254
+ * };
255
+ * const streamInfo = await plugin.getStream(track);
256
+ * console.log(streamInfo.type); // "arbitrary"
257
+ * console.log(streamInfo.stream); // Readable stream
258
+ */
259
+ async getStream(track: Track): Promise<StreamInfo> {
260
+ if (track.source !== this.name) {
261
+ throw new Error("Track is not from AttachmentsPlugin");
262
+ }
263
+
264
+ const url = track.url;
265
+ if (!url) {
266
+ throw new Error("No URL provided for track");
267
+ }
268
+
269
+ try {
270
+ this.debug("Downloading audio from:", url);
271
+
272
+ // Download the file
273
+ const response = await axios.get(url, {
274
+ responseType: "stream",
275
+ timeout: 30000, // 30 second timeout
276
+ maxContentLength: this.opts.maxFileSize,
277
+ });
278
+
279
+ const stream = response.data as Readable;
280
+ const contentType = response.headers["content-type"] || "";
281
+ const contentLength = response.headers["content-length"];
282
+
283
+ this.debug("Download successful:", {
284
+ contentType,
285
+ contentLength: contentLength ? parseInt(contentLength, 10) : "unknown",
286
+ });
287
+
288
+ // Determine stream type based on content type or file extension
289
+ const streamType = this.getStreamType(contentType, track.metadata?.extension);
290
+
291
+ return {
292
+ stream,
293
+ type: streamType,
294
+ metadata: {
295
+ ...track.metadata,
296
+ contentType,
297
+ contentLength: contentLength ? parseInt(contentLength, 10) : undefined,
298
+ },
299
+ };
300
+ } catch (error: any) {
301
+ if (error.code === "ECONNABORTED") {
302
+ throw new Error(`Download timeout for ${url}`);
303
+ }
304
+ if (error.response?.status === 404) {
305
+ throw new Error(`File not found: ${url}`);
306
+ }
307
+ if (error.response?.status === 403) {
308
+ throw new Error(`Access denied: ${url}`);
309
+ }
310
+ if (error.message?.includes("maxContentLength")) {
311
+ throw new Error(`File too large: ${url}`);
312
+ }
313
+ throw new Error(`Failed to download audio: ${error.message || error}`);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Provides a fallback by attempting to re-download the file.
319
+ *
320
+ * @param track - The Track object to get a fallback stream for
321
+ * @returns A StreamInfo object containing the fallback audio stream
322
+ * @throws {Error} If fallback download fails
323
+ */
324
+ async getFallback(track: Track): Promise<StreamInfo> {
325
+ this.debug("Attempting fallback for track:", track.title);
326
+ return await this.getStream(track);
327
+ }
328
+
329
+ /**
330
+ * Checks if a file path or URL is an audio file based on extension.
331
+ */
332
+ private isAudioFile(path: string): boolean {
333
+ const extension = this.getFileExtension(path);
334
+ return this.opts.allowedExtensions!.includes(extension.toLowerCase());
335
+ }
336
+
337
+ /**
338
+ * Extracts the file extension from a path or URL.
339
+ */
340
+ private getFileExtension(path: string): string {
341
+ const lastDot = path.lastIndexOf(".");
342
+ if (lastDot === -1) return "";
343
+
344
+ const extension = path.slice(lastDot + 1);
345
+ // Remove query parameters if present
346
+ const questionMark = extension.indexOf("?");
347
+ return questionMark === -1 ? extension : extension.slice(0, questionMark);
348
+ }
349
+
350
+ /**
351
+ * Extracts filename from a URL or path.
352
+ */
353
+ private extractFilename(path: string): string {
354
+ try {
355
+ if (path.startsWith("http://") || path.startsWith("https://")) {
356
+ const url = new URL(path);
357
+ const pathname = url.pathname;
358
+ const lastSlash = pathname.lastIndexOf("/");
359
+ return lastSlash === -1 ? pathname : pathname.slice(lastSlash + 1);
360
+ }
361
+
362
+ // Local file path
363
+ const lastSlash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
364
+ return lastSlash === -1 ? path : path.slice(lastSlash + 1);
365
+ } catch {
366
+ return "Unknown File";
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Checks if a URL is a Discord attachment URL.
372
+ */
373
+ private isDiscordAttachment(url: string): boolean {
374
+ try {
375
+ const urlObj = new URL(url);
376
+ return urlObj.hostname === "cdn.discordapp.com" || urlObj.hostname === "media.discordapp.net";
377
+ } catch {
378
+ return false;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Generates a unique track ID for a given URL.
384
+ */
385
+ private generateTrackId(url: string): string {
386
+ // Create a hash-like ID from the URL
387
+ const hash = this.simpleHash(url);
388
+ return `attachment-${hash}-${Date.now()}`;
389
+ }
390
+
391
+ /**
392
+ * Simple hash function for generating IDs.
393
+ */
394
+ private simpleHash(str: string): string {
395
+ let hash = 0;
396
+ for (let i = 0; i < str.length; i++) {
397
+ const char = str.charCodeAt(i);
398
+ hash = (hash << 5) - hash + char;
399
+ hash = hash & hash; // Convert to 32-bit integer
400
+ }
401
+ return Math.abs(hash).toString(36);
402
+ }
403
+
404
+ /**
405
+ * Determines the appropriate stream type based on content type and file extension.
406
+ */
407
+ private getStreamType(contentType: string, extension?: string): StreamInfo["type"] {
408
+ const type = contentType.toLowerCase();
409
+ const ext = extension?.toLowerCase() || "";
410
+
411
+ // Check content type first
412
+ if (type.includes("webm") && type.includes("opus")) return "webm/opus";
413
+ if (type.includes("ogg") && type.includes("opus")) return "ogg/opus";
414
+
415
+ // Fallback to extension
416
+ if (ext === "webm") return "webm/opus";
417
+ if (ext === "ogg") return "ogg/opus";
418
+
419
+ // Default to arbitrary for all other types (mp3, wav, flac, mp4, etc.)
420
+ return "arbitrary";
421
+ }
422
+
423
+ /**
424
+ * Formats bytes into a human-readable string.
425
+ */
426
+ private formatBytes(bytes: number): string {
427
+ if (bytes === 0) return "0 Bytes";
428
+ const k = 1024;
429
+ const sizes = ["Bytes", "KB", "MB", "GB"];
430
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
431
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
432
+ }
433
+
434
+ /**
435
+ * Analyzes audio metadata to extract duration and other information.
436
+ *
437
+ * @param url - The URL to analyze
438
+ * @returns Promise containing duration in seconds and metadata
439
+ */
440
+ private async analyzeAudioMetadata(url: string): Promise<{ duration: number; metadata?: any }> {
441
+ try {
442
+ this.debug("Analyzing audio metadata for:", url);
443
+
444
+ // Download a small portion of the file to analyze metadata
445
+ const response = await axios.get(url, {
446
+ responseType: "arraybuffer",
447
+ timeout: 10000, // 10 second timeout for metadata analysis
448
+ maxContentLength: 1024 * 1024, // Only download first 1MB for metadata
449
+ headers: {
450
+ Range: "bytes=0-1048575", // Request first 1MB
451
+ },
452
+ });
453
+
454
+ const buffer = Buffer.from(response.data);
455
+ const metadata = await parseBuffer(buffer);
456
+
457
+ const duration = metadata.format.duration || 0;
458
+ this.debug("Audio metadata analysis result:", {
459
+ duration: `${duration}s`,
460
+ format: metadata.format.container,
461
+ codec: metadata.format.codec,
462
+ bitrate: metadata.format.bitrate,
463
+ });
464
+
465
+ return {
466
+ duration: Math.round(duration),
467
+ metadata: {
468
+ format: metadata.format.container,
469
+ codec: metadata.format.codec,
470
+ bitrate: metadata.format.bitrate,
471
+ sampleRate: metadata.format.sampleRate,
472
+ channels: metadata.format.numberOfChannels,
473
+ title: metadata.common.title,
474
+ artist: metadata.common.artist,
475
+ album: metadata.common.album,
476
+ year: metadata.common.year,
477
+ genre: metadata.common.genre,
478
+ },
479
+ };
480
+ } catch (error: any) {
481
+ this.debug("Failed to analyze audio metadata:", error.message);
482
+
483
+ // Fallback: try without Range header for some servers that don't support it
484
+ try {
485
+ const response = await axios.get(url, {
486
+ responseType: "arraybuffer",
487
+ timeout: 10000,
488
+ maxContentLength: 1024 * 1024, // 1MB limit
489
+ });
490
+
491
+ const buffer = Buffer.from(response.data);
492
+ const metadata = await parseBuffer(buffer);
493
+
494
+ const duration = metadata.format.duration || 0;
495
+ this.debug("Audio metadata analysis (fallback) result:", {
496
+ duration: `${duration}s`,
497
+ format: metadata.format.container,
498
+ });
499
+
500
+ return {
501
+ duration: Math.round(duration),
502
+ metadata: {
503
+ format: metadata.format.container,
504
+ codec: metadata.format.codec,
505
+ bitrate: metadata.format.bitrate,
506
+ },
507
+ };
508
+ } catch (fallbackError: any) {
509
+ this.debug("Fallback metadata analysis also failed:", fallbackError.message);
510
+ return { duration: 0 };
511
+ }
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Debug logging helper.
517
+ */
518
+ private debug(message: string, ...args: any[]): void {
519
+ if (this.opts.debug) {
520
+ console.log(`[AttachmentsPlugin] ${message}`, ...args);
521
+ }
522
+ }
523
+ }
package/src/TTSPlugin.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Readable } from "stream";
3
3
  import { getTTSUrls } from "@zibot/zitts";
4
4
  import axios from "axios";
5
+ import { parseBuffer } from "music-metadata";
5
6
 
6
7
  /**
7
8
  * Configuration options for the TTSPlugin.
@@ -58,6 +59,7 @@ interface TTSConfig {
58
59
  * - Multiple language support
59
60
  * - Configurable speech rate (normal/slow)
60
61
  * - TTS query parsing with language and speed options
62
+ * - Accurate duration analysis by generating sample audio
61
63
  *
62
64
  * @example
63
65
  * const ttsPlugin = new TTSPlugin({
@@ -174,21 +176,94 @@ export class TTSPlugin extends BasePlugin {
174
176
  const config: TTSConfig = { text, lang, slow };
175
177
  const url = this.encodeConfig(config);
176
178
  const title = `TTS (${lang}${slow ? ", slow" : ""}): ${text.slice(0, 64)}${text.length > 64 ? "…" : ""}`;
177
- const estimatedSeconds = Math.max(1, Math.min(60, Math.ceil(text.length / 12)));
179
+
180
+ // Analyze actual TTS duration
181
+ let duration: number;
182
+ try {
183
+ duration = await this.analyzeTTSDuration(text, lang, slow);
184
+ } catch (error) {
185
+ // Fallback to estimation if analysis fails
186
+ duration = this.estimateDuration(text);
187
+ }
178
188
 
179
189
  const track: Track = {
180
190
  id: `tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
181
191
  title,
182
192
  url,
183
- duration: estimatedSeconds,
193
+ duration,
184
194
  requestedBy,
185
195
  source: this.name,
186
- metadata: { tts: config },
196
+ metadata: {
197
+ tts: config,
198
+ analyzedDuration: true,
199
+ textLength: text.length,
200
+ language: lang,
201
+ slowMode: slow,
202
+ },
187
203
  };
188
204
 
189
205
  return { tracks: [track] };
190
206
  }
191
207
 
208
+ /**
209
+ * Analyzes TTS audio duration by generating a small sample and measuring its length.
210
+ *
211
+ * @param text - The text to analyze
212
+ * @param lang - The language code
213
+ * @param slow - Whether to use slow speech rate
214
+ * @returns Promise containing duration in seconds
215
+ */
216
+ private async analyzeTTSDuration(text: string, lang: string, slow: boolean): Promise<number> {
217
+ try {
218
+ // Use a shorter sample text for duration analysis to minimize processing time
219
+ const sampleText = text.length > 50 ? text.slice(0, 50) + "..." : text;
220
+
221
+ const urls = getTTSUrls(sampleText, { lang, slow });
222
+ if (!urls || urls.length === 0) {
223
+ return this.estimateDuration(text);
224
+ }
225
+
226
+ // Download the sample audio
227
+ const parts = await Promise.all(
228
+ urls.map((u) => axios.get<ArrayBuffer>(u, { responseType: "arraybuffer" }).then((r) => Buffer.from(r.data))),
229
+ );
230
+
231
+ const merged = Buffer.concat(parts);
232
+
233
+ // Parse metadata to get actual duration
234
+ const metadata = await parseBuffer(merged);
235
+ const actualDuration = metadata.format.duration || 0;
236
+
237
+ // Calculate ratio and apply to full text
238
+ const sampleRatio = sampleText.length / text.length;
239
+ const estimatedDuration = actualDuration / sampleRatio;
240
+
241
+ return Math.round(estimatedDuration);
242
+ } catch (error) {
243
+ // Fallback to text-based estimation
244
+ return this.estimateDuration(text);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Estimates TTS duration based on text length and language.
250
+ *
251
+ * @param text - The text to estimate duration for
252
+ * @returns Estimated duration in seconds
253
+ */
254
+ private estimateDuration(text: string): number {
255
+ // Base estimation: ~12 characters per second for normal speech
256
+ // Adjust based on text complexity and language
257
+ const baseRate = 12; // characters per second
258
+ const wordCount = text.split(/\s+/).length;
259
+ const charCount = text.length;
260
+
261
+ // Use word count for better estimation
262
+ const estimatedSeconds = Math.max(1, Math.min(300, Math.ceil(wordCount / 2.5)));
263
+
264
+ return estimatedSeconds;
265
+ }
266
+
192
267
  /**
193
268
  * Generates an audio stream for a TTS track.
194
269
  *
@@ -243,10 +318,7 @@ export class TTSPlugin extends BasePlugin {
243
318
  const urlStr = o.url.toString();
244
319
  try {
245
320
  type =
246
- normType(o.type) ||
247
- (urlStr.endsWith(".webm") ? "webm/opus"
248
- : urlStr.endsWith(".ogg") ? "ogg/opus"
249
- : undefined);
321
+ normType(o.type) || (urlStr.endsWith(".webm") ? "webm/opus" : urlStr.endsWith(".ogg") ? "ogg/opus" : undefined);
250
322
  const res = await axios.get(urlStr, { responseType: "stream" });
251
323
  stream = res.data as unknown as Readable;
252
324
  metadata = o.metadata;
package/src/index.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  * const youtubePlugin = new YouTubePlugin();
34
34
  * const result = await youtubePlugin.search("Never Gonna Give You Up", "user123");
35
35
  */
36
- export { YouTubePlugin } from "./YouTubePlugin";
36
+ export * from "./YouTubePlugin";
37
37
 
38
38
  /**
39
39
  * SoundCloud plugin for handling SoundCloud tracks, playlists, and search.
@@ -49,7 +49,7 @@ export { YouTubePlugin } from "./YouTubePlugin";
49
49
  * const soundcloudPlugin = new SoundCloudPlugin();
50
50
  * const result = await soundcloudPlugin.search("chill music", "user123");
51
51
  */
52
- export { SoundCloudPlugin } from "./SoundCloudPlugin";
52
+ export * from "./SoundCloudPlugin";
53
53
 
54
54
  /**
55
55
  * Spotify plugin for metadata extraction and display purposes.
@@ -67,7 +67,7 @@ export { SoundCloudPlugin } from "./SoundCloudPlugin";
67
67
  * const spotifyPlugin = new SpotifyPlugin();
68
68
  * const result = await spotifyPlugin.search("spotify:track:4iV5W9uYEdYUVa79Axb7Rh", "user123");
69
69
  */
70
- export { SpotifyPlugin } from "./SpotifyPlugin";
70
+ export * from "./SpotifyPlugin";
71
71
 
72
72
  /**
73
73
  * Text-to-Speech (TTS) plugin for converting text to audio.
@@ -83,7 +83,7 @@ export { SpotifyPlugin } from "./SpotifyPlugin";
83
83
  * const ttsPlugin = new TTSPlugin({ defaultLang: "en" });
84
84
  * const result = await ttsPlugin.search("tts:Hello world", "user123");
85
85
  */
86
- export { TTSPlugin } from "./TTSPlugin";
86
+ export * from "./TTSPlugin";
87
87
 
88
88
  /**
89
89
  * YTSR plugin for advanced YouTube search without streaming.
@@ -100,4 +100,27 @@ export { TTSPlugin } from "./TTSPlugin";
100
100
  * const result = await ytsrPlugin.search("Never Gonna Give You Up", "user123");
101
101
  * const playlistResult = await ytsrPlugin.searchPlaylist("chill music", "user123");
102
102
  */
103
- export { YTSRPlugin } from "./YTSRPlugin";
103
+ export * from "./YTSRPlugin";
104
+
105
+ /**
106
+ * Attachments plugin for handling Discord attachment URLs and audio files.
107
+ *
108
+ * Provides comprehensive support for attachment content including:
109
+ * - Discord attachment URLs (cdn.discordapp.com, media.discordapp.net)
110
+ * - Direct audio file URLs
111
+ * - Local file paths (if accessible)
112
+ * - Various audio formats (mp3, wav, ogg, m4a, flac, etc.)
113
+ * - File size validation and stream extraction
114
+ * - Proper error handling and fallback support
115
+ *
116
+ * @example
117
+ * const attachmentsPlugin = new AttachmentsPlugin({
118
+ * maxFileSize: 25 * 1024 * 1024, // 25MB
119
+ * allowedExtensions: ['mp3', 'wav', 'ogg', 'm4a', 'flac']
120
+ * });
121
+ * const result = await attachmentsPlugin.search(
122
+ * "https://cdn.discordapp.com/attachments/123/456/audio.mp3",
123
+ * "user123"
124
+ * );
125
+ */
126
+ export * from "./AttachmentsPlugin";