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