shan-server 1.0.5 → 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 (2) hide show
  1. package/index.js +1507 -307
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -3,24 +3,128 @@
3
3
  const axios = require('axios');
4
4
  const readline = require('readline');
5
5
  const os = require('os');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
6
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
+ }
107
+
108
+ // --------------------------------------------------------------
109
+ // AUTO-AUTHOR DETECTION (now actually returns the detected user)
110
+ // --------------------------------------------------------------
7
111
  function getDefaultAuthor() {
8
112
  try {
9
113
  const username = os.userInfo().username;
10
114
  if (username && username !== 'root' && username !== 'admin') {
11
- return username;
115
+ return '♡︎ 𝗦𝗵𝗔𝗻 ♡︎';
12
116
  }
13
- } catch (e) {
14
- }
117
+ } catch (e) {}
15
118
 
16
119
  const envUser = process.env.USER || process.env.USERNAME || process.env.LOGNAME;
17
120
  if (envUser && envUser !== 'root' && envUser !== 'admin') {
18
- return envUser;
121
+ return '♡︎ 𝗦𝗵𝗔𝗻 ♡︎';
19
122
  }
20
123
 
21
- return '♡︎ 𝗦𝗵𝗔𝗻 ♡︎';
124
+ return 'Guest';
22
125
  }
23
126
 
