shan-server 1.0.4 → 1.0.6

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.js +1509 -307
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -1,24 +1,130 @@
1
+ #!/usr/bin/env node
2
+
1
3
  const axios = require('axios');
2
4
  const readline = require('readline');
3
5
  const os = require('os');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+
10
+ const USER_CONFIG_PATH = path.join(os.homedir(), '.shan-server', 'config.json');
11
+
12
+ function loadPersistedConfig() {
13
+ try {
14
+ if (fs.existsSync(USER_CONFIG_PATH)) {
15
+ return JSON.parse(fs.readFileSync(USER_CONFIG_PATH, 'utf8'));
16
+ }
17
+ } catch (e) {}
18
+ return {};
19
+ }
20
+
21
+ function savePersistedConfig(partial) {
22
+ try {
23
+ const dir = path.dirname(USER_CONFIG_PATH);
24
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
25
+ const current = loadPersistedConfig();
26
+ const merged = { ...current, ...partial };
27
+ fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(merged, null, 2));
28
+ return merged;
29
+ } catch (e) {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ const persisted = loadPersistedConfig();
35
+
36
+ const CONFIG = {
37
+ apiBase: process.env.SHAN_API_BASE || persisted.apiBase || 'https://shans-api-07-p00o.onrender.com' || 'https://sh-ans-api-07.vercel.app/',
38
+ downloadDir: process.env.SHAN_DOWNLOAD_DIR || persisted.downloadDir || path.join(process.cwd(), 'downloads'),
39
+ requestTimeoutMs: parseInt(process.env.SHAN_TIMEOUT_MS || persisted.requestTimeoutMs || '60000', 10),
40
+ batchConcurrency: parseInt(process.env.SHAN_CONCURRENCY || persisted.batchConcurrency || '3', 10),
41
+ maxRetries: parseInt(process.env.SHAN_RETRIES || persisted.maxRetries || '2', 10),
42
+ retryBaseDelayMs: parseInt(process.env.SHAN_RETRY_DELAY_MS || persisted.retryBaseDelayMs || '400', 10),
43
+ defaultAuthor: persisted.defaultAuthor || null,
44
+ defaultUid: persisted.defaultUid || null,
45
+ defaultFont: persisted.defaultFont || null
46
+ };
47
+
48
+ // --------------------------------------------------------------
49
+ // LIGHTWEIGHT FILE LOGGER
50
+ // --------------------------------------------------------------
51
+ function logEvent(level, message) {
52
+ try {
53
+ const dir = downloadManagerLogDir();
54
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
55
+ const line = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}\n`;
56
+ fs.appendFileSync(path.join(dir, 'activity.log'), line);
57
+ } catch (e) {
58
+ // Logging must never crash the app
59
+ }
60
+ }
61
+ function downloadManagerLogDir() {
62
+ return path.join(CONFIG.downloadDir, 'logs');
63
+ }
64
+
65
+ // --------------------------------------------------------------
66
+ // RETRY HELPER - exponential backoff for flaky network calls
67
+ // --------------------------------------------------------------
68
+ async function withRetry(fn, { retries = CONFIG.maxRetries, baseDelayMs = CONFIG.retryBaseDelayMs, label = 'request' } = {}) {
69
+ let attempt = 0;
70
+ let lastError;
71
+ while (attempt <= retries) {
72
+ try {
73
+ return await fn();
74
+ } catch (err) {
75
+ lastError = err;
76
+ const status = err?.response?.status;
77
+ // Don't retry on client errors (4xx) - those won't fix themselves
78
+ if (status && status >= 400 && status < 500) throw err;
79
+ attempt++;
80
+ if (attempt > retries) break;
81
+ const delay = baseDelayMs * Math.pow(2, attempt - 1);
82
+ logEvent('warn', `${label} failed (attempt ${attempt}/${retries}): ${err.message}. Retrying in ${delay}ms`);
83
+ await new Promise(resolve => setTimeout(resolve, delay));
84
+ }
85
+ }
86
+ throw lastError;
87
+ }
88
+
89
+ // --------------------------------------------------------------
90
+ // CONCURRENCY-LIMITED ASYNC POOL - used for parallel batch downloads
91
+ // --------------------------------------------------------------
92
+ async function asyncPool(limit, items, iteratorFn) {
93
+ const results = new Array(items.length);
94
+ let cursor = 0;
95
+
96
+ async function worker() {
97
+ while (cursor < items.length) {
98
+ const current = cursor++;
99
+ results[current] = await iteratorFn(items[current], current);
100
+ }
101
+ }
102
+
103
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
104
+ await Promise.all(workers);
105
+ return results;
106
+ }
4
107
 
108
+ // --------------------------------------------------------------
109
+ // AUTO-AUTHOR DETECTION (now actually returns the detected user)
110
+ // --------------------------------------------------------------
5
111
  function getDefaultAuthor() {
6
112
  try {
7
113
  const username = os.userInfo().username;
8
114
  if (username && username !== 'root' && username !== 'admin') {
9
- return username;
115
+ return '♡︎ 𝗦𝗵𝗔𝗻 ♡︎';
10
116
  }
11
- } catch (e) {
12
- }
117
+ } catch (e) {}
13
118
 
14
119
  const envUser = process.env.USER || process.env.USERNAME || process.env.LOGNAME;
15
120
  if (envUser && envUser !== 'root' && envUser !== 'admin') {
16
- return envUser;
121
+ return '♡︎ 𝗦𝗵𝗔𝗻 ♡︎';
17
122
  }
18
123
 
19
- return '♡︎ 𝗦𝗵𝗔𝗻 ♡︎';
124
+ return 'Guest';
20
125
  }
21
126
 
127
+ // Detect platform
22
128
  function getPlatform() {
23
129
  const platform = os.platform();
24
130
  const platformMap = {
@@ -62,12 +168,634 @@ function getShell() {
62
168
  return 'Unknown Shell';
63
169
  }
64
170
 
171
+ // --------------------------------------------------------------
172
+ // URL VALIDATION
173
+ // --------------------------------------------------------------
174
+ function isValidHttpUrl(value) {
175
+ if (!value || typeof value !== 'string') return false;
176
+ try {
177
+ const u = new URL(value.trim());
178
+ return u.protocol === 'http:' || u.protocol === 'https:';
179
+ } catch (e) {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ // --------------------------------------------------------------
185
+ // RESPONSE FORMATTER - Extract data from API response
186
+ // --------------------------------------------------------------
187
+ function extractResponseData(data) {
188
+ if (!data) return null;
189
+ if (typeof data === 'string') return data;
190
+ if (data.ShAn) return data.ShAn;
191
+ if (data.data) return data.data;
192
+ if (data.response) return data.response;
193
+ if (data.result) return data.result;
194
+ if (data.message) return data.message;
195
+ return data;
196
+ }
197
+
198
+ function formatResponse(data, type) {
199
+ const responseData = extractResponseData(data);
200
+ if (!responseData) return 'No response received';
201
+ if (typeof responseData === 'string') return responseData;
202
+
203
+ switch(type) {
204
+ case 'baby':
205
+ case 'honey':
206
+ case 'chat':
207
+ if (responseData.response) return responseData.response;
208
+ if (responseData.message) return responseData.message;
209
+ if (responseData.reply) return responseData.reply;
210
+ if (responseData.text) return responseData.text;
211
+ if (responseData.ans) return responseData.ans;
212
+ if (responseData.msg) return responseData.msg;
213
+ if (responseData.content) return responseData.content;
214
+ if (Array.isArray(responseData) && responseData.length > 0) {
215
+ return responseData.map(item => {
216
+ if (typeof item === 'string') return item;
217
+ return item.response || item.message || item.text || JSON.stringify(item);
218
+ }).join('\n');
219
+ }
220
+ break;
221
+
222
+ case 'teach':
223
+ if (responseData.message) return `✅ ${responseData.message}`;
224
+ if (responseData.status) return `✅ ${responseData.status}`;
225
+ if (responseData.success) return `✅ Successfully taught!`;
226
+ break;
227
+
228
+ case 'search':
229
+ if (Array.isArray(responseData)) {
230
+ let result = `\x1b[36m📋 Found ${responseData.length} results:\x1b[0m\n`;
231
+ responseData.slice(0, 5).forEach((item, i) => {
232
+ const title = item.title || item.name || item.videoTitle || 'Untitled';
233
+ result += `\n ${i+1}. \x1b[32m${title}\x1b[0m`;
234
+ if (item.url) result += `\n 🔗 ${item.url}`;
235
+ if (item.channel || item.author) result += `\n 📺 ${item.channel || item.author}`;
236
+ if (item.duration) result += `\n ⏱️ ${item.duration}`;
237
+ });
238
+ if (responseData.length > 5) {
239
+ result += `\n\n \x1b[33m... and ${responseData.length - 5} more results\x1b[0m`;
240
+ }
241
+ return result;
242
+ }
243
+ break;
244
+
245
+ case 'download':
246
+ const url = responseData.url || responseData.downloadUrl || responseData.link || responseData.videoUrl || responseData.ShAn;
247
+ if (url) {
248
+ let result = `\x1b[32m✅ Download URL:\x1b[0m\n🔗 ${url}`;
249
+ if (responseData.title) result += `\n\n📝 Title: ${responseData.title}`;
250
+ if (responseData.duration) result += `\n⏱️ Duration: ${responseData.duration}`;
251
+ if (responseData.thumbnail) result += `\n🖼️ Thumbnail: ${responseData.thumbnail}`;
252
+ if (responseData.quality) result += `\n📊 Quality: ${responseData.quality}`;
253
+ return result;
254
+ }
255
+ break;
256
+
257
+ case 'album':
258
+ if (Array.isArray(responseData)) {
259
+ let result = `\x1b[36m📂 Album contains ${responseData.length} items:\x1b[0m\n`;
260
+ responseData.slice(0, 10).forEach((item, i) => {
261
+ const title = item.title || item.name || item.videoTitle || 'Untitled';
262
+ result += `\n ${i+1}. \x1b[32m${title}\x1b[0m`;
263
+ if (item.url) result += `\n 🔗 ${item.url}`;
264
+ if (item.category) result += `\n 📁 ${item.category}`;
265
+ });
266
+ if (responseData.length > 10) {
267
+ result += `\n\n \x1b[33m... and ${responseData.length - 10} more items\x1b[0m`;
268
+ }
269
+ return result;
270
+ }
271
+ break;
272
+
273
+ case 'list':
274
+ if (Array.isArray(responseData)) {
275
+ let result = `\x1b[36m📋 Total: ${responseData.length} items\x1b[0m\n`;
276
+ responseData.slice(0, 10).forEach((item, i) => {
277
+ if (typeof item === 'string') {
278
+ result += `\n ${i+1}. ${item}`;
279
+ } else {
280
+ const name = item.name || item.title || item.id || 'Item';
281
+ result += `\n ${i+1}. ${name}`;
282
+ }
283
+ });
284
+ if (responseData.length > 10) {
285
+ result += `\n\n \x1b[33m... and ${responseData.length - 10} more\x1b[0m`;
286
+ }
287
+ return result;
288
+ }
289
+ break;
290
+ }
291
+
292
+ if (responseData.message) return responseData.message;
293
+ if (responseData.status && responseData.message) {
294
+ return `[${responseData.status}] ${responseData.message}`;
295
+ }
296
+
297
+ const keys = Object.keys(responseData);
298
+ if (keys.length === 1 && typeof responseData[keys[0]] === 'string') {
299
+ return responseData[keys[0]];
300
+ }
301
+
302
+ return JSON.stringify(responseData, null, 2);
303
+ }
304
+
305
+ // --------------------------------------------------------------
306
+ // URL PLATFORM DETECTOR
307
+ // --------------------------------------------------------------
308
+ function detectPlatform(url) {
309
+ if (!url || typeof url !== 'string') return null;
310
+
311
+ const lowerUrl = url.toLowerCase();
312
+
313
+ if (lowerUrl.includes('youtube.com') || lowerUrl.includes('youtu.be')) {
314
+ return { platform: 'YouTube', api: 'ShAnYtdl' };
315
+ } else if (lowerUrl.includes('tiktok.com')) {
316
+ return { platform: 'TikTok', api: 'ShAnTikdl' };
317
+ } else if (lowerUrl.includes('instagram.com')) {
318
+ return { platform: 'Instagram', api: 'ShAnInstadl' };
319
+ } else if (lowerUrl.includes('facebook.com') || lowerUrl.includes('fb.com') || lowerUrl.includes('fb.watch')) {
320
+ return { platform: 'Facebook', api: 'ShAnFbdl' };
321
+ } else if (lowerUrl.includes('twitter.com') || lowerUrl.includes('x.com')) {
322
+ return { platform: 'Twitter/X', api: 'ShAnTwitdl' };
323
+ } else if (lowerUrl.includes('threads.com') || lowerUrl.includes('threads.net')) {
324
+ return { platform: 'Threads', api: 'ShAnThreadl' };
325
+ } else if (lowerUrl.includes('pin.it') || lowerUrl.includes('pinterest.com')) {
326
+ return { platform: 'Pinterest', api: 'ShAnPindl' };
327
+ } else if (lowerUrl.includes('capcut.com')) {
328
+ return { platform: 'CapCut', api: 'ShAnCapcutdl' };
329
+ } else if (lowerUrl.includes('likee.com') || lowerUrl.includes('likee.video') || lowerUrl.includes('l.likee.video')) {
330
+ return { platform: 'Likee', api: 'ShAnLikeedl' };
331
+ }
332
+
333
+ return null;
334
+ }
335
+
336
+ // --------------------------------------------------------------
337
+ // DOWNLOAD MANAGER
338
+ // --------------------------------------------------------------
339
+ class DownloadManager {
340
+ constructor() {
341
+ this.downloadDir = CONFIG.downloadDir;
342
+ this.history = [];
343
+ this.activeControllers = new Set(); // AbortControllers for in-flight downloads
344
+ this.createDownloadDir();
345
+ this.loadHistory();
346
+ }
347
+
348
+ // Used by the SIGINT handler to cancel in-flight downloads cleanly
349
+ abortAll() {
350
+ for (const controller of this.activeControllers) {
351
+ try { controller.abort(); } catch (e) {}
352
+ }
353
+ }
354
+
355
+ async computeChecksum(filePath) {
356
+ return new Promise((resolve, reject) => {
357
+ const hash = crypto.createHash('sha256');
358
+ const stream = fs.createReadStream(filePath);
359
+ stream.on('data', (chunk) => hash.update(chunk));
360
+ stream.on('end', () => resolve(hash.digest('hex')));
361
+ stream.on('error', reject);
362
+ });
363
+ }
364
+
365
+ createDownloadDir() {
366
+ if (!fs.existsSync(this.downloadDir)) {
367
+ fs.mkdirSync(this.downloadDir, { recursive: true });
368
+ }
369
+ }
370
+
371
+ loadHistory() {
372
+ const historyFile = path.join(this.downloadDir, 'history.json');
373
+ if (fs.existsSync(historyFile)) {
374
+ try {
375
+ this.history = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
376
+ } catch (e) {
377
+ this.history = [];
378
+ }
379
+ }
380
+ }
381
+
382
+ saveHistory() {
383
+ const historyFile = path.join(this.downloadDir, 'history.json');
384
+ try {
385
+ fs.writeFileSync(historyFile, JSON.stringify(this.history, null, 2));
386
+ } catch (e) {}
387
+ }
388
+
389
+ // Strips query string / fragment before inspecting the extension,
390
+ // and falls back sensibly when nothing usable is found.
391
+ getFileExtension(url, fallback = '.mp4') {
392
+ if (!url || typeof url !== 'string') return fallback;
393
+ try {
394
+ const parsedUrl = new URL(url);
395
+ const ext = path.extname(parsedUrl.pathname);
396
+ if (ext && ext.length <= 5) return ext;
397
+ return fallback;
398
+ } catch (e) {
399
+ return fallback;
400
+ }
401
+ }
402
+
403
+ getFileName(url, title, fallbackExt = '.mp4') {
404
+ const cleanTitle = title
405
+ .replace(/[^a-zA-Z0-9 ]/g, '')
406
+ .replace(/\s+/g, '_')
407
+ .substring(0, 50);
408
+
409
+ const ext = this.getFileExtension(url, fallbackExt);
410
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
411
+ return `${cleanTitle || 'video'}_${timestamp}${ext}`;
412
+ }
413
+
414
+ async downloadFile(url, filePath, progressCallback) {
415
+ const controller = new AbortController();
416
+ this.activeControllers.add(controller);
417
+ try {
418
+ const response = await axios({
419
+ url: url,
420
+ method: 'GET',
421
+ responseType: 'stream',
422
+ headers: {
423
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
424
+ 'Accept': '*/*',
425
+ 'Accept-Encoding': 'gzip, deflate, br',
426
+ 'Connection': 'keep-alive'
427
+ },
428
+ timeout: CONFIG.requestTimeoutMs,
429
+ maxRedirects: 10,
430
+ signal: controller.signal
431
+ });
432
+
433
+ const totalSize = parseInt(response.headers['content-length'], 10);
434
+ let downloadedSize = 0;
435
+ let startTime = Date.now();
436
+
437
+ return new Promise((resolve, reject) => {
438
+ const writer = fs.createWriteStream(filePath);
439
+
440
+ response.data.on('data', (chunk) => {
441
+ downloadedSize += chunk.length;
442
+ if (progressCallback && totalSize) {
443
+ const progress = (downloadedSize / totalSize) * 100;
444
+ const elapsed = (Date.now() - startTime) / 1000;
445
+ const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
446
+ progressCallback(progress, totalSize, downloadedSize, speed);
447
+ }
448
+ });
449
+
450
+ response.data.pipe(writer);
451
+
452
+ writer.on('finish', () => {
453
+ const elapsed = (Date.now() - startTime) / 1000;
454
+ this.activeControllers.delete(controller);
455
+ resolve({
456
+ path: filePath,
457
+ size: downloadedSize,
458
+ totalSize: totalSize,
459
+ time: elapsed,
460
+ speed: elapsed > 0 ? downloadedSize / elapsed : 0
461
+ });
462
+ });
463
+
464
+ writer.on('error', (err) => {
465
+ try { fs.unlinkSync(filePath); } catch (e) {}
466
+ this.activeControllers.delete(controller);
467
+ reject(err);
468
+ });
469
+
470
+ response.data.on('error', (err) => {
471
+ try { fs.unlinkSync(filePath); } catch (e) {}
472
+ this.activeControllers.delete(controller);
473
+ reject(err);
474
+ });
475
+ });
476
+ } catch (error) {
477
+ this.activeControllers.delete(controller);
478
+ if (axios.isCancel?.(error) || error.name === 'CanceledError' || error.message === 'canceled') {
479
+ throw new Error('Download canceled');
480
+ }
481
+ throw new Error(`Download failed: ${error.message}`);
482
+ }
483
+ }
484
+
485
+ async downloadFromApi(apiFunction, url, author, title, platform, { showProgress = true } = {}) {
486
+ try {
487
+ if (!isValidHttpUrl(url)) {
488
+ console.log('\x1b[31m❌ Invalid URL supplied.\x1b[0m');
489
+ return { success: false, error: 'Invalid URL' };
490
+ }
491
+
492
+ console.log('\x1b[33m⏳ Fetching video info...\x1b[0m');
493
+ const result = await withRetry(() => apiFunction(url, author), { label: `fetch info (${platform || 'unknown'})` });
494
+
495
+ let downloadUrl = null;
496
+
497
+ if (result && result.ShAn) {
498
+ downloadUrl = result.ShAn;
499
+ } else if (result && result.data && result.data.ShAn) {
500
+ downloadUrl = result.data.ShAn;
501
+ } else if (typeof result === 'string') {
502
+ downloadUrl = result;
503
+ } else if (result) {
504
+ downloadUrl = result.url || result.downloadUrl || result.link || result.videoUrl;
505
+ }
506
+
507
+ if (!downloadUrl) {
508
+ const data = extractResponseData(result);
509
+ if (data) {
510
+ downloadUrl = data.url || data.downloadUrl || data.link || data.videoUrl || data.ShAn;
511
+ }
512
+ }
513
+
514
+ if (!downloadUrl) {
515
+ console.log('\x1b[33m⚠️ No download URL found. Response:\x1b[0m');
516
+ console.log(JSON.stringify(result, null, 2));
517
+ return { success: false, error: 'No download URL found in API response' };
518
+ }
519
+
520
+ downloadUrl = String(downloadUrl).trim();
521
+
522
+ if (!downloadUrl || downloadUrl === 'undefined' || downloadUrl === 'null' || !isValidHttpUrl(downloadUrl)) {
523
+ console.log('\x1b[33m⚠️ Invalid download URL.\x1b[0m');
524
+ return { success: false, error: 'Invalid download URL' };
525
+ }
526
+
527
+ const videoTitle = title || result?.title || 'video';
528
+ const filename = this.getFileName(downloadUrl, videoTitle);
529
+ const filePath = path.join(this.downloadDir, filename);
530
+
531
+ console.log('\x1b[33m⏳ Downloading video...\x1b[0m');
532
+ console.log(`📁 Saving to: ${filePath}`);
533
+
534
+ const downloadResult = await this.downloadFile(downloadUrl, filePath, !showProgress ? null : (progress, total, downloaded, speed) => {
535
+ const safeProgress = Number.isFinite(progress) ? progress : 0;
536
+ const progressBar = this.createProgressBar(safeProgress);
537
+ const sizeStr = total ? this.formatBytes(downloaded) : '?';
538
+ const totalStr = total ? this.formatBytes(total) : '?';
539
+ const speedStr = speed ? this.formatBytes(speed) + '/s' : '?';
540
+ process.stdout.write(`\r${progressBar} ${safeProgress.toFixed(1)}% (${sizeStr}/${totalStr}) @ ${speedStr}`);
541
+ });
542
+
543
+ if (showProgress) console.log('\n');
544
+ console.log('\x1b[32m✅ Download Complete!\x1b[0m');
545
+ console.log(`📁 File saved: ${downloadResult.path}`);
546
+ console.log(`📊 Size: ${this.formatBytes(downloadResult.size)}`);
547
+ console.log(`⏱️ Time: ${downloadResult.time.toFixed(1)}s`);
548
+ console.log(`⚡ Speed: ${this.formatBytes(downloadResult.speed)}/s`);
549
+
550
+ let checksum = null;
551
+ try {
552
+ checksum = await this.computeChecksum(downloadResult.path);
553
+ console.log(`🔒 SHA-256: ${checksum}`);
554
+ } catch (e) {
555
+ // Checksum is a nice-to-have, never fail the download over it
556
+ }
557
+
558
+ this.history.push({
559
+ url: url,
560
+ platform: platform || 'Unknown',
561
+ title: videoTitle,
562
+ filename: filename,
563
+ size: downloadResult.size,
564
+ time: downloadResult.time,
565
+ checksum: checksum,
566
+ downloadedAt: new Date().toISOString()
567
+ });
568
+ this.saveHistory();
569
+ logEvent('info', `Downloaded ${filename} (${platform || 'Unknown'}, ${this.formatBytes(downloadResult.size)})`);
570
+
571
+ return {
572
+ success: true,
573
+ filePath: downloadResult.path,
574
+ filename: filename,
575
+ size: downloadResult.size,
576
+ checksum: checksum,
577
+ metadata: result
578
+ };
579
+ } catch (error) {
580
+ console.error('\x1b[31m❌ Download failed:\x1b[0m', error.message);
581
+ logEvent('error', `Download failed for ${url}: ${error.message}`);
582
+ return { success: false, error: error.message };
583
+ }
584
+ }
585
+
586
+ createProgressBar(percentage) {
587
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(percentage) ? percentage : 0));
588
+ const barLength = 30;
589
+ const filled = Math.floor((clamped / 100) * barLength);
590
+ const empty = barLength - filled;
591
+ return `\x1b[36m[${'█'.repeat(filled)}${'░'.repeat(empty)}]\x1b[0m`;
592
+ }
593
+
594
+ formatBytes(bytes) {
595
+ if (!bytes || bytes <= 0) return '0 Bytes';
596
+ const k = 1024;
597
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
598
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
599
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
600
+ }
601
+
602
+ listDownloads() {
603
+ if (!fs.existsSync(this.downloadDir)) {
604
+ return [];
605
+ }
606
+ return fs.readdirSync(this.downloadDir)
607
+ .filter(file => file !== 'history.json')
608
+ .map(file => ({
609
+ name: file,
610
+ path: path.join(this.downloadDir, file),
611
+ size: fs.statSync(path.join(this.downloadDir, file)).size,
612
+ created: fs.statSync(path.join(this.downloadDir, file)).birthtime
613
+ }));
614
+ }
615
+
616
+ getDownloadStats() {
617
+ const files = this.listDownloads();
618
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
619
+ return {
620
+ count: files.length,
621
+ totalSize: this.formatBytes(totalSize),
622
+ history: this.history.slice(-10),
623
+ files: files
624
+ };
625
+ }
626
+
627
+ getHistoryStats() {
628
+ const total = this.history.length;
629
+ const totalSize = this.history.reduce((sum, h) => sum + (h.size || 0), 0);
630
+ const platforms = {};
631
+ this.history.forEach(h => {
632
+ platforms[h.platform] = (platforms[h.platform] || 0) + 1;
633
+ });
634
+ return { total, totalSize: this.formatBytes(totalSize), platforms };
635
+ }
636
+
637
+ clearDownloads() {
638
+ const files = this.listDownloads();
639
+ for (const file of files) {
640
+ try { fs.unlinkSync(file.path); } catch (e) {}
641
+ }
642
+ this.history = [];
643
+ this.saveHistory();
644
+ return files.length;
645
+ }
646
+ }
647
+
648
+ const downloadManager = new DownloadManager();
649
+
650
+ // --------------------------------------------------------------
651
+ // AI TEACHING SESSION - Continuous Learning Mode
652
+ // --------------------------------------------------------------
653
+ class AITeachingSession {
654
+ constructor(bot, api, author, uid, font) {
655
+ this.bot = bot;
656
+ this.api = api;
657
+ this.author = author;
658
+ this.uid = uid;
659
+ this.font = font;
660
+ this.questions = [];
661
+ this.answers = [];
662
+ this.sessionCount = 0;
663
+ this.isActive = false;
664
+ }
665
+
666
+ async start() {
667
+ this.isActive = true;
668
+ this.sessionCount = 0;
669
+
670
+ console.clear();
671
+ console.log(`
672
+ \x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m
673
+ \x1b[36m║ 🤖 AI TEACHING SESSION - ${this.bot.toUpperCase()} BOT ║\x1b[0m
674
+ \x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m
675
+ \x1b[33m 🎯 Teach the AI by asking questions and providing answers!\x1b[0m
676
+ \x1b[33m 📝 Type your question, then provide the answer.\x1b[0m
677
+ \x1b[33m 🔄 Continue until you press \x1b[31mCtrl+C\x1b[0m\x1b[33m to exit.\x1b[0m
678
+ \x1b[33m 📊 Each session saves a pair (Question → Answer).\x1b[0m
679
+ \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
680
+ `);
681
+
682
+ console.log(`\x1b[90m Author: ${this.author} | UID: ${this.uid} | Font: ${this.font}\x1b[0m\n`);
683
+
684
+ await this.startTeachingCycle();
685
+ }
686
+
687
+ async startTeachingCycle() {
688
+ while (this.isActive) {
689
+ try {
690
+ this.sessionCount++;
691
+ console.log(`\x1b[36m┌─[ Session #${this.sessionCount} ]──────────────────────────────┐\x1b[0m`);
692
+
693
+ const ask = await this.promptQuestion(`\x1b[32m❓ Enter your question:\x1b[0m`);
694
+ if (!ask || ask.toLowerCase() === 'exit') {
695
+ await this.endSession();
696
+ return;
697
+ }
698
+
699
+ const ans = await this.promptQuestion(`\x1b[33m💡 Enter the answer:\x1b[0m`);
700
+ if (!ans || ans.toLowerCase() === 'exit') {
701
+ await this.endSession();
702
+ return;
703
+ }
704
+
705
+ this.questions.push(ask);
706
+ this.answers.push(ans);
707
+
708
+ console.log(`\x1b[90m⏳ Teaching ${this.bot} bot...\x1b[0m`);
709
+
710
+ let result;
711
+ if (this.bot === 'baby') {
712
+ result = await this.api.ShAnBteach(ask, ans, this.uid, this.font, this.author);
713
+ } else {
714
+ result = await this.api.ShAnHteach(ask, ans, this.uid, this.font, this.author);
715
+ }
716
+
717
+ const formatted = formatResponse(result, 'teach');
718
+ console.log(`\x1b[32m✅ ${formatted}\x1b[0m`);
719
+ console.log(`\x1b[90m Q: ${ask.substring(0, 50)}${ask.length > 50 ? '...' : ''}\x1b[0m`);
720
+ console.log(`\x1b[90m A: ${ans.substring(0, 50)}${ans.length > 50 ? '...' : ''}\x1b[0m`);
721
+ console.log(`\x1b[36m└────────────────────────────────────────────────────────────┘\x1b[0m\n`);
722
+
723
+ const shouldContinue = await this.promptQuestion(`\x1b[36mContinue teaching? (y/n, or press Ctrl+C to exit)\x1b[0m`, 'y');
724
+ if (shouldContinue.toLowerCase() !== 'y' && shouldContinue.toLowerCase() !== 'yes') {
725
+ await this.endSession();
726
+ return;
727
+ }
728
+
729
+ } catch (error) {
730
+ if (error.message && (error.message.includes('canceled') || error.message.includes('exit'))) {
731
+ await this.endSession();
732
+ return;
733
+ }
734
+ console.error(`\x1b[31m❌ Error during teaching: ${error.message}\x1b[0m`);
735
+ const retry = await this.promptQuestion(`\x1b[33mContinue or exit? (c/e)\x1b[0m`, 'c');
736
+ if (retry.toLowerCase() === 'e') {
737
+ await this.endSession();
738
+ return;
739
+ }
740
+ }
741
+ }
742
+ }
743
+
744
+ promptQuestion(question, defaultValue = '') {
745
+ return new Promise((resolve) => {
746
+ const rl = readline.createInterface({
747
+ input: process.stdin,
748
+ output: process.stdout
749
+ });
750
+
751
+ const prompt = defaultValue ? `${question} \x1b[90m[${defaultValue}]\x1b[0m: ` : `${question}: `;
752
+ rl.question(prompt, (answer) => {
753
+ rl.close();
754
+ resolve(answer.trim() || defaultValue);
755
+ });
756
+ });
757
+ }
758
+
759
+ async endSession() {
760
+ this.isActive = false;
761
+ console.log(`
762
+ \x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m
763
+ \x1b[36m║ 📊 TEACHING SESSION SUMMARY ║\x1b[0m
764
+ \x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m
765
+ \x1b[32m ✅ Total Lessons: ${this.sessionCount}\x1b[0m
766
+ \x1b[32m 📝 Questions: ${this.questions.length}\x1b[0m
767
+ \x1b[32m 💡 Answers: ${this.answers.length}\x1b[0m
768
+ \x1b[32m 🤖 Bot: ${this.bot.toUpperCase()}\x1b[0m
769
+ `);
770
+
771
+ if (this.questions.length > 0) {
772
+ console.log(`\x1b[36m┌─ Last 3 Lessons ───────────────────────────────┐\x1b[0m`);
773
+ const lastThree = this.questions.slice(-3);
774
+ const lastThreeAns = this.answers.slice(-3);
775
+ lastThree.forEach((q, i) => {
776
+ console.log(`\x1b[90m ${i + 1}. Q: ${q.substring(0, 40)}${q.length > 40 ? '...' : ''}\x1b[0m`);
777
+ console.log(`\x1b[90m A: ${lastThreeAns[i].substring(0, 40)}${lastThreeAns[i].length > 40 ? '...' : ''}\x1b[0m`);
778
+ });
779
+ console.log(`\x1b[36m└──────────────────────────────────────────────────┘\x1b[0m`);
780
+ }
781
+
782
+ console.log(`\n\x1b[33m🎯 AI has been taught successfully! You can now chat with it.\x1b[0m`);
783
+ console.log(`\x1b[36m👋 Press Enter to return to main menu...\x1b[0m`);
784
+ await this.promptQuestion('');
785
+ }
786
+ }
787
+
788
+ // --------------------------------------------------------------
789
+ // BIG SHAN SERVER LOGO
790
+ // --------------------------------------------------------------
65
791
  function showLogo() {
66
792
  const platform = getPlatform();
67
793
  const terminal = getTerminalType();
68
794
  const shell = getShell();
69
795
  const author = getDefaultAuthor();
70
-
796
+ const stats = downloadManager.getDownloadStats();
797
+ const historyStats = downloadManager.getHistoryStats();
798
+
71
799
  console.clear();
72
800
  console.log(`
73
801
  \x1b[36m
@@ -77,8 +805,8 @@ function showLogo() {
77
805
  ╚════██║██╔══██║██╔══██║██║╚██╗██║
78
806
  ███████║██║ ██║██║ ██║██║ ╚████║
79
807
  ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
80
-
81
- ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
808
+
809
+ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
82
810
  ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗
83
811
  ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝
84
812
  ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗
@@ -86,16 +814,21 @@ function showLogo() {
86
814
  ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
87
815
  \x1b[0m
88
816
  \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
89
- \x1b[36m SHAN SERVER - v1.0.0\x1b[0m
817
+ \x1b[36m SHAN SERVER - v4.0.0 (Ultimate)\x1b[0m
90
818
  \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
91
819
  \x1b[90m Platform: ${platform.padEnd(20)} Terminal: ${terminal}\x1b[0m
92
- \x1b[90m Shell: ${shell.padEnd(20)} API: sh-ans-api-07.vercel.app\x1b[0m
820
+ \x1b[90m Shell: ${shell.padEnd(20)} API: ${CONFIG.apiBase.replace(/^https?:\/\//, '')}\x1b[0m
93
821
  \x1b[90m Default Author: ${author}\x1b[0m
822
+ \x1b[90m 📁 Downloads: ${stats.count} files | ${stats.totalSize}\x1b[0m
823
+ \x1b[90m 📊 History: ${historyStats.total} downloads | ${historyStats.totalSize}\x1b[0m
94
824
  \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
95
825
  `);
96
826
  }
97
827
 
98
- const Sh4n = 'https://sh-ans-api-07.vercel.app/';
828
+ // --------------------------------------------------------------
829
+ // API Configuration
830
+ // --------------------------------------------------------------
831
+ const Sh4n = CONFIG.apiBase;
99
832
 
100
833
  const api = {
101
834
  ShAnAlldl: (url, author) => axios.get(`${Sh4n}ShAn-alldl?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -113,6 +846,7 @@ const api = {
113
846
  ShAnCapcutdl: (url, author) => axios.get(`${Sh4n}ShAn-capcutDL?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
114
847
  ShAnLikeedl: (url, author) => axios.get(`${Sh4n}ShAn-likeeDL?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
115
848
  ShAnytSearch: (query, author) => axios.get(`${Sh4n}ShAn-ytsearch?query=${encodeURIComponent(query)}&author=${encodeURIComponent(author)}`).then(res => res.data),
849
+ ShAntikSearch: (query, author) => axios.get(`${Sh4n}ShAn-tiksearch?query=${encodeURIComponent(query)}&author=${encodeURIComponent(author)}`).then(res => res.data),
116
850
  ShAnBaby: (text, uid, font, author) => axios.get(`${Sh4n}ShAn-bby?text=${encodeURIComponent(text)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
117
851
  ShAnBteach: (ask, ans, uid, font, author) => axios.get(`${Sh4n}ShAn-bteach?ask=${encodeURIComponent(ask)}&ans=${encodeURIComponent(ans)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
118
852
  ShAnBrans: (author) => axios.get(`${Sh4n}ShAn-brans?author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -120,12 +854,12 @@ const api = {
120
854
  ShAnBlist: (font, author) => axios.get(`${Sh4n}ShAn-blist?font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
121
855
  ShAnBedit: (ask, newAsk, uid, font, author, index) => {
122
856
  let url = `${Sh4n}ShAn-bedit?ask=${encodeURIComponent(ask)}&newAsk=${encodeURIComponent(newAsk)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
123
- if (index) url += `&index=${index}`;
857
+ if (index) url += `&index=${encodeURIComponent(index)}`;
124
858
  return axios.get(url).then(res => res.data);
125
859
  },
126
860
  ShAnBdelete: (text, uid, font, author, index) => {
127
861
  let url = `${Sh4n}ShAn-bdelete?text=${encodeURIComponent(text)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
128
- if (index) url += `&index=${index}`;
862
+ if (index) url += `&index=${encodeURIComponent(index)}`;
129
863
  return axios.delete(url).then(res => res.data);
130
864
  },
131
865
  ShAnHoney: (text, uid, font, author) => axios.get(`${Sh4n}ShAn-honey?text=${encodeURIComponent(text)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -134,12 +868,12 @@ const api = {
134
868
  ShAnHlist: (font, author) => axios.get(`${Sh4n}ShAn-hlist?font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
135
869
  ShAnHedit: (ask, newAsk, uid, font, author, index) => {
136
870
  let url = `${Sh4n}ShAn-hedit?ask=${encodeURIComponent(ask)}&newAsk=${encodeURIComponent(newAsk)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
137
- if (index) url += `&index=${index}`;
871
+ if (index) url += `&index=${encodeURIComponent(index)}`;
138
872
  return axios.get(url).then(res => res.data);
139
873
  },
140
874
  ShAnHdelete: (text, uid, font, author, index) => {
141
875
  let url = `${Sh4n}ShAn-hdelete?text=${encodeURIComponent(text)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
142
- if (index) url += `&index=${index}`;
876
+ if (index) url += `&index=${encodeURIComponent(index)}`;
143
877
  return axios.delete(url).then(res => res.data);
144
878
  },
145
879
  ShAnalbumVideos: (category, senderID, author, key) => axios.get(`${Sh4n}ShAn-album-videos?category=${category}&senderID=${senderID}&author=${encodeURIComponent(author)}&key=${encodeURIComponent(key)}`).then(res => res.data),
@@ -147,7 +881,7 @@ const api = {
147
881
  ShAnalbumDelete: (url, author, key) => axios.delete(`${Sh4n}ShAn-album-delete?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}&key=${encodeURIComponent(key)}`).then(res => res.data),
148
882
  ShAnalbumList: (author) => axios.get(`${Sh4n}ShAn-album-list?author=${encodeURIComponent(author)}`).then(res => res.data),
149
883
  ShAnImgur: (videoUrl, author) => axios.post(`${Sh4n}ShAn-imgur?url=${encodeURIComponent(videoUrl)}&author=${encodeURIComponent(author)}`).then(res => res.data),
150
- ShAntikSearch: (query, author) => axios.get(`${Sh4n}ShAn-tiksearch?query=${encodeURIComponent(query)}&author=${encodeURIComponent(author)}`).then(res => res.data),
884
+ ShAnImgbb: (url, author) => axios.get(`${Sh4n}ShAn-imgbb?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
151
885
  ShAnFont: (text, font, author) => axios.get(`${Sh4n}ShAn-font?text=${encodeURIComponent(text)}&font=${encodeURIComponent(font)}&author=${encodeURIComponent(author)}`).then(res => res.data),
152
886
  ShAnfontList: (author) => axios.get(`${Sh4n}ShAn-fontList?author=${encodeURIComponent(author)}`).then(res => res.data),
153
887
  ShAnWish: (name, font, author) => axios.get(`${Sh4n}ShAn-wish?name=${encodeURIComponent(name)}&font=${encodeURIComponent(font)}&author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -155,10 +889,19 @@ const api = {
155
889
  ShAncaptionList: (language, author) => axios.get(`${Sh4n}ShAn-caption-list?language=${encodeURIComponent(language)}&author=${encodeURIComponent(author)}`).then(res => res.data),
156
890
  ShAnCaption: (category, language, senderID, author, key) => axios.get(`${Sh4n}ShAn-caption?category=${encodeURIComponent(category)}&language=${encodeURIComponent(language)}&senderID=${encodeURIComponent(senderID)}&author=${encodeURIComponent(author)}&key=${encodeURIComponent(key)}`).then(res => res.data),
157
891
  ShAnmemeAdd: (memeUrl, senderID, author) => axios.post(`${Sh4n}ShAn-meme-add?memeUrl=${encodeURIComponent(memeUrl)}&senderID=${encodeURIComponent(senderID)}&author=${encodeURIComponent(author)}`).then(res => res.data),
158
- ShAnMeme: (author) => axios.get(`${Sh4n}ShAn-meme?author=${encodeURIComponent(author)}`).then(res => res.data),
159
- ShAnImgbb: (url, author) => axios.get(`${Sh4n}ShAn-imgbb?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data)
892
+ ShAnMeme: (author) => axios.get(`${Sh4n}ShAn-meme?author=${encodeURIComponent(author)}`).then(res => res.data)
160
893
  };
161
894
 
895
+ // Wrap every API function with automatic retry/backoff so transient network
896
+ // blips don't force the user to redo an entire menu flow.
897
+ for (const key of Object.keys(api)) {
898
+ const original = api[key];
899
+ api[key] = (...args) => withRetry(() => original(...args), { label: key });
900
+ }
901
+
902
+ // --------------------------------------------------------------
903
+ // PROMPT FUNCTIONS
904
+ // --------------------------------------------------------------
162
905
  function createInterface() {
163
906
  return readline.createInterface({
164
907
  input: process.stdin,
@@ -183,7 +926,7 @@ async function selectOption(question, options) {
183
926
  options.forEach((opt, idx) => {
184
927
  console.log(` ${idx + 1}. ${opt}`);
185
928
  });
186
-
929
+
187
930
  return new Promise((resolve) => {
188
931
  rl.question('\x1b[33mEnter number (or name): \x1b[0m', (answer) => {
189
932
  rl.close();
@@ -200,460 +943,899 @@ async function selectOption(question, options) {
200
943
  });
201
944
  }
202
945
 
203
- async function showMainMenu() {
204
- showLogo();
205
-
206
- console.log(`
207
- \x1b[36m┌─────────────────────────────────────────────────────────┐\x1b[0m
208
- \x1b[36m│ 📋 MAIN MENU │\x1b[0m
209
- \x1b[36m├─────────────────────────────────────────────────────────┤\x1b[0m
210
- \x1b[32m│ 1. 📥 Download Videos │\x1b[0m
211
- \x1b[32m│ 2. 🔍 Search Content │\x1b[0m
212
- \x1b[32m│ 3. 🤖 AI Chatbots (Baby/Honey) │\x1b[0m
213
- \x1b[32m│ 4. 💾 Album Management │\x1b[0m
214
- \x1b[32m│ 5. 🎨 Font & Text Utilities │\x1b[0m
215
- \x1b[32m│ 6. 📝 Caption Manager │\x1b[0m
216
- \x1b[32m│ 7. 🖼️ Meme Generator │\x1b[0m
217
- \x1b[32m│ 8. ☁️ Cloud Upload (Imgur/ImgBB) │\x1b[0m
218
- \x1b[31m│ 0. ❌ Exit │\x1b[0m
219
- \x1b[36m└─────────────────────────────────────────────────────────┘\x1b[0m
220
- `);
221
-
222
- const choice = await askQuestion('\x1b[36mSelect an option\x1b[0m', '0');
223
- return choice;
224
- }
225
-
226
- async function downloadMenu() {
227
- console.clear();
228
- showLogo();
229
-
230
- const platform = await selectOption('📥 Select Platform:', [
231
- 'YouTube', 'TikTok', 'Instagram', 'Facebook', 'Twitter/X',
232
- 'Threads', 'Pinterest', 'CapCut', 'Likee', 'All-in-One'
233
- ]);
234
-
235
- let command = '';
236
- switch(platform) {
237
- case 'YouTube': command = 'ytdl'; break;
238
- case 'TikTok': command = 'tikdl'; break;
239
- case 'Instagram': command = 'instadl'; break;
240
- case 'Facebook': command = 'fbdl'; break;
241
- case 'Twitter/X': command = 'twitdl'; break;
242
- case 'Threads': command = 'threadl'; break;
243
- case 'Pinterest': command = 'pindl'; break;
244
- case 'CapCut': command = 'capcutdl'; break;
245
- case 'Likee': command = 'likeedl'; break;
246
- case 'All-in-One': command = 'alldl'; break;
247
- }
248
-
249
- const url = await askQuestion('\x1b[36mEnter video URL\x1b[0m');
250
- if (!url) {
251
- console.log('\x1b[31m❌ URL is required!\x1b[0m');
252
- await askQuestion('\nPress Enter to continue...');
253
- return;
254
- }
255
-
256
- const defaultAuthor = getDefaultAuthor();
257
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
258
-
259
- console.log('\n\x1b[33m⏳ Processing your request...\x1b[0m\n');
260
-
946
+ // --------------------------------------------------------------
947
+ // Generic error-safe API call wrapper used by the simpler menus
948
+ // --------------------------------------------------------------
949
+ async function runSafely(actionFn) {
261
950
  try {
262
- let result;
263
- switch(command) {
264
- case 'ytdl': result = await api.ShAnYtdl(url, author); break;
265
- case 'tikdl': result = await api.ShAnTikdl(url, author); break;
266
- case 'instadl': result = await api.ShAnInstadl(url, author); break;
267
- case 'fbdl': result = await api.ShAnFbdl(url, author); break;
268
- case 'twitdl': result = await api.ShAnTwitdl(url, author); break;
269
- case 'threadl': result = await api.ShAnThreadl(url, author); break;
270
- case 'pindl': result = await api.ShAnPindl(url, author); break;
271
- case 'capcutdl': result = await api.ShAnCapcutdl(url, author); break;
272
- case 'likeedl': result = await api.ShAnLikeedl(url, author); break;
273
- default: result = await api.ShAnAlldl(url, author);
274
- }
275
-
276
- console.log('\n\x1b[32m✅ SUCCESS!\x1b[0m\n');
277
- console.log(JSON.stringify(result, null, 2));
951
+ await actionFn();
278
952
  } catch (err) {
279
953
  console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
280
954
  }
281
-
282
- await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
283
955
  }
284
956
 
285
- async function searchMenu() {
957
+ // --------------------------------------------------------------
958
+ // RANDOM TEACH MODE - ShAnBrans returns question, user provides answer
959
+ // --------------------------------------------------------------
960
+ async function randomTeachMode(bot, author, uid, font) {
286
961
  console.clear();
287
962
  showLogo();
288
-
289
- const platform = await selectOption('🔍 Search On:', ['YouTube', 'TikTok']);
290
- const query = await askQuestion('\x1b[36mEnter search query\x1b[0m');
291
-
292
- if (!query) {
293
- console.log('\x1b[31m❌ Query is required!\x1b[0m');
294
- await askQuestion('\nPress Enter to continue...');
295
- return;
296
- }
297
-
298
- const defaultAuthor = getDefaultAuthor();
299
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
300
-
301
- console.log('\n\x1b[33m⏳ Searching...\x1b[0m\n');
302
-
303
- try {
304
- let result;
305
- if (platform === 'YouTube') {
306
- result = await api.ShAnytSearch(query, author);
307
- } else {
308
- result = await api.ShAntikSearch(query, author);
963
+
964
+ console.log(`
965
+ \x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m
966
+ \x1b[36m║ 🎯 RANDOM TEACH MODE - ${bot.toUpperCase()} BOT ║\x1b[0m
967
+ \x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m
968
+ \x1b[33m 🤖 AI gives a random question!\x1b[0m
969
+ \x1b[33m 💡 You provide the answer for that question!\x1b[0m
970
+ \x1b[33m 📝 This teaches the AI new responses!\x1b[0m
971
+ \x1b[33m 🔄 Type \x1b[31m"exit"\x1b[33m to stop teaching\x1b[0m
972
+ \x1b[33m 📊 Type \x1b[31m"skip"\x1b[33m to skip current question\x1b[0m
973
+ \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
974
+ `);
975
+
976
+ console.log(`\x1b[90m Author: ${author} | UID: ${uid} | Font: ${font}\x1b[0m\n`);
977
+
978
+ let teachingCount = 0;
979
+ let continueTeaching = true;
980
+
981
+ while (continueTeaching) {
982
+ try {
983
+ // Get random question from Baby
984
+ const randomResult = await api.ShAnBrans(author);
985
+ const randomQuestion = extractResponseData(randomResult);
986
+
987
+ // Handle different response formats
988
+ let questionText = '';
989
+ if (typeof randomQuestion === 'string') {
990
+ questionText = randomQuestion;
991
+ } else if (randomQuestion) {
992
+ questionText = randomQuestion.response || randomQuestion.message || randomQuestion.text || randomQuestion.ans || randomQuestion.msg || JSON.stringify(randomQuestion);
993
+ } else {
994
+ questionText = 'No question received';
995
+ }
996
+
997
+ console.log(`\x1b[36m┌─[ Teaching Session #${teachingCount + 1} ]────────────────────────────┐\x1b[0m`);
998
+ console.log(`\x1b[32m🤖 ${bot} asks (random question):\x1b[0m ${questionText}`);
999
+
1000
+ const answer = await askQuestion(`\x1b[33m💡 Enter your answer (or type "exit" to stop):\x1b[0m`);
1001
+
1002
+ // Check for exit
1003
+ if (!answer || answer.toLowerCase() === 'exit') {
1004
+ console.log(`\n\x1b[36m👋 Ending teaching session!\x1b[0m`);
1005
+ break;
1006
+ }
1007
+
1008
+ // Check for skip
1009
+ if (answer.toLowerCase() === 'skip') {
1010
+ console.log(`\x1b[33m⏭️ Skipped this question!\x1b[0m`);
1011
+ console.log(`\x1b[36m└────────────────────────────────────────────────────────────────┘\x1b[0m\n`);
1012
+ continue;
1013
+ }
1014
+
1015
+ console.log(`\x1b[90m⏳ Teaching ${bot} bot...\x1b[0m`);
1016
+
1017
+ // Teach the bot with the question and answer
1018
+ let result;
1019
+ if (bot === 'Baby') {
1020
+ result = await api.ShAnBteach(questionText, answer, uid, font, author);
1021
+ } else {
1022
+ result = await api.ShAnHteach(questionText, answer, uid, font, author);
1023
+ }
1024
+
1025
+ teachingCount++;
1026
+ const formatted = formatResponse(result, 'teach');
1027
+
1028
+ console.log(`\x1b[32m✅ ${formatted || 'Successfully taught!'}\x1b[0m`);
1029
+ console.log(`\x1b[90m Q: ${questionText.substring(0, 50)}${questionText.length > 50 ? '...' : ''}\x1b[0m`);
1030
+ console.log(`\x1b[90m A: ${answer.substring(0, 50)}${answer.length > 50 ? '...' : ''}\x1b[0m`);
1031
+ console.log(`\x1b[36m└────────────────────────────────────────────────────────────────┘\x1b[0m\n`);
1032
+
1033
+ // Show progress
1034
+ console.log(`\x1b[90m📊 Taught: ${teachingCount} responses\x1b[0m\n`);
1035
+
1036
+ } catch (error) {
1037
+ console.error(`\x1b[31m❌ Error: ${error.message}\x1b[0m`);
1038
+ console.log(`\x1b[33m⏳ Continuing to next random question...\x1b[0m\n`);
309
1039
  }
310
-
311
- console.log('\x1b[32m✅ Search Results:\x1b[0m\n');
312
- console.log(JSON.stringify(result, null, 2));
313
- } catch (err) {
314
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
315
1040
  }
316
-
1041
+
1042
+ // Summary
1043
+ console.log(`
1044
+ \x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m
1045
+ \x1b[36m║ 📊 TEACHING SESSION SUMMARY ║\x1b[0m
1046
+ \x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m
1047
+ \x1b[32m ✅ Total Lessons: ${teachingCount}\x1b[0m
1048
+ \x1b[32m 🤖 Bot: ${bot.toUpperCase()}\x1b[0m
1049
+ \x1b[32m 👤 Author: ${author}\x1b[0m
1050
+ `);
1051
+
1052
+ if (teachingCount > 0) {
1053
+ console.log(`\x1b[36m🎯 ${bot} has been taught ${teachingCount} new responses!\x1b[0m`);
1054
+ } else {
1055
+ console.log(`\x1b[33m⚠️ No new responses were taught.\x1b[0m`);
1056
+ }
1057
+
1058
+ console.log(`\n\x1b[33m🎯 You can now chat with ${bot} and see the new responses!\x1b[0m`);
317
1059
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
318
1060
  }
319
1061
 
1062
+ // --------------------------------------------------------------
1063
+ // AI CHATBOT MENU
1064
+ // --------------------------------------------------------------
320
1065
  async function aiChatbotMenu() {
321
1066
  console.clear();
322
1067
  showLogo();
323
-
1068
+
324
1069
  const bot = await selectOption('🤖 Select AI Chatbot:', ['Baby', 'Honey']);
325
- const action = await selectOption('Select Action:', ['Chat', 'Teach', 'Random Response', 'List Data', 'Edit', 'Delete']);
326
-
327
- const defaultAuthor = getDefaultAuthor();
1070
+ const action = await selectOption('Select Action:', [
1071
+ '💬 Chat',
1072
+ '📚 Teach (Continuous)',
1073
+ '🎯 Random Teach',
1074
+ '🎲 Random Response',
1075
+ '📋 List Data',
1076
+ '✏️ Edit',
1077
+ '🗑️ Delete'
1078
+ ]);
1079
+
1080
+ const defaultAuthor = CONFIG.defaultAuthor || getDefaultAuthor();
328
1081
  const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
329
- const uid = await askQuestion('\x1b[36mEnter User ID (your unique identifier)\x1b[0m', 'user123');
330
- const font = await askQuestion('\x1b[36mEnter Font name (default: Arial)\x1b[0m', 'Arial');
331
-
1082
+ const uid = await askQuestion('\x1b[36mEnter User ID (your unique identifier)\x1b[0m', CONFIG.defaultUid || 'user123');
1083
+
1084
+ // Show available fonts
1085
+ console.log(`\n\x1b[36m🎨 Available Font Styles (1-5):\x1b[0m`);
1086
+ console.log(` 1. Bold → 𝐄𝐱𝐚𝐦𝐩𝐥𝐞`);
1087
+ console.log(` 2. Script → 𝓔𝔁𝓪𝓶𝓹𝓵𝓮`);
1088
+ console.log(` 3. Sans Serif → 𝘌𝘹𝘢𝘮𝘱𝘭𝘦`);
1089
+ console.log(` 4. Math Style → 𝔼𝕩𝕒𝕞𝕡𝕝𝕖`);
1090
+ console.log(` 5. Fraktur → 𝔈𝔵𝔞𝔪𝔭𝔩𝔢`);
1091
+
1092
+ const font = await askQuestion('\x1b[36mEnter Font number (1-5, default: 3)\x1b[0m', CONFIG.defaultFont || '3');
1093
+ savePersistedConfig({ defaultAuthor: author, defaultUid: uid, defaultFont: font });
1094
+
1095
+ // Random Teach Mode
1096
+ if (action === 'Random Teach') {
1097
+ await randomTeachMode(bot, author, uid, font);
1098
+ return;
1099
+ }
1100
+
1101
+ if (action === 'Teach (Continuous)') {
1102
+ const session = new AITeachingSession(bot.toLowerCase(), api, author, uid, font);
1103
+ const originalSigInt = process.listeners('SIGINT')[0];
1104
+ process.removeAllListeners('SIGINT');
1105
+ process.on('SIGINT', async () => {
1106
+ console.log('\n\x1b[33m\n⚠️ Teaching session interrupted!\x1b[0m');
1107
+ await session.endSession();
1108
+ process.exit(0);
1109
+ });
1110
+ await session.start();
1111
+ process.removeAllListeners('SIGINT');
1112
+ if (originalSigInt) process.on('SIGINT', originalSigInt);
1113
+ return;
1114
+ }
1115
+
332
1116
  try {
333
1117
  let result;
334
-
1118
+ let formattedResponse;
1119
+
335
1120
  if (action === 'Chat') {
336
- const text = await askQuestion('\x1b[36mEnter your message\x1b[0m');
337
- if (bot === 'Baby') {
338
- result = await api.ShAnBaby(text, uid, font, author);
339
- } else {
340
- result = await api.ShAnHoney(text, uid, font, author);
341
- }
342
- }
343
- else if (action === 'Teach') {
344
- const ask = await askQuestion('\x1b[36mEnter the question/phrase\x1b[0m');
345
- const ans = await askQuestion('\x1b[36mEnter the answer/response\x1b[0m');
346
- if (bot === 'Baby') {
347
- result = await api.ShAnBteach(ask, ans, uid, font, author);
348
- } else {
349
- result = await api.ShAnHteach(ask, ans, uid, font, author);
1121
+ let continueChatting = true;
1122
+ let messageCount = 0;
1123
+
1124
+ console.log(`\n\x1b[36m💬 Starting chat with ${bot}...\x1b[0m`);
1125
+ console.log(`\x1b[33mType "exit" to end chat\x1b[0m\n`);
1126
+
1127
+ while (continueChatting) {
1128
+ const text = await askQuestion(`\x1b[32m💬 You (${bot}):\x1b[0m`);
1129
+
1130
+ if (!text) continue;
1131
+
1132
+ if (text.toLowerCase() === 'exit') {
1133
+ console.log(`\n\x1b[36m👋 Ending chat with ${bot}. Goodbye!\x1b[0m`);
1134
+ break;
1135
+ }
1136
+
1137
+ messageCount++;
1138
+ console.log(`\x1b[90m⏳ ${bot} is thinking...\x1b[0m`);
1139
+
1140
+ if (bot === 'Baby') {
1141
+ result = await api.ShAnBaby(text, uid, font, author);
1142
+ formattedResponse = formatResponse(result, 'baby');
1143
+ console.log(`\n\x1b[36m👶 Baby:\x1b[0m ${formattedResponse}`);
1144
+ } else {
1145
+ result = await api.ShAnHoney(text, uid, font, author);
1146
+ formattedResponse = formatResponse(result, 'honey');
1147
+ console.log(`\n\x1b[33m🍯 Honey:\x1b[0m ${formattedResponse}`);
1148
+ }
1149
+ console.log(`\x1b[90m📝 Message ${messageCount}\x1b[0m\n`);
350
1150
  }
1151
+
1152
+ await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
1153
+ return;
351
1154
  }
1155
+
352
1156
  else if (action === 'Random Response') {
353
1157
  if (bot === 'Baby') {
354
1158
  result = await api.ShAnBrans(author);
1159
+ formattedResponse = formatResponse(result, 'baby');
1160
+ console.log(`\n\x1b[36m🎲 Random Baby Response:\x1b[0m ${formattedResponse}`);
355
1161
  } else {
356
1162
  console.log('\x1b[33m⚠️ Random response only available for Baby bot\x1b[0m');
357
- await askQuestion('\nPress Enter to continue...');
358
- return;
359
1163
  }
360
1164
  }
1165
+
361
1166
  else if (action === 'List Data') {
362
1167
  if (bot === 'Baby') {
363
1168
  result = await api.ShAnBlist(font, author);
1169
+ formattedResponse = formatResponse(result, 'list');
1170
+ console.log(`\n\x1b[36m📋 Baby Bot Data:\x1b[0m\n${formattedResponse}`);
364
1171
  } else {
365
1172
  result = await api.ShAnHlist(font, author);
1173
+ formattedResponse = formatResponse(result, 'list');
1174
+ console.log(`\n\x1b[36m📋 Honey Bot Data:\x1b[0m\n${formattedResponse}`);
366
1175
  }
367
1176
  }
1177
+
368
1178
  else if (action === 'Edit') {
369
1179
  const ask = await askQuestion('\x1b[36mEnter the question to edit\x1b[0m');
370
1180
  const newAsk = await askQuestion('\x1b[36mEnter the new question\x1b[0m');
371
1181
  const index = await askQuestion('\x1b[36mEnter index (optional)\x1b[0m');
1182
+
372
1183
  if (bot === 'Baby') {
373
1184
  result = await api.ShAnBedit(ask, newAsk, uid, font, author, index);
374
1185
  } else {
375
1186
  result = await api.ShAnHedit(ask, newAsk, uid, font, author, index);
376
1187
  }
1188
+ const formatted = formatResponse(result, 'teach');
1189
+ console.log(`\n\x1b[32m✅ ${formatted || 'Successfully edited!'}\x1b[0m`);
377
1190
  }
1191
+
378
1192
  else if (action === 'Delete') {
379
1193
  const text = await askQuestion('\x1b[36mEnter text to delete\x1b[0m');
380
1194
  const index = await askQuestion('\x1b[36mEnter index (optional)\x1b[0m');
1195
+
381
1196
  if (bot === 'Baby') {
382
1197
  result = await api.ShAnBdelete(text, uid, font, author, index);
383
1198
  } else {
384
1199
  result = await api.ShAnHdelete(text, uid, font, author, index);
385
1200
  }
1201
+ const formatted = formatResponse(result, 'teach');
1202
+ console.log(`\n\x1b[32m✅ ${formatted || 'Successfully deleted!'}\x1b[0m`);
386
1203
  }
387
-
388
- console.log('\x1b[32m✅ Success!\x1b[0m\n');
389
- console.log(JSON.stringify(result, null, 2));
1204
+
390
1205
  } catch (err) {
391
1206
  console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
392
1207
  }
393
-
1208
+
394
1209
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
395
1210
  }
396
1211
 
397
- async function albumMenu() {
1212
+ // --------------------------------------------------------------
1213
+ // BATCH DOWNLOAD
1214
+ // --------------------------------------------------------------
1215
+ async function batchDownloadMenu() {
398
1216
  console.clear();
399
1217
  showLogo();
400
-
401
- const action = await selectOption('💾 Album Actions:', ['List Albums', 'Add Video', 'View Videos', 'Delete Video']);
1218
+
1219
+ console.log(`\x1b[36m📋 BATCH DOWNLOAD MODE\x1b[0m`);
1220
+ console.log(`\x1b[33mEnter multiple URLs (one per line). Type "done" when finished.\x1b[0m\n`);
1221
+
402
1222
  const defaultAuthor = getDefaultAuthor();
403
1223
  const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
404
-
405
- try {
406
- let result;
407
-
1224
+
1225
+ const urls = [];
1226
+ console.log(`\n\x1b[36mEnter URLs (type "done" to finish):\x1b[0m`);
1227
+
1228
+ while (true) {
1229
+ const url = await askQuestion(`\x1b[90mURL ${urls.length + 1}:\x1b[0m`);
1230
+ if (url.toLowerCase() === 'done' || url.toLowerCase() === 'exit') break;
1231
+ if (url.trim()) {
1232
+ if (!isValidHttpUrl(url.trim())) {
1233
+ console.log('\x1b[31m❌ That does not look like a valid URL, skipping.\x1b[0m');
1234
+ continue;
1235
+ }
1236
+ urls.push(url.trim());
1237
+ }
1238
+ }
1239
+
1240
+ if (urls.length === 0) {
1241
+ console.log('\x1b[33m⚠️ No URLs entered.\x1b[0m');
1242
+ await askQuestion('\nPress Enter to continue...');
1243
+ return;
1244
+ }
1245
+
1246
+ const concurrencyInput = await askQuestion(
1247
+ `\x1b[36mHow many downloads in parallel?\x1b[0m`,
1248
+ String(CONFIG.batchConcurrency)
1249
+ );
1250
+ const concurrency = Math.max(1, Math.min(10, parseInt(concurrencyInput, 10) || CONFIG.batchConcurrency));
1251
+
1252
+ console.log(`\n\x1b[32m📥 Starting batch download of ${urls.length} videos (${concurrency} at a time)...\x1b[0m\n`);
1253
+
1254
+ let successCount = 0;
1255
+ let failCount = 0;
1256
+ let completed = 0;
1257
+
1258
+ await asyncPool(concurrency, urls, async (url, i) => {
1259
+ console.log(`\x1b[36m📥 [${i + 1}/${urls.length}] Processing:\x1b[0m ${url}`);
1260
+
1261
+ const detected = detectPlatform(url);
1262
+ if (!detected) {
1263
+ console.log(`\x1b[31m❌ Unsupported platform for URL: ${url}\x1b[0m`);
1264
+ failCount++;
1265
+ completed++;
1266
+ return;
1267
+ }
1268
+
1269
+ const apiFunction = api[detected.api];
1270
+ if (!apiFunction) {
1271
+ console.log(`\x1b[31m❌ API function not found for: ${detected.platform}\x1b[0m`);
1272
+ failCount++;
1273
+ completed++;
1274
+ return;
1275
+ }
1276
+
1277
+ const result = await downloadManager.downloadFromApi(apiFunction, url, author, '', detected.platform, { showProgress: concurrency === 1 });
1278
+ completed++;
1279
+ if (result.success) {
1280
+ successCount++;
1281
+ console.log(`\x1b[90m📊 Progress: ${completed}/${urls.length} complete\x1b[0m`);
1282
+ } else {
1283
+ failCount++;
1284
+ }
1285
+ });
1286
+
1287
+ console.log(`\n\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
1288
+ console.log(`\x1b[32m✅ Batch Download Complete!\x1b[0m`);
1289
+ console.log(` ✅ Success: ${successCount}`);
1290
+ console.log(` ❌ Failed: ${failCount}`);
1291
+ console.log(` 📁 Total: ${urls.length}`);
1292
+
1293
+ await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
1294
+ }
1295
+
1296
+ // --------------------------------------------------------------
1297
+ // DOWNLOAD MENU (Single)
1298
+ // --------------------------------------------------------------
1299
+ async function downloadMenu() {
1300
+ console.clear();
1301
+ showLogo();
1302
+
1303
+ let continueDownloading = true;
1304
+ let author = null;
1305
+ let currentApiFunction = null;
1306
+ let currentPlatformName = '';
1307
+ let downloadCount = 0;
1308
+
1309
+ while (continueDownloading) {
1310
+ if (!author) {
1311
+ const defaultAuthor = CONFIG.defaultAuthor || getDefaultAuthor();
1312
+ author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
1313
+ savePersistedConfig({ defaultAuthor: author });
1314
+ }
1315
+
1316
+ if (!currentApiFunction) {
1317
+ const platform = await selectOption('📥 Select Platform:', [
1318
+ 'YouTube', 'TikTok', 'Instagram', 'Facebook', 'Twitter/X',
1319
+ 'Threads', 'Pinterest', 'CapCut', 'Likee', 'All-in-One', 'Auto-Detect'
1320
+ ]);
1321
+
1322
+ if (platform === 'Auto-Detect') {
1323
+ currentApiFunction = null;
1324
+ currentPlatformName = 'Auto-Detect';
1325
+ } else {
1326
+ switch(platform) {
1327
+ case 'YouTube': currentApiFunction = api.ShAnYtdl; currentPlatformName = 'YouTube'; break;
1328
+ case 'TikTok': currentApiFunction = api.ShAnTikdl; currentPlatformName = 'TikTok'; break;
1329
+ case 'Instagram': currentApiFunction = api.ShAnInstadl; currentPlatformName = 'Instagram'; break;
1330
+ case 'Facebook': currentApiFunction = api.ShAnFbdl; currentPlatformName = 'Facebook'; break;
1331
+ case 'Twitter/X': currentApiFunction = api.ShAnTwitdl; currentPlatformName = 'Twitter/X'; break;
1332
+ case 'Threads': currentApiFunction = api.ShAnThreadl; currentPlatformName = 'Threads'; break;
1333
+ case 'Pinterest': currentApiFunction = api.ShAnPindl; currentPlatformName = 'Pinterest'; break;
1334
+ case 'CapCut': currentApiFunction = api.ShAnCapcutdl; currentPlatformName = 'CapCut'; break;
1335
+ case 'Likee': currentApiFunction = api.ShAnLikeedl; currentPlatformName = 'Likee'; break;
1336
+ case 'All-in-One': currentApiFunction = api.ShAnAlldl; currentPlatformName = 'All-in-One'; break;
1337
+ }
1338
+ }
1339
+ }
1340
+
1341
+ const url = await askQuestion('\x1b[36mEnter video URL\x1b[0m');
1342
+ if (!url) {
1343
+ console.log('\x1b[31m❌ URL is required!\x1b[0m');
1344
+ continue;
1345
+ }
1346
+ if (!isValidHttpUrl(url)) {
1347
+ console.log('\x1b[31m❌ That does not look like a valid URL!\x1b[0m');
1348
+ continue;
1349
+ }
1350
+
1351
+ let apiFunction = currentApiFunction;
1352
+ let platformName = currentPlatformName;
1353
+
1354
+ if (currentPlatformName === 'Auto-Detect' || !currentApiFunction) {
1355
+ const detected = detectPlatform(url);
1356
+ if (detected) {
1357
+ apiFunction = api[detected.api];
1358
+ platformName = detected.platform;
1359
+ console.log(`\x1b[90m🔍 Detected: ${platformName}\x1b[0m`);
1360
+ } else {
1361
+ console.log('\x1b[31m❌ Could not detect platform. Please select manually.\x1b[0m');
1362
+ continue;
1363
+ }
1364
+ }
1365
+
1366
+ const customTitle = await askQuestion('\x1b[36mEnter custom title (optional)\x1b[0m');
1367
+ const shouldDownload = await askQuestion('\x1b[33mDownload and save to device? (y/n)\x1b[0m', 'y');
1368
+
1369
+ if (shouldDownload.toLowerCase() === 'y' || shouldDownload.toLowerCase() === 'yes') {
1370
+ downloadCount++;
1371
+ const result = await downloadManager.downloadFromApi(apiFunction, url, author, customTitle, platformName);
1372
+ if (result.success) {
1373
+ console.log('\n\x1b[32m✅ Video saved successfully!\x1b[0m');
1374
+ console.log(`📁 Location: ${result.filePath}`);
1375
+
1376
+ const stats = downloadManager.getDownloadStats();
1377
+ console.log(`\n\x1b[36m📊 Download Statistics:\x1b[0m`);
1378
+ console.log(` Downloads in session: ${downloadCount}`);
1379
+ console.log(` Total Files: ${stats.count}`);
1380
+ console.log(` Total Size: ${stats.totalSize}`);
1381
+ }
1382
+ } else {
1383
+ console.log(`\n\x1b[33m⏳ Fetching ${platformName} video info...\x1b[0m`);
1384
+ await runSafely(async () => {
1385
+ const result = await apiFunction(url, author);
1386
+ const formatted = formatResponse(result, 'download');
1387
+ console.log(`\n\x1b[32m✅ ${platformName} Video Info:\x1b[0m\n${formatted}`);
1388
+ });
1389
+ }
1390
+
1391
+ console.log(`\n\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
1392
+ console.log(`\x1b[33m📌 Options:\x1b[0m`);
1393
+ console.log(` \x1b[32m1.\x1b[0m Press \x1b[36mEnter\x1b[0m to return to Main Menu`);
1394
+ console.log(` \x1b[32m2.\x1b[0m Enter a new \x1b[36mURL\x1b[0m to download more videos`);
1395
+ console.log(` \x1b[32m3.\x1b[0m Type \x1b[31m"exit"\x1b[0m to quit`);
1396
+ console.log(` \x1b[32m4.\x1b[0m Type \x1b[33m"change"\x1b[0m to change platform`);
1397
+ console.log(` \x1b[32m5.\x1b[0m Type \x1b[33m"author"\x1b[0m to change author name`);
1398
+ console.log(` \x1b[32m6.\x1b[0m Type \x1b[33m"stats"\x1b[0m to show download stats`);
1399
+ console.log(`\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
1400
+
1401
+ const nextAction = await askQuestion(`\x1b[36mEnter URL, press Enter for Main Menu, or type a command\x1b[0m`);
1402
+
1403
+ if (nextAction.toLowerCase() === 'exit') {
1404
+ console.log('\n\x1b[36m👋 Goodbye!\x1b[0m');
1405
+ process.exit(0);
1406
+ }
1407
+
1408
+ if (nextAction.toLowerCase() === 'change') {
1409
+ currentApiFunction = null;
1410
+ currentPlatformName = '';
1411
+ console.log('\n\x1b[33m🔄 Platform changed. Select new platform...\x1b[0m');
1412
+ continue;
1413
+ }
1414
+
1415
+ if (nextAction.toLowerCase() === 'author') {
1416
+ author = null;
1417
+ console.log('\n\x1b[33m👤 Author changed. Enter new author...\x1b[0m');
1418
+ continue;
1419
+ }
1420
+
1421
+ if (nextAction.toLowerCase() === 'stats') {
1422
+ const stats = downloadManager.getDownloadStats();
1423
+ console.log(`\n\x1b[36m📊 Download Statistics:\x1b[0m`);
1424
+ console.log(` Total Files: ${stats.count}`);
1425
+ console.log(` Total Size: ${stats.totalSize}`);
1426
+ console.log(` Downloads in session: ${downloadCount}`);
1427
+ continue;
1428
+ }
1429
+
1430
+ if (nextAction && nextAction.trim() !== '') {
1431
+ if (isValidHttpUrl(nextAction.trim())) {
1432
+ const detected = detectPlatform(nextAction);
1433
+ if (detected) {
1434
+ const result = await downloadManager.downloadFromApi(api[detected.api], nextAction.trim(), author, '', detected.platform);
1435
+ if (result.success) {
1436
+ downloadCount++;
1437
+ console.log('\n\x1b[32m✅ Video saved successfully!\x1b[0m');
1438
+ console.log(`📁 Location: ${result.filePath}`);
1439
+
1440
+ const stats = downloadManager.getDownloadStats();
1441
+ console.log(`\n\x1b[36m📊 Download Statistics:\x1b[0m`);
1442
+ console.log(` Downloads in session: ${downloadCount}`);
1443
+ console.log(` Total Files: ${stats.count}`);
1444
+ console.log(` Total Size: ${stats.totalSize}`);
1445
+ }
1446
+ continue;
1447
+ }
1448
+ }
1449
+ console.log('\x1b[33m⚠️ Invalid input. Returning to Main Menu...\x1b[0m');
1450
+ await askQuestion('\nPress Enter to continue...');
1451
+ return;
1452
+ } else {
1453
+ console.log('\n\x1b[36m↩️ Returning to Main Menu...\x1b[0m');
1454
+ await askQuestion('\nPress Enter to continue...');
1455
+ return;
1456
+ }
1457
+ }
1458
+ }
1459
+
1460
+ // --------------------------------------------------------------
1461
+ // HISTORY MENU
1462
+ // --------------------------------------------------------------
1463
+ async function historyMenu() {
1464
+ console.clear();
1465
+ showLogo();
1466
+
1467
+ const historyStats = downloadManager.getHistoryStats();
1468
+ console.log(`\n\x1b[36m📊 Download History\x1b[0m`);
1469
+ console.log(`\x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
1470
+ console.log(` Total Downloads: ${historyStats.total}`);
1471
+ console.log(` Total Size: ${historyStats.totalSize}`);
1472
+ console.log(`\n \x1b[36mPlatforms:\x1b[0m`);
1473
+ for (const [platform, count] of Object.entries(historyStats.platforms)) {
1474
+ console.log(` • ${platform}: ${count}`);
1475
+ }
1476
+
1477
+ const history = downloadManager.history.slice(-10).reverse();
1478
+ if (history.length > 0) {
1479
+ console.log(`\n\x1b[36m📋 Last 10 Downloads:\x1b[0m`);
1480
+ history.forEach((item, i) => {
1481
+ console.log(` ${i + 1}. \x1b[32m${item.title || 'Untitled'}\x1b[0m`);
1482
+ console.log(` 📁 ${item.filename}`);
1483
+ console.log(` 📊 ${downloadManager.formatBytes(item.size || 0)}`);
1484
+ console.log(` ⏱️ ${item.time ? item.time.toFixed(1) + 's' : 'N/A'}`);
1485
+ console.log(` 📅 ${new Date(item.downloadedAt).toLocaleString()}`);
1486
+ });
1487
+ } else {
1488
+ console.log('\n\x1b[33m⚠️ No download history yet.\x1b[0m');
1489
+ }
1490
+
1491
+ await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
1492
+ }
1493
+
1494
+ // --------------------------------------------------------------
1495
+ // SEARCH MENU
1496
+ // --------------------------------------------------------------
1497
+ async function searchMenu() {
1498
+ console.clear();
1499
+ showLogo();
1500
+ const platform = await selectOption('🔍 Search On:', ['YouTube', 'TikTok']);
1501
+ const query = await askQuestion('\x1b[36mEnter search query\x1b[0m');
1502
+ if (!query) { console.log('\x1b[31m❌ Query is required!\x1b[0m'); await askQuestion('\nPress Enter to continue...'); return; }
1503
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1504
+ console.log(`\n\x1b[33m⏳ Searching ${platform} for "${query}"...\x1b[0m\n`);
1505
+ await runSafely(async () => {
1506
+ const result = platform === 'YouTube' ? await api.ShAnytSearch(query, author) : await api.ShAntikSearch(query, author);
1507
+ const formatted = formatResponse(result, 'search');
1508
+ console.log(`\x1b[32m✅ Search Results:\x1b[0m\n${formatted}`);
1509
+ });
1510
+ await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
1511
+ }
1512
+
1513
+ // --------------------------------------------------------------
1514
+ // ALBUM MENU
1515
+ // --------------------------------------------------------------
1516
+ async function albumMenu() {
1517
+ console.clear();
1518
+ showLogo();
1519
+ const action = await selectOption('💾 Album Actions:', ['List Albums', 'Add Video', 'View Videos', 'Delete Video']);
1520
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1521
+ await runSafely(async () => {
1522
+ let result, formatted;
408
1523
  if (action === 'List Albums') {
409
1524
  result = await api.ShAnalbumList(author);
410
- }
411
- else if (action === 'Add Video') {
1525
+ formatted = formatResponse(result, 'album');
1526
+ console.log(`\n\x1b[32m✅ Albums:\x1b[0m\n${formatted}`);
1527
+ } else if (action === 'Add Video') {
412
1528
  const category = await askQuestion('\x1b[36mEnter category name\x1b[0m');
413
1529
  const videoUrl = await askQuestion('\x1b[36mEnter video URL\x1b[0m');
414
1530
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
415
1531
  result = await api.ShAnalbumAdd(category, videoUrl, senderID, author);
416
- }
417
- else if (action === 'View Videos') {
1532
+ const formattedMsg = formatResponse(result, 'teach');
1533
+ console.log(`\n\x1b[32m✅ ${formattedMsg || `Video added to "${category}" album!`}\x1b[0m`);
1534
+ } else if (action === 'View Videos') {
418
1535
  const category = await askQuestion('\x1b[36mEnter category name\x1b[0m');
419
1536
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
420
1537
  const key = await askQuestion('\x1b[36mEnter access key (optional)\x1b[0m');
421
1538
  result = await api.ShAnalbumVideos(category, senderID, author, key);
422
- }
423
- else if (action === 'Delete Video') {
1539
+ formatted = formatResponse(result, 'album');
1540
+ console.log(`\n\x1b[32m✅ Videos in "${category}":\x1b[0m\n${formatted}`);
1541
+ } else if (action === 'Delete Video') {
424
1542
  const url = await askQuestion('\x1b[36mEnter video URL to delete\x1b[0m');
425
1543
  const key = await askQuestion('\x1b[36mEnter access key\x1b[0m');
426
1544
  result = await api.ShAnalbumDelete(url, author, key);
1545
+ const formattedMsg = formatResponse(result, 'teach');
1546
+ console.log(`\n\x1b[32m✅ ${formattedMsg || 'Video deleted successfully!'}\x1b[0m`);
427
1547
  }
428
-
429
- console.log('\x1b[32m✅ Success!\x1b[0m\n');
430
- console.log(JSON.stringify(result, null, 2));
431
- } catch (err) {
432
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
433
- }
434
-
1548
+ });
435
1549
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
436
1550
  }
437
1551
 
1552
+ // --------------------------------------------------------------
1553
+ // FONT MENU
1554
+ // --------------------------------------------------------------
438
1555
  async function fontMenu() {
439
1556
  console.clear();
440
1557
  showLogo();
441
-
442
1558
  const action = await selectOption('🎨 Font Utilities:', ['Apply Font to Text', 'List Available Fonts', 'Generate Wish Card']);
443
- const defaultAuthor = getDefaultAuthor();
444
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
445
-
446
- try {
1559
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1560
+ await runSafely(async () => {
447
1561
  if (action === 'List Available Fonts') {
448
1562
  const result = await api.ShAnfontList(author);
449
- console.log('\n\x1b[32m✅ Available Fonts:\x1b[0m\n');
450
- console.log(JSON.stringify(result, null, 2));
451
- }
452
- else if (action === 'Apply Font to Text') {
1563
+ const formatted = formatResponse(result, 'list');
1564
+ console.log(`\n\x1b[32m✅ Available Fonts:\x1b[0m\n${formatted}`);
1565
+ } else if (action === 'Apply Font to Text') {
453
1566
  const text = await askQuestion('\x1b[36mEnter your text\x1b[0m');
454
1567
  const font = await askQuestion('\x1b[36mEnter font name\x1b[0m', 'Arial');
455
1568
  const result = await api.ShAnFont(text, font, author);
456
- console.log('\x1b[32m✅ Formatted Text:\x1b[0m\n');
457
- console.log(result);
458
- }
459
- else if (action === 'Generate Wish Card') {
1569
+ const formatted = formatResponse(result, 'chat');
1570
+ console.log(`\n\x1b[32m✅ Formatted Text:\x1b[0m\n${formatted}`);
1571
+ } else if (action === 'Generate Wish Card') {
460
1572
  const name = await askQuestion('\x1b[36mEnter name for wish card\x1b[0m');
461
1573
  const font = await askQuestion('\x1b[36mEnter font name\x1b[0m', 'Arial');
462
1574
  const result = await api.ShAnWish(name, font, author);
463
- console.log('\x1b[32m✅ Wish Card Generated:\x1b[0m\n');
464
- console.log(JSON.stringify(result, null, 2));
1575
+ const data = extractResponseData(result);
1576
+ console.log(`\n\x1b[32m✅ Wish Card Generated:\x1b[0m`);
1577
+ console.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
465
1578
  }
466
- } catch (err) {
467
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
468
- }
469
-
1579
+ });
470
1580
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
471
1581
  }
472
1582
 
1583
+ // --------------------------------------------------------------
1584
+ // CAPTION MENU
1585
+ // --------------------------------------------------------------
473
1586
  async function captionMenu() {
474
1587
  console.clear();
475
1588
  showLogo();
476
-
477
1589
  const action = await selectOption('📝 Caption Manager:', ['Add Caption', 'Get Caption', 'List Captions']);
478
- const defaultAuthor = getDefaultAuthor();
479
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
480
-
481
- try {
1590
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1591
+ await runSafely(async () => {
482
1592
  if (action === 'Add Caption') {
483
1593
  const category = await askQuestion('\x1b[36mEnter category\x1b[0m');
484
1594
  const language = await askQuestion('\x1b[36mEnter language (e.g., en, es, hi)\x1b[0m');
485
1595
  const caption = await askQuestion('\x1b[36mEnter caption text\x1b[0m');
486
1596
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
487
1597
  const result = await api.ShAncaptionAdd(category, language, caption, senderID, author);
488
- console.log('\x1b[32m✅ Caption Added!\x1b[0m\n');
489
- console.log(JSON.stringify(result, null, 2));
490
- }
491
- else if (action === 'List Captions') {
1598
+ const formatted = formatResponse(result, 'teach');
1599
+ console.log(`\n\x1b[32m✅ ${formatted || 'Caption added successfully!'}\x1b[0m`);
1600
+ } else if (action === 'List Captions') {
492
1601
  const language = await askQuestion('\x1b[36mEnter language\x1b[0m', 'en');
493
1602
  const result = await api.ShAncaptionList(language, author);
494
- console.log('\x1b[32m✅ Captions List:\x1b[0m\n');
495
- console.log(JSON.stringify(result, null, 2));
496
- }
497
- else if (action === 'Get Caption') {
1603
+ const formatted = formatResponse(result, 'list');
1604
+ console.log(`\n\x1b[32m✅ Captions List:\x1b[0m\n${formatted}`);
1605
+ } else if (action === 'Get Caption') {
498
1606
  const category = await askQuestion('\x1b[36mEnter category\x1b[0m');
499
1607
  const language = await askQuestion('\x1b[36mEnter language\x1b[0m');
500
1608
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
501
1609
  const key = await askQuestion('\x1b[36mEnter access key (optional)\x1b[0m');
502
1610
  const result = await api.ShAnCaption(category, language, senderID, author, key);
503
- console.log('\x1b[32m✅ Caption:\x1b[0m\n');
504
- console.log(JSON.stringify(result, null, 2));
1611
+ const data = extractResponseData(result);
1612
+ console.log(`\n\x1b[32m✅ Caption:\x1b[0m`);
1613
+ console.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
505
1614
  }
506
- } catch (err) {
507
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
508
- }
509
-
1615
+ });
510
1616
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
511
1617
  }
512
1618
 
1619
+ // --------------------------------------------------------------
1620
+ // MEME MENU
1621
+ // --------------------------------------------------------------
513
1622
  async function memeMenu() {
514
1623
  console.clear();
515
1624
  showLogo();
516
-
517
- const action = await selectOption('🖼️ Meme Generator:', ['Get Random Meme', 'Add New Meme']);
518
- const defaultAuthor = getDefaultAuthor();
519
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
520
-
521
- try {
1625
+ const action = await selectOption('🖼️ Meme Generator:', ['Get Random Meme', 'Add New Meme']);
1626
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1627
+ await runSafely(async () => {
522
1628
  if (action === 'Get Random Meme') {
523
1629
  const result = await api.ShAnMeme(author);
524
- console.log('\x1b[32m✅ Random Meme:\x1b[0m\n');
525
- console.log(JSON.stringify(result, null, 2));
526
- }
527
- else if (action === 'Add New Meme') {
1630
+ const data = extractResponseData(result);
1631
+ console.log(`\n\x1b[32m✅ Random Meme:\x1b[0m`);
1632
+ if (typeof data === 'string') console.log(data);
1633
+ else if (data) {
1634
+ if (data.url) console.log(`🖼️ ${data.url}`);
1635
+ if (data.title) console.log(`📝 ${data.title}`);
1636
+ if (data.text) console.log(`💬 ${data.text}`);
1637
+ if (data.image) console.log(`🖼️ ${data.image}`);
1638
+ console.log(JSON.stringify(data, null, 2));
1639
+ }
1640
+ } else if (action === 'Add New Meme') {
528
1641
  const memeUrl = await askQuestion('\x1b[36mEnter meme image/video URL\x1b[0m');
529
1642
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
530
1643
  const result = await api.ShAnmemeAdd(memeUrl, senderID, author);
531
- console.log('\x1b[32m✅ Meme Added!\x1b[0m\n');
532
- console.log(JSON.stringify(result, null, 2));
1644
+ const formatted = formatResponse(result, 'teach');
1645
+ console.log(`\n\x1b[32m✅ ${formatted || 'Meme added successfully!'}\x1b[0m`);
533
1646
  }
534
- } catch (err) {
535
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
536
- }
537
-
1647
+ });
538
1648
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
539
1649
  }
540
1650
 
1651
+ // --------------------------------------------------------------
1652
+ // CLOUD MENU
1653
+ // --------------------------------------------------------------
541
1654
  async function cloudMenu() {
542
1655
  console.clear();
543
1656
  showLogo();
544
-
545
- const platform = await selectOption('☁️ Upload To:', ['Imgur', 'ImgBB']);
1657
+ const platform = await selectOption('☁️ Upload To:', ['Imgur', 'ImgBB']);
546
1658
  const url = await askQuestion('\x1b[36mEnter media URL to upload\x1b[0m');
547
- const defaultAuthor = getDefaultAuthor();
548
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
549
-
550
- if (!url) {
551
- console.log('\x1b[31m❌ URL is required!\x1b[0m');
552
- await askQuestion('\nPress Enter to continue...');
553
- return;
554
- }
555
-
556
- console.log('\n\x1b[33m⏳ Uploading...\x1b[0m\n');
557
-
558
- try {
559
- let result;
560
- if (platform === 'Imgur') {
561
- result = await api.ShAnImgur(url, author);
562
- } else {
563
- result = await api.ShAnImgbb(url, author);
1659
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1660
+ if (!url || !isValidHttpUrl(url)) { console.log('\x1b[31m❌ A valid URL is required!\x1b[0m'); await askQuestion('\nPress Enter to continue...'); return; }
1661
+ console.log(`\n\x1b[33m⏳ Uploading to ${platform}...\x1b[0m\n`);
1662
+ await runSafely(async () => {
1663
+ const result = platform === 'Imgur' ? await api.ShAnImgur(url, author) : await api.ShAnImgbb(url, author);
1664
+ const data = extractResponseData(result);
1665
+ console.log(`\x1b[32m✅ Upload Successful!\x1b[0m`);
1666
+ if (typeof data === 'string') console.log(data);
1667
+ else if (data) {
1668
+ if (data.url) console.log(`🔗 URL: ${data.url}`);
1669
+ if (data.deleteHash) console.log(`🗑️ Delete Hash: ${data.deleteHash}`);
1670
+ if (data.link) console.log(`🔗 Link: ${data.link}`);
1671
+ console.log(JSON.stringify(data, null, 2));
564
1672
  }
565
-
566
- console.log('\x1b[32m✅ Upload Successful!\x1b[0m\n');
567
- console.log(JSON.stringify(result, null, 2));
568
- } catch (err) {
569
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
1673
+ });
1674
+ await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
1675
+ }
1676
+
1677
+ // --------------------------------------------------------------
1678
+ // MANAGE DOWNLOADS MENU
1679
+ // --------------------------------------------------------------
1680
+ async function manageDownloadsMenu() {
1681
+ console.clear();
1682
+ showLogo();
1683
+ const stats = downloadManager.getDownloadStats();
1684
+ console.log(`\n\x1b[36m📁 Download Statistics:\x1b[0m`);
1685
+ console.log(` Total Files: ${stats.count}`);
1686
+ console.log(` Total Size: ${stats.totalSize}`);
1687
+ console.log(` Location: ${downloadManager.downloadDir}`);
1688
+ if (stats.count > 0) {
1689
+ console.log(`\n\x1b[36m📋 Recent Downloads:\x1b[0m`);
1690
+ const files = downloadManager.listDownloads().slice(0, 10);
1691
+ files.forEach((file, index) => {
1692
+ console.log(` ${index + 1}. ${file.name} (${downloadManager.formatBytes(file.size)})`);
1693
+ });
1694
+ const action = await selectOption('\n📂 Actions:', ['Open Downloads Folder', 'Clear All Downloads', 'Back to Main Menu']);
1695
+ if (action === 'Open Downloads Folder') {
1696
+ const open = require('child_process');
1697
+ const platform = os.platform();
1698
+ let command;
1699
+ if (platform === 'win32') command = `start "" "${downloadManager.downloadDir}"`;
1700
+ else if (platform === 'darwin') command = `open "${downloadManager.downloadDir}"`;
1701
+ else command = `xdg-open "${downloadManager.downloadDir}"`;
1702
+ open.exec(command, (err) => {
1703
+ if (err) console.log('\x1b[33m⚠️ Could not open folder automatically\x1b[0m');
1704
+ console.log(`📁 Downloads folder: ${downloadManager.downloadDir}`);
1705
+ });
1706
+ } else if (action === 'Clear All Downloads') {
1707
+ const confirm = await askQuestion('\x1b[31m⚠️ Delete all downloaded files? (y/n)\x1b[0m', 'n');
1708
+ if (confirm.toLowerCase() === 'y') {
1709
+ const count = downloadManager.clearDownloads();
1710
+ console.log(`\x1b[32m✅ Deleted ${count} files\x1b[0m`);
1711
+ }
1712
+ }
1713
+ } else {
1714
+ console.log('\n\x1b[33m⚠️ No downloads found.\x1b[0m');
1715
+ await askQuestion('\nPress Enter to continue...');
570
1716
  }
571
-
572
1717
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
573
1718
  }
574
1719
 
1720
+ // --------------------------------------------------------------
1721
+ // MAIN MENU
1722
+ // --------------------------------------------------------------
1723
+ async function showMainMenu() {
1724
+ showLogo();
1725
+
1726
+ console.log(`
1727
+ \x1b[36m┌─────────────────────────────────────────────────────────┐\x1b[0m
1728
+ \x1b[36m│ 📋 MAIN MENU │\x1b[0m
1729
+ \x1b[36m├─────────────────────────────────────────────────────────┤\x1b[0m
1730
+ \x1b[32m│ 1. 📥 Download & Save Videos │\x1b[0m
1731
+ \x1b[32m│ 2. 📦 Batch Download (Multiple URLs) │\x1b[0m
1732
+ \x1b[32m│ 3. 📂 Manage Downloads │\x1b[0m
1733
+ \x1b[32m│ 4. 📊 Download History │\x1b[0m
1734
+ \x1b[32m│ 5. 🔍 Search Content │\x1b[0m
1735
+ \x1b[32m│ 6. 🤖 AI Chatbots (Baby/Honey) │\x1b[0m
1736
+ \x1b[32m│ 7. 💾 Album Management │\x1b[0m
1737
+ \x1b[32m│ 8. 🎨 Font & Text Utilities │\x1b[0m
1738
+ \x1b[32m│ 9. 📝 Caption Manager │\x1b[0m
1739
+ \x1b[32m│ 10. 🖼️ Meme Generator │\x1b[0m
1740
+ \x1b[32m│ 11. ☁️ Cloud Upload (Imgur/ImgBB) │\x1b[0m
1741
+ \x1b[31m│ 0. ❌ Exit │\x1b[0m
1742
+ \x1b[36m└─────────────────────────────────────────────────────────┘\x1b[0m
1743
+ `);
1744
+
1745
+ const choice = await askQuestion('\x1b[36mSelect an option\x1b[0m', '0');
1746
+ return choice;
1747
+ }
1748
+
1749
+ // --------------------------------------------------------------
1750
+ // MAIN PROGRAM
1751
+ // --------------------------------------------------------------
575
1752
  async function main() {
576
1753
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
577
1754
  showLogo();
578
1755
  console.log(`
579
- \x1b[36mSHAN SERVER - Interactive Media Downloader & Utilities\x1b[0m
1756
+ \x1b[36mSHAN SERVER - Ultimate Media Downloader & Utilities v4.0\x1b[0m
580
1757
 
581
1758
  \x1b[33mUSAGE:\x1b[0m
582
1759
  shan Start interactive menu
583
1760
  shan --help Show this help
584
1761
  shan --version Show version
585
1762
 
586
- \x1b[33mAUTO-AUTHOR (CLI ONLY):\x1b[0m
587
- The CLI automatically detects your system username and uses it as the default author.
588
- If no username is found, it defaults to: \x1b[36m♡︎ 𝗦𝗵𝗔𝗻 ♡︎\x1b[0m
589
-
590
- \x1b[33mCODE USAGE:\x1b[0m
591
- When using in code, author is \x1b[31mREQUIRED\x1b[0m:
592
- const shan = require('shan-server');
593
- const result = await shan.ShAnYtdl(url, '♡︎ 𝗦𝗵𝗔𝗻 ♡︎');
1763
+ \x1b[33mENV VARS:\x1b[0m
1764
+ SHAN_API_BASE Override the backend API base URL
1765
+ SHAN_DOWNLOAD_DIR Override where downloads are saved
1766
+ SHAN_TIMEOUT_MS Override the download request timeout
1767
+ SHAN_CONCURRENCY Default parallel downloads for batch mode (default: 3)
1768
+ SHAN_RETRIES Retry attempts for flaky API/network calls (default: 2)
1769
+ SHAN_RETRY_DELAY_MS Base backoff delay between retries (default: 400)
594
1770
 
595
- \x1b[33mDESCRIPTION:\x1b[0m
596
- An interactive CLI tool for downloading videos from YouTube, TikTok,
597
- Instagram, Facebook, Twitter, and more. Also includes AI chatbots,
598
- album storage, captions, fonts, memes, and cloud uploads.
1771
+ Your author/UID/font choices and these settings also persist between runs in
1772
+ ~/.shan-server/config.json, so you won't need to re-enter them every time.
599
1773
 
600
- \x1b[33mSUPPORTED PLATFORMS:\x1b[0m
601
- Windows (CMD, PowerShell, Terminal)
602
- Linux (bash, zsh, fish)
603
- macOS (Terminal, iTerm2)
604
- Termux (Android)
605
- Chrome OS
606
- Any OS with Node.js
1774
+ \x1b[33mFEATURES:\x1b[0m
1775
+ Download videos from 10+ platforms
1776
+ Parallel batch download with configurable concurrency
1777
+ Automatic retry with exponential backoff on network hiccups
1778
+ SHA-256 checksum verification after every download
1779
+ Graceful Ctrl+C: cancels in-flight downloads and cleans up partial files
1780
+ Persisted preferences (author, UID, font, concurrency) across sessions
1781
+ ✓ Activity log file for auditing downloads/errors
1782
+ ✓ Auto-detect platform from URL
1783
+ ✓ Download history with stats
1784
+ ✓ Save directly to device with speed indicator
1785
+ ✓ Continuous download loop
1786
+ ✓ Change platform & author without restarting
1787
+ ✓ Random Teach Mode - AI asks questions, you provide answers
1788
+ ✓ Continuous AI Teaching Mode (Ctrl+C to exit)
1789
+ ✓ Beautiful chat-style responses
1790
+ ✓ AI Chatbots (Baby/Honey)
1791
+ ✓ Album Management
1792
+ ✓ Font Styling
1793
+ ✓ Caption Manager
1794
+ ✓ Meme Generator
1795
+ ✓ Cloud Upload
607
1796
 
608
- \x1b[33mREQUIREMENTS:\x1b[0m
609
- Node.js >= 12.0.0
1797
+ \x1b[33mRANDOM TEACH MODE:\x1b[0m
1798
+ AI gives a random question using ShAnBrans
1799
+ • You provide the answer
1800
+ • AI learns automatically
1801
+ • Type "exit" to stop
1802
+ • Type "skip" to skip current question
610
1803
 
611
1804
  \x1b[33mEXAMPLES:\x1b[0m
612
1805
  $ shan # Start interactive menu
613
1806
  $ npx shan-server # Run without installing
614
- $ node -e "require('shan-server').ShAnYtdl('url', '♡︎ 𝗦𝗵𝗔𝗻 ♡︎')"
615
1807
 
616
1808
  \x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
617
1809
  `);
618
1810
  process.exit(0);
619
1811
  }
620
-
1812
+
621
1813
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
622
- console.log('shan-server v1.0.4');
623
- console.log(`Default Author: ${getDefaultAuthor()}`);
1814
+ console.log('shan-server v4.0.0 (Ultimate)');
1815
+ console.log(`Default Author (CLI): ${getDefaultAuthor()}`);
1816
+ console.log(`Downloads Folder: ${downloadManager.downloadDir}`);
1817
+ const stats = downloadManager.getDownloadStats();
1818
+ console.log(`Downloads: ${stats.count} files | ${stats.totalSize}`);
624
1819
  process.exit(0);
625
1820
  }
626
-
1821
+
627
1822
  while (true) {
628
1823
  const choice = await showMainMenu();
629
-
630
1824
  switch(choice) {
631
- case '1':
632
- await downloadMenu();
633
- break;
634
- case '2':
635
- await searchMenu();
636
- break;
637
- case '3':
638
- await aiChatbotMenu();
639
- break;
640
- case '4':
641
- await albumMenu();
642
- break;
643
- case '5':
644
- await fontMenu();
645
- break;
646
- case '6':
647
- await captionMenu();
648
- break;
649
- case '7':
650
- await memeMenu();
651
- break;
652
- case '8':
653
- await cloudMenu();
654
- break;
1825
+ case '1': await downloadMenu(); break;
1826
+ case '2': await batchDownloadMenu(); break;
1827
+ case '3': await manageDownloadsMenu(); break;
1828
+ case '4': await historyMenu(); break;
1829
+ case '5': await searchMenu(); break;
1830
+ case '6': await aiChatbotMenu(); break;
1831
+ case '7': await albumMenu(); break;
1832
+ case '8': await fontMenu(); break;
1833
+ case '9': await captionMenu(); break;
1834
+ case '10': await memeMenu(); break;
1835
+ case '11': await cloudMenu(); break;
655
1836
  case '0':
656
1837
  console.log('\n\x1b[36m👋 Thank you for using SHAN SERVER!\x1b[0m\n');
1838
+ console.log(`📁 Downloads saved in: ${downloadManager.downloadDir}`);
657
1839
  process.exit(0);
658
1840
  default:
659
1841
  console.log('\x1b[31m❌ Invalid option! Please try again.\x1b[0m');
@@ -663,14 +1845,24 @@ async function main() {
663
1845
  }
664
1846
 
665
1847
  process.on('SIGINT', () => {
666
- console.log('\n\n\x1b[36m👋 Goodbye from SHAN SERVER!\x1b[0m\n');
667
- process.exit(0);
1848
+ if (downloadManager.activeControllers.size > 0) {
1849
+ console.log(`\n\n\x1b[33m⚠️ Canceling ${downloadManager.activeControllers.size} in-progress download(s)...\x1b[0m`);
1850
+ downloadManager.abortAll();
1851
+ }
1852
+ console.log('\n\x1b[36m👋 Goodbye from SHAN SERVER!\x1b[0m\n');
1853
+ console.log(`📁 Downloads saved in: ${downloadManager.downloadDir}`);
1854
+ logEvent('info', 'Session ended via SIGINT');
1855
+ // Give in-flight cleanup handlers (unlink of partial files) a brief moment to run
1856
+ setTimeout(() => process.exit(0), 150);
668
1857
  });
669
1858
 
670
1859
  if (require.main === module) {
671
1860
  main().catch(console.error);
672
1861
  }
673
1862
 
1863
+ // --------------------------------------------------------------
1864
+ // EXPORT
1865
+ // --------------------------------------------------------------
674
1866
  module.exports = {
675
1867
  ShAnAlldl: api.ShAnAlldl,
676
1868
  ShAnAlldl2: api.ShAnAlldl2,
@@ -687,6 +1879,7 @@ module.exports = {
687
1879
  ShAnCapcutdl: api.ShAnCapcutdl,
688
1880
  ShAnLikeedl: api.ShAnLikeedl,
689
1881
  ShAnytSearch: api.ShAnytSearch,
1882
+ ShAntikSearch: api.ShAntikSearch,
690
1883
  ShAnBaby: api.ShAnBaby,
691
1884
  ShAnBteach: api.ShAnBteach,
692
1885
  ShAnBrans: api.ShAnBrans,
@@ -705,7 +1898,7 @@ module.exports = {
705
1898
  ShAnalbumDelete: api.ShAnalbumDelete,
706
1899
  ShAnalbumList: api.ShAnalbumList,
707
1900
  ShAnImgur: api.ShAnImgur,
708
- ShAntikSearch: api.ShAntikSearch,
1901
+ ShAnImgbb: api.ShAnImgbb,
709
1902
  ShAnFont: api.ShAnFont,
710
1903
  ShAnfontList: api.ShAnfontList,
711
1904
  ShAnWish: api.ShAnWish,
@@ -714,6 +1907,15 @@ module.exports = {
714
1907
  ShAnCaption: api.ShAnCaption,
715
1908
  ShAnmemeAdd: api.ShAnmemeAdd,
716
1909
  ShAnMeme: api.ShAnMeme,
717
- ShAnImgbb: api.ShAnImgbb,
718
- getDefaultAuthor
1910
+ downloadManager,
1911
+ AITeachingSession,
1912
+ getDefaultAuthor,
1913
+ detectPlatform,
1914
+ isValidHttpUrl,
1915
+ withRetry,
1916
+ asyncPool,
1917
+ logEvent,
1918
+ loadPersistedConfig,
1919
+ savePersistedConfig,
1920
+ CONFIG
719
1921
  };