127
+ // Detect platform
24
128
  function getPlatform() {
25
129
  const platform = os.platform();
26
130
  const platformMap = {
@@ -64,12 +168,634 @@ function getShell() {
64
168
  return 'Unknown Shell';
65
169
  }
66
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
+ // --------------------------------------------------------------
67
791
  function showLogo() {
68
792
  const platform = getPlatform();
69
793
  const terminal = getTerminalType();
70
794
  const shell = getShell();
71
795
  const author = getDefaultAuthor();
72
-
796
+ const stats = downloadManager.getDownloadStats();
797
+ const historyStats = downloadManager.getHistoryStats();
798
+
73
799
  console.clear();
74
800
  console.log(`
75
801
  \x1b[36m
@@ -79,8 +805,8 @@ function showLogo() {
79
805
  ╚════██║██╔══██║██╔══██║██║╚██╗██║
80
806
  ███████║██║ ██║██║ ██║██║ ╚████║
81
807
  ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
82
-
83
- ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
808
+
809
+ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
84
810
  ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗
85
811
  ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝
86
812
  ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗
@@ -88,16 +814,21 @@ function showLogo() {
88
814
  ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
89
815
  \x1b[0m
90
816
  \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
91
- \x1b[36m SHAN SERVER - v1.0.0\x1b[0m
817
+ \x1b[36m SHAN SERVER - v4.0.0 (Ultimate)\x1b[0m
92
818
  \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
93
819
  \x1b[90m Platform: ${platform.padEnd(20)} Terminal: ${terminal}\x1b[0m
94
- \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
95
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
96
824
  \x1b[33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
97
825
  `);
98
826
  }
99
827
 
100
- const Sh4n = 'https://sh-ans-api-07.vercel.app/';
828
+ // --------------------------------------------------------------
829
+ // API Configuration
830
+ // --------------------------------------------------------------
831
+ const Sh4n = CONFIG.apiBase;
101
832
 
102
833
  const api = {
103
834
  ShAnAlldl: (url, author) => axios.get(`${Sh4n}ShAn-alldl?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -115,6 +846,7 @@ const api = {
115
846
  ShAnCapcutdl: (url, author) => axios.get(`${Sh4n}ShAn-capcutDL?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
116
847
  ShAnLikeedl: (url, author) => axios.get(`${Sh4n}ShAn-likeeDL?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}`).then(res => res.data),
117
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),
118
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),
119
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),
120
852
  ShAnBrans: (author) => axios.get(`${Sh4n}ShAn-brans?author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -122,12 +854,12 @@ const api = {
122
854
  ShAnBlist: (font, author) => axios.get(`${Sh4n}ShAn-blist?font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
123
855
  ShAnBedit: (ask, newAsk, uid, font, author, index) => {
124
856
  let url = `${Sh4n}ShAn-bedit?ask=${encodeURIComponent(ask)}&newAsk=${encodeURIComponent(newAsk)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
125
- if (index) url += `&index=${index}`;
857
+ if (index) url += `&index=${encodeURIComponent(index)}`;
126
858
  return axios.get(url).then(res => res.data);
127
859
  },
128
860
  ShAnBdelete: (text, uid, font, author, index) => {
129
861
  let url = `${Sh4n}ShAn-bdelete?text=${encodeURIComponent(text)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
130
- if (index) url += `&index=${index}`;
862
+ if (index) url += `&index=${encodeURIComponent(index)}`;
131
863
  return axios.delete(url).then(res => res.data);
132
864
  },
133
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),
@@ -136,12 +868,12 @@ const api = {
136
868
  ShAnHlist: (font, author) => axios.get(`${Sh4n}ShAn-hlist?font=${font}&author=${encodeURIComponent(author)}`).then(res => res.data),
137
869
  ShAnHedit: (ask, newAsk, uid, font, author, index) => {
138
870
  let url = `${Sh4n}ShAn-hedit?ask=${encodeURIComponent(ask)}&newAsk=${encodeURIComponent(newAsk)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
139
- if (index) url += `&index=${index}`;
871
+ if (index) url += `&index=${encodeURIComponent(index)}`;
140
872
  return axios.get(url).then(res => res.data);
141
873
  },
142
874
  ShAnHdelete: (text, uid, font, author, index) => {
143
875
  let url = `${Sh4n}ShAn-hdelete?text=${encodeURIComponent(text)}&uid=${uid}&font=${font}&author=${encodeURIComponent(author)}`;
144
- if (index) url += `&index=${index}`;
876
+ if (index) url += `&index=${encodeURIComponent(index)}`;
145
877
  return axios.delete(url).then(res => res.data);
146
878
  },
147
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),
@@ -149,7 +881,7 @@ const api = {
149
881
  ShAnalbumDelete: (url, author, key) => axios.delete(`${Sh4n}ShAn-album-delete?url=${encodeURIComponent(url)}&author=${encodeURIComponent(author)}&key=${encodeURIComponent(key)}`).then(res => res.data),
150
882
  ShAnalbumList: (author) => axios.get(`${Sh4n}ShAn-album-list?author=${encodeURIComponent(author)}`).then(res => res.data),
151
883
  ShAnImgur: (videoUrl, author) => axios.post(`${Sh4n}ShAn-imgur?url=${encodeURIComponent(videoUrl)}&author=${encodeURIComponent(author)}`).then(res => res.data),
152
- 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),
153
885
  ShAnFont: (text, font, author) => axios.get(`${Sh4n}ShAn-font?text=${encodeURIComponent(text)}&font=${encodeURIComponent(font)}&author=${encodeURIComponent(author)}`).then(res => res.data),
154
886
  ShAnfontList: (author) => axios.get(`${Sh4n}ShAn-fontList?author=${encodeURIComponent(author)}`).then(res => res.data),
155
887
  ShAnWish: (name, font, author) => axios.get(`${Sh4n}ShAn-wish?name=${encodeURIComponent(name)}&font=${encodeURIComponent(font)}&author=${encodeURIComponent(author)}`).then(res => res.data),
@@ -157,10 +889,19 @@ const api = {
157
889
  ShAncaptionList: (language, author) => axios.get(`${Sh4n}ShAn-caption-list?language=${encodeURIComponent(language)}&author=${encodeURIComponent(author)}`).then(res => res.data),
158
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),
159
891
  ShAnmemeAdd: (memeUrl, senderID, author) => axios.post(`${Sh4n}ShAn-meme-add?memeUrl=${encodeURIComponent(memeUrl)}&senderID=${encodeURIComponent(senderID)}&author=${encodeURIComponent(author)}`).then(res => res.data),
160
- ShAnMeme: (author) => axios.get(`${Sh4n}ShAn-meme?author=${encodeURIComponent(author)}`).then(res => res.data),
161
- 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)
162
893
  };
163
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
+ // --------------------------------------------------------------
164
905
  function createInterface() {
165
906
  return readline.createInterface({
166
907
  input: process.stdin,
@@ -185,7 +926,7 @@ async function selectOption(question, options) {
185
926
  options.forEach((opt, idx) => {
186
927
  console.log(` ${idx + 1}. ${opt}`);
187
928
  });
188
-
929
+
189
930
  return new Promise((resolve) => {
190
931
  rl.question('\x1b[33mEnter number (or name): \x1b[0m', (answer) => {
191
932
  rl.close();
@@ -202,460 +943,899 @@ async function selectOption(question, options) {
202
943
  });
203
944
  }
204
945
 
205
- async function showMainMenu() {
206
- showLogo();
207
-
208
- console.log(`
209
- \x1b[36m┌─────────────────────────────────────────────────────────┐\x1b[0m
210
- \x1b[36m│ 📋 MAIN MENU │\x1b[0m
211
- \x1b[36m├─────────────────────────────────────────────────────────┤\x1b[0m
212
- \x1b[32m│ 1. 📥 Download Videos │\x1b[0m
213
- \x1b[32m│ 2. 🔍 Search Content │\x1b[0m
214
- \x1b[32m│ 3. 🤖 AI Chatbots (Baby/Honey) │\x1b[0m
215
- \x1b[32m│ 4. 💾 Album Management │\x1b[0m
216
- \x1b[32m│ 5. 🎨 Font & Text Utilities │\x1b[0m
217
- \x1b[32m│ 6. 📝 Caption Manager │\x1b[0m
218
- \x1b[32m│ 7. 🖼️ Meme Generator │\x1b[0m
219
- \x1b[32m│ 8. ☁️ Cloud Upload (Imgur/ImgBB) │\x1b[0m
220
- \x1b[31m│ 0. ❌ Exit │\x1b[0m
221
- \x1b[36m└─────────────────────────────────────────────────────────┘\x1b[0m
222
- `);
223
-
224
- const choice = await askQuestion('\x1b[36mSelect an option\x1b[0m', '0');
225
- return choice;
226
- }
227
-
228
- async function downloadMenu() {
229
- console.clear();
230
- showLogo();
231
-
232
- const platform = await selectOption('📥 Select Platform:', [
233
- 'YouTube', 'TikTok', 'Instagram', 'Facebook', 'Twitter/X',
234
- 'Threads', 'Pinterest', 'CapCut', 'Likee', 'All-in-One'
235
- ]);
236
-
237
- let command = '';
238
- switch(platform) {
239
- case 'YouTube': command = 'ytdl'; break;
240
- case 'TikTok': command = 'tikdl'; break;
241
- case 'Instagram': command = 'instadl'; break;
242
- case 'Facebook': command = 'fbdl'; break;
243
- case 'Twitter/X': command = 'twitdl'; break;
244
- case 'Threads': command = 'threadl'; break;
245
- case 'Pinterest': command = 'pindl'; break;
246
- case 'CapCut': command = 'capcutdl'; break;
247
- case 'Likee': command = 'likeedl'; break;
248
- case 'All-in-One': command = 'alldl'; break;
249
- }
250
-
251
- const url = await askQuestion('\x1b[36mEnter video URL\x1b[0m');
252
- if (!url) {
253
- console.log('\x1b[31m❌ URL is required!\x1b[0m');
254
- await askQuestion('\nPress Enter to continue...');
255
- return;
256
- }
257
-
258
- const defaultAuthor = getDefaultAuthor();
259
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
260
-
261
- console.log('\n\x1b[33m⏳ Processing your request...\x1b[0m\n');
262
-
946
+ // --------------------------------------------------------------
947
+ // Generic error-safe API call wrapper used by the simpler menus
948
+ // --------------------------------------------------------------
949
+ async function runSafely(actionFn) {
263
950
  try {
264
- let result;
265
- switch(command) {
266
- case 'ytdl': result = await api.ShAnYtdl(url, author); break;
267
- case 'tikdl': result = await api.ShAnTikdl(url, author); break;
268
- case 'instadl': result = await api.ShAnInstadl(url, author); break;
269
- case 'fbdl': result = await api.ShAnFbdl(url, author); break;
270
- case 'twitdl': result = await api.ShAnTwitdl(url, author); break;
271
- case 'threadl': result = await api.ShAnThreadl(url, author); break;
272
- case 'pindl': result = await api.ShAnPindl(url, author); break;
273
- case 'capcutdl': result = await api.ShAnCapcutdl(url, author); break;
274
- case 'likeedl': result = await api.ShAnLikeedl(url, author); break;
275
- default: result = await api.ShAnAlldl(url, author);
276
- }
277
-
278
- console.log('\n\x1b[32m✅ SUCCESS!\x1b[0m\n');
279
- console.log(JSON.stringify(result, null, 2));
951
+ await actionFn();
280
952
  } catch (err) {
281
953
  console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
282
954
  }
283
-
284
- await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
285
955
  }
286
956
 
287
- async function searchMenu() {
957
+ // --------------------------------------------------------------
958
+ // RANDOM TEACH MODE - ShAnBrans returns question, user provides answer
959
+ // --------------------------------------------------------------
960
+ async function randomTeachMode(bot, author, uid, font) {
288
961
  console.clear();
289
962
  showLogo();
290
-
291
- const platform = await selectOption('🔍 Search On:', ['YouTube', 'TikTok']);
292
- const query = await askQuestion('\x1b[36mEnter search query\x1b[0m');
293
-
294
- if (!query) {
295
- console.log('\x1b[31m❌ Query is required!\x1b[0m');
296
- await askQuestion('\nPress Enter to continue...');
297
- return;
298
- }
299
-
300
- const defaultAuthor = getDefaultAuthor();
301
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
302
-
303
- console.log('\n\x1b[33m⏳ Searching...\x1b[0m\n');
304
-
305
- try {
306
- let result;
307
- if (platform === 'YouTube') {
308
- result = await api.ShAnytSearch(query, author);
309
- } else {
310
- 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`);
311
1039
  }
312
-
313
- console.log('\x1b[32m✅ Search Results:\x1b[0m\n');
314
- console.log(JSON.stringify(result, null, 2));
315
- } catch (err) {
316
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
317
1040
  }
318
-
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`);
319
1059
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
320
1060
  }
321
1061
 
1062
+ // --------------------------------------------------------------
1063
+ // AI CHATBOT MENU
1064
+ // --------------------------------------------------------------
322
1065
  async function aiChatbotMenu() {
323
1066
  console.clear();
324
1067
  showLogo();
325
-
1068
+
326
1069
  const bot = await selectOption('🤖 Select AI Chatbot:', ['Baby', 'Honey']);
327
- const action = await selectOption('Select Action:', ['Chat', 'Teach', 'Random Response', 'List Data', 'Edit', 'Delete']);
328
-
329
- 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();
330
1081
  const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
331
- const uid = await askQuestion('\x1b[36mEnter User ID (your unique identifier)\x1b[0m', 'user123');
332
- const font = await askQuestion('\x1b[36mEnter Font name (default: Arial)\x1b[0m', 'Arial');
333
-
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
+
334
1116
  try {
335
1117
  let result;
336
-
1118
+ let formattedResponse;
1119
+
337
1120
  if (action === 'Chat') {
338
- const text = await askQuestion('\x1b[36mEnter your message\x1b[0m');
339
- if (bot === 'Baby') {
340
- result = await api.ShAnBaby(text, uid, font, author);
341
- } else {
342
- result = await api.ShAnHoney(text, uid, font, author);
343
- }
344
- }
345
- else if (action === 'Teach') {
346
- const ask = await askQuestion('\x1b[36mEnter the question/phrase\x1b[0m');
347
- const ans = await askQuestion('\x1b[36mEnter the answer/response\x1b[0m');
348
- if (bot === 'Baby') {
349
- result = await api.ShAnBteach(ask, ans, uid, font, author);
350
- } else {
351
- 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`);
352
1150
  }
1151
+
1152
+ await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
1153
+ return;
353
1154
  }
1155
+
354
1156
  else if (action === 'Random Response') {
355
1157
  if (bot === 'Baby') {
356
1158
  result = await api.ShAnBrans(author);
1159
+ formattedResponse = formatResponse(result, 'baby');
1160
+ console.log(`\n\x1b[36m🎲 Random Baby Response:\x1b[0m ${formattedResponse}`);
357
1161
  } else {
358
1162
  console.log('\x1b[33m⚠️ Random response only available for Baby bot\x1b[0m');
359
- await askQuestion('\nPress Enter to continue...');
360
- return;
361
1163
  }
362
1164
  }
1165
+
363
1166
  else if (action === 'List Data') {
364
1167
  if (bot === 'Baby') {
365
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}`);
366
1171
  } else {
367
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}`);
368
1175
  }
369
1176
  }
1177
+
370
1178
  else if (action === 'Edit') {
371
1179
  const ask = await askQuestion('\x1b[36mEnter the question to edit\x1b[0m');
372
1180
  const newAsk = await askQuestion('\x1b[36mEnter the new question\x1b[0m');
373
1181
  const index = await askQuestion('\x1b[36mEnter index (optional)\x1b[0m');
1182
+
374
1183
  if (bot === 'Baby') {
375
1184
  result = await api.ShAnBedit(ask, newAsk, uid, font, author, index);
376
1185
  } else {
377
1186
  result = await api.ShAnHedit(ask, newAsk, uid, font, author, index);
378
1187
  }
1188
+ const formatted = formatResponse(result, 'teach');
1189
+ console.log(`\n\x1b[32m✅ ${formatted || 'Successfully edited!'}\x1b[0m`);
379
1190
  }
1191
+
380
1192
  else if (action === 'Delete') {
381
1193
  const text = await askQuestion('\x1b[36mEnter text to delete\x1b[0m');
382
1194
  const index = await askQuestion('\x1b[36mEnter index (optional)\x1b[0m');
1195
+
383
1196
  if (bot === 'Baby') {
384
1197
  result = await api.ShAnBdelete(text, uid, font, author, index);
385
1198
  } else {
386
1199
  result = await api.ShAnHdelete(text, uid, font, author, index);
387
1200
  }
1201
+ const formatted = formatResponse(result, 'teach');
1202
+ console.log(`\n\x1b[32m✅ ${formatted || 'Successfully deleted!'}\x1b[0m`);
388
1203
  }
389
-
390
- console.log('\x1b[32m✅ Success!\x1b[0m\n');
391
- console.log(JSON.stringify(result, null, 2));
1204
+
392
1205
  } catch (err) {
393
1206
  console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
394
1207
  }
395
-
1208
+
396
1209
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
397
1210
  }
398
1211
 
399
- async function albumMenu() {
1212
+ // --------------------------------------------------------------
1213
+ // BATCH DOWNLOAD
1214
+ // --------------------------------------------------------------
1215
+ async function batchDownloadMenu() {
400
1216
  console.clear();
401
1217
  showLogo();
402
-
403
- 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
+
404
1222
  const defaultAuthor = getDefaultAuthor();
405
1223
  const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
406
-
407
- try {
408
- let result;
409
-
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;
410
1523
  if (action === 'List Albums') {
411
1524
  result = await api.ShAnalbumList(author);
412
- }
413
- 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') {
414
1528
  const category = await askQuestion('\x1b[36mEnter category name\x1b[0m');
415
1529
  const videoUrl = await askQuestion('\x1b[36mEnter video URL\x1b[0m');
416
1530
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
417
1531
  result = await api.ShAnalbumAdd(category, videoUrl, senderID, author);
418
- }
419
- 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') {
420
1535
  const category = await askQuestion('\x1b[36mEnter category name\x1b[0m');
421
1536
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
422
1537
  const key = await askQuestion('\x1b[36mEnter access key (optional)\x1b[0m');
423
1538
  result = await api.ShAnalbumVideos(category, senderID, author, key);
424
- }
425
- 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') {
426
1542
  const url = await askQuestion('\x1b[36mEnter video URL to delete\x1b[0m');
427
1543
  const key = await askQuestion('\x1b[36mEnter access key\x1b[0m');
428
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`);
429
1547
  }
430
-
431
- console.log('\x1b[32m✅ Success!\x1b[0m\n');
432
- console.log(JSON.stringify(result, null, 2));
433
- } catch (err) {
434
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
435
- }
436
-
1548
+ });
437
1549
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
438
1550
  }
439
1551
 
1552
+ // --------------------------------------------------------------
1553
+ // FONT MENU
1554
+ // --------------------------------------------------------------
440
1555
  async function fontMenu() {
441
1556
  console.clear();
442
1557
  showLogo();
443
-
444
1558
  const action = await selectOption('🎨 Font Utilities:', ['Apply Font to Text', 'List Available Fonts', 'Generate Wish Card']);
445
- const defaultAuthor = getDefaultAuthor();
446
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
447
-
448
- try {
1559
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1560
+ await runSafely(async () => {
449
1561
  if (action === 'List Available Fonts') {
450
1562
  const result = await api.ShAnfontList(author);
451
- console.log('\n\x1b[32m✅ Available Fonts:\x1b[0m\n');
452
- console.log(JSON.stringify(result, null, 2));
453
- }
454
- 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') {
455
1566
  const text = await askQuestion('\x1b[36mEnter your text\x1b[0m');
456
1567
  const font = await askQuestion('\x1b[36mEnter font name\x1b[0m', 'Arial');
457
1568
  const result = await api.ShAnFont(text, font, author);
458
- console.log('\x1b[32m✅ Formatted Text:\x1b[0m\n');
459
- console.log(result);
460
- }
461
- 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') {
462
1572
  const name = await askQuestion('\x1b[36mEnter name for wish card\x1b[0m');
463
1573
  const font = await askQuestion('\x1b[36mEnter font name\x1b[0m', 'Arial');
464
1574
  const result = await api.ShAnWish(name, font, author);
465
- console.log('\x1b[32m✅ Wish Card Generated:\x1b[0m\n');
466
- 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));
467
1578
  }
468
- } catch (err) {
469
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
470
- }
471
-
1579
+ });
472
1580
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
473
1581
  }
474
1582
 
1583
+ // --------------------------------------------------------------
1584
+ // CAPTION MENU
1585
+ // --------------------------------------------------------------
475
1586
  async function captionMenu() {
476
1587
  console.clear();
477
1588
  showLogo();
478
-
479
1589
  const action = await selectOption('📝 Caption Manager:', ['Add Caption', 'Get Caption', 'List Captions']);
480
- const defaultAuthor = getDefaultAuthor();
481
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
482
-
483
- try {
1590
+ const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', getDefaultAuthor());
1591
+ await runSafely(async () => {
484
1592
  if (action === 'Add Caption') {
485
1593
  const category = await askQuestion('\x1b[36mEnter category\x1b[0m');
486
1594
  const language = await askQuestion('\x1b[36mEnter language (e.g., en, es, hi)\x1b[0m');
487
1595
  const caption = await askQuestion('\x1b[36mEnter caption text\x1b[0m');
488
1596
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
489
1597
  const result = await api.ShAncaptionAdd(category, language, caption, senderID, author);
490
- console.log('\x1b[32m✅ Caption Added!\x1b[0m\n');
491
- console.log(JSON.stringify(result, null, 2));
492
- }
493
- 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') {
494
1601
  const language = await askQuestion('\x1b[36mEnter language\x1b[0m', 'en');
495
1602
  const result = await api.ShAncaptionList(language, author);
496
- console.log('\x1b[32m✅ Captions List:\x1b[0m\n');
497
- console.log(JSON.stringify(result, null, 2));
498
- }
499
- 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') {
500
1606
  const category = await askQuestion('\x1b[36mEnter category\x1b[0m');
501
1607
  const language = await askQuestion('\x1b[36mEnter language\x1b[0m');
502
1608
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
503
1609
  const key = await askQuestion('\x1b[36mEnter access key (optional)\x1b[0m');
504
1610
  const result = await api.ShAnCaption(category, language, senderID, author, key);
505
- console.log('\x1b[32m✅ Caption:\x1b[0m\n');
506
- 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));
507
1614
  }
508
- } catch (err) {
509
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
510
- }
511
-
1615
+ });
512
1616
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
513
1617
  }
514
1618
 
1619
+ // --------------------------------------------------------------
1620
+ // MEME MENU
1621
+ // --------------------------------------------------------------
515
1622
  async function memeMenu() {
516
1623
  console.clear();
517
1624
  showLogo();
518
-
519
- const action = await selectOption('🖼️ Meme Generator:', ['Get Random Meme', 'Add New Meme']);
520
- const defaultAuthor = getDefaultAuthor();
521
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
522
-
523
- 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 () => {
524
1628
  if (action === 'Get Random Meme') {
525
1629
  const result = await api.ShAnMeme(author);
526
- console.log('\x1b[32m✅ Random Meme:\x1b[0m\n');
527
- console.log(JSON.stringify(result, null, 2));
528
- }
529
- 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') {
530
1641
  const memeUrl = await askQuestion('\x1b[36mEnter meme image/video URL\x1b[0m');
531
1642
  const senderID = await askQuestion('\x1b[36mEnter sender ID\x1b[0m');
532
1643
  const result = await api.ShAnmemeAdd(memeUrl, senderID, author);
533
- console.log('\x1b[32m✅ Meme Added!\x1b[0m\n');
534
- 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`);
535
1646
  }
536
- } catch (err) {
537
- console.error('\x1b[31m❌ Error:\x1b[0m', err.response?.data || err.message);
538
- }
539
-
1647
+ });
540
1648
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
541
1649
  }
542
1650
 
1651
+ // --------------------------------------------------------------
1652
+ // CLOUD MENU
1653
+ // --------------------------------------------------------------
543
1654
  async function cloudMenu() {
544
1655
  console.clear();
545
1656
  showLogo();
546
-
547
- const platform = await selectOption('☁️ Upload To:', ['Imgur', 'ImgBB']);
1657
+ const platform = await selectOption('☁️ Upload To:', ['Imgur', 'ImgBB']);
548
1658
  const url = await askQuestion('\x1b[36mEnter media URL to upload\x1b[0m');
549
- const defaultAuthor = getDefaultAuthor();
550
- const author = await askQuestion('\x1b[36mEnter your name/author ID\x1b[0m', defaultAuthor);
551
-
552
- if (!url) {
553
- console.log('\x1b[31m❌ URL is required!\x1b[0m');
554
- await askQuestion('\nPress Enter to continue...');
555
- return;
556
- }
557
-
558
- console.log('\n\x1b[33m⏳ Uploading...\x1b[0m\n');
559
-
560
- try {
561
- let result;
562
- if (platform === 'Imgur') {
563
- result = await api.ShAnImgur(url, author);
564
- } else {
565
- 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));
566
1672
  }
567
-
568
- console.log('\x1b[32m✅ Upload Successful!\x1b[0m\n');
569
- console.log(JSON.stringify(result, null, 2));
570
- } catch (err) {
571
- 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...');
572
1716
  }
573
-
574
1717
  await askQuestion('\n\x1b[36mPress Enter to continue...\x1b[0m');
575
1718
  }
576
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
+ // --------------------------------------------------------------
577
1752
  async function main() {
578
1753
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
579
1754
  showLogo();
580
1755
  console.log(`
581
- \x1b[36mSHAN SERVER - Interactive Media Downloader & Utilities\x1b[0m
1756
+ \x1b[36mSHAN SERVER - Ultimate Media Downloader & Utilities v4.0\x1b[0m
582
1757
 
583
1758
  \x1b[33mUSAGE:\x1b[0m
584
1759
  shan Start interactive menu
585
1760
  shan --help Show this help
586
1761
  shan --version Show version
587
1762
 
588
- \x1b[33mAUTO-AUTHOR (CLI ONLY):\x1b[0m
589
- The CLI automatically detects your system username and uses it as the default author.
590
- If no username is found, it defaults to: \x1b[36m♡︎ 𝗦𝗵𝗔𝗻 ♡︎\x1b[0m
591
-
592
- \x1b[33mCODE USAGE:\x1b[0m
593
- When using in code, author is \x1b[31mREQUIRED\x1b[0m:
594
- const shan = require('shan-server');
595
- 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)
596
1770
 
597
- \x1b[33mDESCRIPTION:\x1b[0m
598
- An interactive CLI tool for downloading videos from YouTube, TikTok,
599
- Instagram, Facebook, Twitter, and more. Also includes AI chatbots,
600
- 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.
601
1773
 
602
- \x1b[33mSUPPORTED PLATFORMS:\x1b[0m
603
- Windows (CMD, PowerShell, Terminal)
604
- Linux (bash, zsh, fish)
605
- macOS (Terminal, iTerm2)
606
- Termux (Android)
607
- Chrome OS
608
- 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
609
1796
 
610
- \x1b[33mREQUIREMENTS:\x1b[0m
611
- 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
612
1803
 
613
1804
  \x1b[33mEXAMPLES:\x1b[0m
614
1805
  $ shan # Start interactive menu
615
1806
  $ npx shan-server # Run without installing
616
- $ node -e "require('shan-server').ShAnYtdl('url', '♡︎ 𝗦𝗵𝗔𝗻 ♡︎')"
617
1807
 
618
1808
  \x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
619
1809
  `);
620
1810
  process.exit(0);
621
1811
  }
622
-
1812
+
623
1813
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
624
- console.log('shan-server v1.0.4');
625
- 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}`);
626
1819
  process.exit(0);
627
1820
  }
628
-
1821
+
629
1822
  while (true) {
630
1823
  const choice = await showMainMenu();
631
-
632
1824
  switch(choice) {
633
- case '1':
634
- await downloadMenu();
635
- break;
636
- case '2':
637
- await searchMenu();
638
- break;
639
- case '3':
640
- await aiChatbotMenu();
641
- break;
642
- case '4':
643
- await albumMenu();
644
- break;
645
- case '5':
646
- await fontMenu();
647
- break;
648
- case '6':
649
- await captionMenu();
650
- break;
651
- case '7':
652
- await memeMenu();
653
- break;
654
- case '8':
655
- await cloudMenu();
656
- 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;
657
1836
  case '0':
658
1837
  console.log('\n\x1b[36m👋 Thank you for using SHAN SERVER!\x1b[0m\n');
1838
+ console.log(`📁 Downloads saved in: ${downloadManager.downloadDir}`);
659
1839
  process.exit(0);
660
1840
  default:
661
1841
  console.log('\x1b[31m❌ Invalid option! Please try again.\x1b[0m');
@@ -665,14 +1845,24 @@ async function main() {
665
1845
  }
666
1846
 
667
1847
  process.on('SIGINT', () => {
668
- console.log('\n\n\x1b[36m👋 Goodbye from SHAN SERVER!\x1b[0m\n');
669
- 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);
670
1857
  });
671
1858
 
672
1859
  if (require.main === module) {
673
1860
  main().catch(console.error);
674
1861
  }
675
1862
 
1863
+ // --------------------------------------------------------------
1864
+ // EXPORT
1865
+ // --------------------------------------------------------------
676
1866
  module.exports = {
677
1867
  ShAnAlldl: api.ShAnAlldl,
678
1868
  ShAnAlldl2: api.ShAnAlldl2,
@@ -689,6 +1879,7 @@ module.exports = {
689
1879
  ShAnCapcutdl: api.ShAnCapcutdl,
690
1880
  ShAnLikeedl: api.ShAnLikeedl,
691
1881
  ShAnytSearch: api.ShAnytSearch,
1882
+ ShAntikSearch: api.ShAntikSearch,
692
1883
  ShAnBaby: api.ShAnBaby,
693
1884
  ShAnBteach: api.ShAnBteach,
694
1885
  ShAnBrans: api.ShAnBrans,
@@ -707,7 +1898,7 @@ module.exports = {
707
1898
  ShAnalbumDelete: api.ShAnalbumDelete,
708
1899
  ShAnalbumList: api.ShAnalbumList,
709
1900
  ShAnImgur: api.ShAnImgur,
710
- ShAntikSearch: api.ShAntikSearch,
1901
+ ShAnImgbb: api.ShAnImgbb,
711
1902
  ShAnFont: api.ShAnFont,
712
1903
  ShAnfontList: api.ShAnfontList,
713
1904
  ShAnWish: api.ShAnWish,
@@ -716,6 +1907,15 @@ module.exports = {
716
1907
  ShAnCaption: api.ShAnCaption,
717
1908
  ShAnmemeAdd: api.ShAnmemeAdd,
718
1909
  ShAnMeme: api.ShAnMeme,
719
- ShAnImgbb: api.ShAnImgbb,
720
- getDefaultAuthor
1910
+ downloadManager,
1911
+ AITeachingSession,
1912
+ getDefaultAuthor,
1913
+ detectPlatform,
1914
+ isValidHttpUrl,
1915
+ withRetry,
1916
+ asyncPool,
1917
+ logEvent,
1918
+ loadPersistedConfig,
1919
+ savePersistedConfig,
1920
+ CONFIG
721
1921
  };