media-dl 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,17 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawn, execSync } = require('child_process');
4
- const readline = require('readline');
5
4
  const path = require('path');
6
5
  const fs = require('fs');
7
6
  const os = require('os');
7
+ const { C, printHeader, renderProgressBar, askQuestion, rl } = require('./ui');
8
8
 
9
- const rl = readline.createInterface({
10
- input: process.stdin,
11
- output: process.stdout,
12
- });
9
+ // --- KONFIGURASI VISUAL (ANSI COLORS) ---
13
10
 
14
- // Penentuan folder Tools berdasarkan OS
15
11
  const TOOLS_DIR = path.join(os.homedir(), '.media-dl');
16
12
  const isWindows = process.platform === 'win32';
17
13
  const isMac = process.platform === 'darwin';
@@ -19,11 +15,10 @@ const isMac = process.platform === 'darwin';
19
15
  const YTDLP_PATH = path.join(TOOLS_DIR, isWindows ? 'yt-dlp.exe' : 'yt-dlp');
20
16
  const FFMPEG_PATH = path.join(TOOLS_DIR, isWindows ? 'ffmpeg.exe' : 'ffmpeg');
21
17
 
22
- if (!fs.existsSync(TOOLS_DIR)) fs.mkdirSync(TOOLS_DIR, { recursive: true });
18
+ // State Aplikasi
19
+ let safeMode = true;
23
20
 
24
- function askQuestion(query) {
25
- return new Promise((resolve) => rl.question(query, resolve));
26
- }
21
+ if (!fs.existsSync(TOOLS_DIR)) fs.mkdirSync(TOOLS_DIR, { recursive: true });
27
22
 
28
23
  function checkTools() {
29
24
  return {
@@ -32,8 +27,10 @@ function checkTools() {
32
27
  };
33
28
  }
34
29
 
30
+ // --- INSTALLERS ---
35
31
  async function installYtdlp() {
36
- console.log('\n⏳ Mengunduh yt-dlp...');
32
+ printHeader('INSTALL / UPDATE YT-DLP');
33
+ console.log(`${C.blue}⏳ Sedang mengunduh engine terbaru...${C.reset}`);
37
34
  const url = isWindows
38
35
  ? 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'
39
36
  : 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
@@ -47,16 +44,23 @@ async function installYtdlp() {
47
44
  execSync(`curl -L -# "${url}" -o "${YTDLP_PATH}"`, { stdio: 'inherit' });
48
45
  execSync(`chmod a+rx "${YTDLP_PATH}"`);
49
46
  }
50
- console.log('✅ yt-dlp berhasil diinstal.');
47
+ console.log(`\n${C.green}✅ yt-dlp berhasil dikonfigurasi!${C.reset}`);
51
48
  } catch (e) {
52
- console.error('❌ Gagal mengunduh yt-dlp.');
49
+ console.error(
50
+ `\n${C.red}❌ Gagal mengunduh. Periksa koneksi internet Anda.${C.reset}`
51
+ );
53
52
  }
54
53
  }
55
54
 
56
55
  async function installFfmpeg() {
57
- console.log('\n⏳ Mengunduh FFmpeg (Diperlukan untuk konversi)...');
56
+ printHeader('INSTALL FFmpeg');
57
+ console.log(
58
+ `${C.dim}FFmpeg diperlukan untuk kualitas 1080p+ dan konversi MP3.${C.reset}\n`
59
+ );
60
+
58
61
  try {
59
62
  if (isMac) {
63
+ console.log(`${C.blue}⏳ Mengunduh FFmpeg untuk macOS...${C.reset}`);
60
64
  const zipPath = path.join(TOOLS_DIR, 'ffmpeg.zip');
61
65
  execSync(
62
66
  `curl -L -# "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o "${zipPath}"`,
@@ -65,47 +69,86 @@ async function installFfmpeg() {
65
69
  execSync(`unzip -o "${zipPath}" -d "${TOOLS_DIR}"`, { stdio: 'inherit' });
66
70
  execSync(`rm "${zipPath}"`);
67
71
  execSync(`chmod a+rx "${FFMPEG_PATH}"`);
68
- console.log('✅ FFmpeg berhasil disiapkan.');
72
+ console.log(`\n${C.green}✅ FFmpeg aktif di macOS.${C.reset}`);
73
+ } else if (isWindows) {
74
+ console.log(
75
+ `${C.yellow}ℹ️ Untuk Windows, disarankan mengunduh 'ffmpeg.exe' secara manual`
76
+ );
77
+ console.log(`dan letakkan di: ${C.white}${TOOLS_DIR}${C.reset}`);
69
78
  }
70
79
  } catch (e) {
71
- console.error('❌ Gagal instal FFmpeg.');
80
+ console.error(
81
+ `${C.red}❌ Gagal menginstal FFmpeg secara otomatis.${C.reset}`
82
+ );
72
83
  }
73
84
  }
74
85
 
75
- function parseSelection(input, max) {
76
- if (!input || input.toLowerCase() === 'all') return null;
77
- const selected = new Set();
78
- const parts = input.split(',');
79
- parts.forEach((part) => {
80
- if (part.includes('-')) {
81
- const [start, end] = part.split('-').map(Number);
82
- for (let i = start; i <= end; i++) if (i > 0 && i <= max) selected.add(i);
83
- } else {
84
- const num = parseInt(part.trim());
85
- if (num > 0 && num <= max) selected.add(num);
86
- }
86
+ function runSpawn(command, args) {
87
+ return new Promise((resolve) => {
88
+ const proc = spawn(command, args);
89
+ let lastOutput = '';
90
+
91
+ proc.stdout.on('data', (data) => {
92
+ const output = data.toString();
93
+ // Regex untuk menangkap progress dari yt-dlp
94
+ const progressMatch = output.match(
95
+ /\[download\]\s+(\d+\.\d+)%\s+of\s+.*\s+at\s+([\d\w\./s]+)\s+ETA\s+([\d:]+)/
96
+ );
97
+
98
+ if (progressMatch) {
99
+ const [_, percent, speed, eta] = progressMatch;
100
+ renderProgressBar(parseFloat(percent), speed, eta);
101
+ } else {
102
+ // Jika bukan bar, print normal (misal: info merging/ffmpeg)
103
+ if (output.trim() && !output.includes('[download]')) {
104
+ process.stdout.write(`\n${C.dim}${output.trim()}${C.reset}\n`);
105
+ }
106
+ }
107
+ });
108
+
109
+ proc.stderr.on('data', (data) => {
110
+ const err = data.toString();
111
+ if (!err.includes('WARNING')) {
112
+ process.stdout.write(`\n${C.red}⚠️ ${err}${C.reset}`);
113
+ }
114
+ });
115
+
116
+ proc.on('close', (code) => {
117
+ process.stdout.write('\n'); // Baris baru setelah selesai
118
+ resolve(code);
119
+ });
87
120
  });
88
- return Array.from(selected).join(',');
89
121
  }
90
122
 
123
+ // --- DOWNLOAD ENGINE ---
91
124
  async function startDownload() {
92
125
  let { ytExists, ffExists } = checkTools();
93
126
  if (!ytExists) {
94
- console.log('\n⚠️ yt-dlp belum terpasang.');
95
- const ans = await askQuestion('Instal sekarang? (y/n): ');
96
- if (ans.toLowerCase() === 'y') await installYtdlp();
97
- if (!fs.existsSync(YTDLP_PATH)) return mainMenu();
127
+ console.log(
128
+ `\n${C.red}❌ Engine yt-dlp tidak ditemukan. Silakan pilih menu Update/Install.${C.reset}`
129
+ );
130
+ await askQuestion('Tekan Enter...');
131
+ return mainMenu();
132
+ }
133
+
134
+ if (!ffExists) {
135
+ console.log(
136
+ `${C.yellow}⚠️ Peringatan: FFmpeg tidak ditemukan. Video mungkin tidak tergabung dengan audio.${C.reset}`
137
+ );
138
+ const cont = await askQuestion('Lanjutkan saja? (y/n): ');
139
+ if (cont.toLowerCase() !== 'y') return mainMenu();
98
140
  }
99
141
 
100
- const videoURL = await askQuestion('\n🔗 Masukkan Link (Video/Playlist): ');
142
+ const videoURL = await askQuestion('Masukkan Link (Video/Playlist): ');
143
+ if (!videoURL) return mainMenu();
101
144
 
102
- console.log('Mengecek link...');
145
+ console.log(`${C.dim}Menganalisa tautan...${C.reset}`);
103
146
  let playlistInfo = { isPlaylist: false, title: '', items: [] };
104
147
 
105
148
  try {
106
149
  const rawInfo = execSync(
107
- `"${YTDLP_PATH}" --flat-playlist --print "%(playlist_title)s|%(index)s. %(title)s" "${videoURL}"`,
108
- { encoding: 'utf-8' }
150
+ `"${YTDLP_PATH}" --flat-playlist --print "%(playlist_title)s|%(title)s" "${videoURL}"`,
151
+ { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }
109
152
  );
110
153
  const lines = rawInfo.trim().split('\n');
111
154
  if (lines.length > 1 || videoURL.includes('playlist?list=')) {
@@ -117,149 +160,410 @@ async function startDownload() {
117
160
 
118
161
  let playlistSelection = null;
119
162
  if (playlistInfo.isPlaylist) {
120
- console.log(`\n📂 PLAYLIST TERDETEKSI: ${playlistInfo.title}`);
121
- playlistInfo.items.forEach((item) => console.log(item));
122
- const selectionInput = await askQuestion(
123
- '\nPilih nomor video (contoh: 1,3,5-10) atau tekan Enter untuk semua: '
163
+ console.log(
164
+ `\n${C.bgBlue}${C.bright} 📂 PLAYLIST TERDETEKSI: ${playlistInfo.title} ${C.reset}`
124
165
  );
125
- playlistSelection = parseSelection(
126
- selectionInput,
127
- playlistInfo.items.length
166
+ playlistInfo.items.forEach((item, index) => {
167
+ console.log(
168
+ `${C.cyan}${(index + 1).toString().padStart(3, ' ')}.${C.reset} ${item}`
169
+ );
170
+ });
171
+ console.log(
172
+ `\n${C.dim}Contoh pilih nomor: 1,3,5-10 atau biarkan kosong untuk semua.${C.reset}`
128
173
  );
174
+ const selectionInput = await askQuestion('Pilih nomor: ');
175
+
176
+ // Parsing nomor playlist
177
+ if (selectionInput) {
178
+ const selected = new Set();
179
+ selectionInput.split(',').forEach((p) => {
180
+ if (p.includes('-')) {
181
+ const [s, e] = p.split('-').map(Number);
182
+ for (let i = s; i <= e; i++)
183
+ if (i > 0 && i <= playlistInfo.items.length) selected.add(i);
184
+ } else {
185
+ const n = parseInt(p.trim());
186
+ if (n > 0 && n <= playlistInfo.items.length) selected.add(n);
187
+ }
188
+ });
189
+ playlistSelection = Array.from(selected).join(',');
190
+ }
129
191
  }
130
192
 
131
- console.log('\n[PILIH FORMAT]');
132
- console.log('1. Video MP4 (Sangat Kompatibel Mac/QuickTime)');
133
- console.log('2. Audio (MP3) ' + (ffExists ? '✅' : '❌ (Butuh FFmpeg)'));
193
+ console.log(`\n${C.bright} [ PILIH FORMAT ]${C.reset}`);
194
+ console.log(` ${C.green}1.${C.reset} Video (Kualitas Terbaik/MP4)`);
195
+ console.log(
196
+ ` ${C.green}2.${C.reset} Audio Only (MP3) ${
197
+ ffExists ? C.green + '✅' : C.red + '❌ (Butuh FFmpeg)'
198
+ }`
199
+ );
134
200
  const mode = await askQuestion('Pilihan: ');
135
201
 
136
202
  const baseDir = path.join(os.homedir(), 'Downloads', 'media-dl');
137
203
  const subFolder = mode === '2' ? 'audio' : 'video';
138
- let finalOutputDir = path.join(baseDir, subFolder);
139
-
140
- if (playlistInfo.isPlaylist) {
141
- const folderName = playlistInfo.title.replace(/[\\/:"*?<>|]/g, '_');
142
- finalOutputDir = path.join(finalOutputDir, folderName);
143
- }
144
-
145
- if (!fs.existsSync(finalOutputDir))
146
- fs.mkdirSync(finalOutputDir, { recursive: true });
204
+ let outputDir = path.join(baseDir, subFolder);
205
+ if (playlistInfo.isPlaylist)
206
+ outputDir = path.join(
207
+ outputDir,
208
+ playlistInfo.title.replace(/[\\/:"*?<>|]/g, '_')
209
+ );
210
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
147
211
 
148
- const qtFormat =
149
- 'bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[vcodec^=avc1]/best';
150
212
  let args = [
151
213
  '--ffmpeg-location',
152
214
  FFMPEG_PATH,
153
215
  '-o',
154
- `${finalOutputDir}/%(title).100s.%(ext)s`,
216
+ `${outputDir}/%(title).100s.%(ext)s`,
155
217
  videoURL,
156
218
  ];
157
219
 
158
- if (playlistSelection) {
159
- args.push('--playlist-items', playlistSelection);
160
- } else if (!playlistInfo.isPlaylist) {
161
- args.push('--no-playlist');
220
+ // Integrasi Safe Mode
221
+ if (safeMode) {
222
+ args.push(
223
+ '--ratelimit',
224
+ '5M',
225
+ '--sleep-interval',
226
+ '3',
227
+ '--max-sleep-interval',
228
+ '10',
229
+ '--user-agent',
230
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
231
+ );
162
232
  }
163
233
 
234
+ if (playlistSelection) args.push('--playlist-items', playlistSelection);
235
+ else if (!playlistInfo.isPlaylist) args.push('--no-playlist');
236
+
164
237
  if (mode === '2') {
165
238
  if (!ffExists) {
166
- console.log('❌ Butuh FFmpeg.');
239
+ console.log(
240
+ `${C.red}❌ Error: Anda wajib menginstal FFmpeg untuk mengunduh audio.${C.reset}`
241
+ );
242
+ await askQuestion('Kembali...');
167
243
  return mainMenu();
168
244
  }
169
245
  args.unshift('-x', '--audio-format', 'mp3');
170
246
  } else {
171
- console.log('\n[PILIH KUALITAS]');
172
- console.log('1. Terbaik (Auto-Conversion ke MP4)');
173
- console.log('2. 1080p');
174
- console.log('3. 720p');
175
- const res = await askQuestion('Pilih: ');
176
-
177
- let fCode = qtFormat;
178
- if (res === '2')
179
- fCode =
180
- 'bestvideo[height<=1080][vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[height<=1080]/best';
181
- if (res === '3')
182
- fCode =
183
- 'bestvideo[height<=720][vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[height<=720]/best';
184
-
185
- args.unshift('-f', fCode);
247
+ args.unshift(
248
+ '-f',
249
+ 'bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[vcodec^=avc1]/best'
250
+ );
186
251
  if (ffExists) args.unshift('--recode-video', 'mp4');
187
252
  }
188
253
 
189
- console.log('\n🚀 Memulai proses unduhan...');
190
- const download = spawn(YTDLP_PATH, args);
254
+ args.push('--no-mtime');
191
255
 
192
- download.stdout.on('data', (data) => process.stdout.write(data));
193
- download.stderr.on('data', (data) => process.stderr.write(data));
256
+ console.log(`\n${C.bgBlue}${C.bright} 🚀 MEMULAI PROSES... ${C.reset}\n`);
194
257
 
195
- download.on('close', (code) => {
196
- if (code === 0) {
197
- console.log(`\n✅ BERHASIL! File disimpan di: ${finalOutputDir}`);
258
+ const code = await runSpawn(YTDLP_PATH, args); // Menunggu sampai benar-benar selesai
259
+
260
+ if (code === 0) {
261
+ console.log(`\n${C.green}✨ SELESAI! Cek folder: ${outputDir}${C.reset}`);
262
+ // Auto-open folder
263
+ try {
198
264
  execSync(
199
- isMac ? `open "${finalOutputDir}"` : `explorer "${finalOutputDir}"`
265
+ isWindows
266
+ ? `explorer "${outputDir}"`
267
+ : isMac
268
+ ? `open "${outputDir}"`
269
+ : `xdg-open "${outputDir}"`
200
270
  );
201
- } else {
202
- console.log('\n❌ Gagal.');
203
- }
204
- mainMenu();
205
- });
271
+ } catch (e) {}
272
+ } else {
273
+ console.log(`\n${C.red}❌ Terjadi kesalahan saat mengunduh.${C.reset}`);
274
+ }
206
275
  }
207
276
 
208
277
  async function showSupport() {
209
- console.log('\n====================================');
210
- console.log(' ❤️ SUPPORT DEVELOPER ');
211
- console.log('====================================');
212
- console.log('Terima kasih telah menggunakan MEDIA-DL!');
213
- console.log('Dukungan Anda sangat berarti bagi pengembangan skrip ini.');
214
- console.log(
215
- '\n* Beli Kopi: https://app.midtrans.com/payment-links/coffee-developer'
216
- );
217
- console.log(
218
- '* 🍕 Beli Pizza: https://app.midtrans.com/payment-links/pizza-developer'
219
- );
220
- console.log('------------------------------------');
221
- await askQuestion('\nTekan Enter untuk kembali ke Menu Utama...');
278
+ // Menggunakan 2 parameter: Judul dan Summary
279
+ printHeader('TENTANG APLIKASI', 'Media-DL Manager Pro v2.0.0 - 2026');
280
+
281
+ // --- SEKSI FITUR ---
282
+ console.log(` ${C.bright}${C.cyan}OVERVIEW${C.reset}`);
283
+ console.log(` Terima kasih telah memilih MEDIA-DL. Skrip ini dirancang`);
284
+ console.log(` untuk memudahkan manajemen unduhan media secara lokal.\n`);
285
+
286
+ console.log(` ${C.bright}${C.cyan}FITUR UNGGULAN${C.reset}`);
287
+ const features = [
288
+ {
289
+ icon: '',
290
+ title: 'High Quality',
291
+ desc: 'Mendukung hingga 4K & Audio 320kbps',
292
+ },
293
+ {
294
+ icon: '✦',
295
+ title: 'Multi-Source',
296
+ desc: 'YouTube, TikTok, IG Reels, & Shorts',
297
+ },
298
+ {
299
+ icon: '✦',
300
+ title: 'Batch Mode',
301
+ desc: 'Mendukung unduhan Playlist secara massal',
302
+ },
303
+ {
304
+ icon: '✦',
305
+ title: 'Safe Guard',
306
+ desc: 'Mode proteksi agar akun/IP tidak terblokir',
307
+ },
308
+ ];
309
+
310
+ features.forEach((f) => {
311
+ console.log(
312
+ ` ${C.green}${f.icon}${C.reset} ${C.white}${f.title.padEnd(15)}${
313
+ C.reset
314
+ } ${C.dim}• ${f.desc}${C.reset}`
315
+ );
316
+ });
317
+
318
+ console.log('\n' + '─'.repeat(52));
319
+
320
+ // --- SEKSI DUKUNGAN ---
321
+ console.log(`\n ${C.bright}${C.magenta}DUKUNGAN & DONASI${C.reset}`);
322
+ console.log(` Dukungan Anda sangat membantu pengembang untuk terus`);
323
+ console.log(` memperbarui engine dan fitur aplikasi ini.\n`);
324
+
325
+ // Menampilkan Link dengan label background agar menonjol
326
+ const links = [
327
+ { label: ' ☕ BELI KOPI ', url: 'https://app.midtrans.com/coffee' },
328
+ { label: ' 🍕 BELI PIZZA', url: 'https://app.midtrans.com/pizza' },
329
+ ];
330
+
331
+ links.forEach((l) => {
332
+ console.log(
333
+ ` ${C.bgBlue}${C.white}${l.label}${C.reset} ${C.blue}➜${C.reset} ${C.dim}${l.url}${C.reset}`
334
+ );
335
+ });
336
+
337
+ console.log(`\n${C.cyan}${'━'.repeat(52)}${C.reset}`);
338
+
339
+ await askQuestion('Tekan Enter untuk kembali ke Menu Utama...');
222
340
  mainMenu();
223
341
  }
224
342
 
225
343
  async function mainMenu() {
226
344
  const { ytExists, ffExists } = checkTools();
227
- console.log('\n====================================');
228
- console.log(' MEDIA-DL MANAGER 2026 ');
229
- console.log('====================================');
345
+
346
+ // Menggunakan 2 parameter: Judul dan Summary status singkat
347
+ printHeader('MEDIA-DL PRO 2026', 'Pusat Kendali Unduhan Media Lokal');
348
+
349
+ // --- SEKSI DASHBOARD (INFO SISTEM) ---
350
+ const statusYt = ytExists
351
+ ? `${C.green}Ready${C.reset}`
352
+ : `${C.red}Not Found${C.reset}`;
353
+ const statusFf = ffExists
354
+ ? `${C.green}Ready${C.reset}`
355
+ : `${C.yellow}Missing${C.reset}`;
356
+ const safeBadge = safeMode
357
+ ? `${C.bgBlue}${C.white} ON ${C.reset}`
358
+ : `${C.bgRed}${C.white} OFF ${C.reset}`;
359
+
360
+ console.log(` ${C.bright}SYSTEM STATUS${C.reset}`);
361
+ console.log(` 🤖 Engine : [ ${statusYt} ] | 🎬 FFmpeg : [ ${statusFf} ]`);
362
+ console.log(` 🛡️ Safe Mode Guard : ${safeBadge}\n`);
363
+
364
+ console.log(` ${C.cyan}━${'━'.repeat(48)}${C.reset}`);
365
+
366
+ // --- SEKSI NAVIGASI ---
367
+ console.log(` ${C.bright}MAIN SERVICES${C.reset}`);
230
368
  console.log(
231
- ` OS : ${process.platform} | yt-dlp: ${
232
- ytExists ? '✅' : '❌'
233
- } | ffmpeg: ${ffExists ? '✅' : '❌'}`
369
+ ` ${C.cyan}1.${C.reset} 📥 Download Media ${C.dim}(Video, Music, Playlist)${C.reset}`
234
370
  );
235
- console.log('------------------------------------');
236
- console.log(' 1. 📥 Download Media (Single/Playlist)');
237
- console.log(' 2. ⚙️ Update yt-dlp');
238
- console.log(' 3. 🔨 Instal FFmpeg (macOS)');
239
- console.log(' 4. ❤️ Support Developer');
240
- console.log(' 5. 🗑️ Uninstall & Hapus Data');
241
- console.log(' 6. 🚪 Keluar');
242
-
243
- const choice = await askQuestion('\nPilih menu: ');
244
- if (choice === '1') await startDownload();
245
- else if (choice === '2') {
246
- await installYtdlp();
247
- mainMenu();
248
- } else if (choice === '3') {
249
- await installFfmpeg();
250
- mainMenu();
251
- } else if (choice === '4') await showSupport();
252
- else if (choice === '5') {
253
- const confirm = await askQuestion('Hapus semua? (y/n): ');
254
- if (confirm.toLowerCase() === 'y') {
255
- fs.rmSync(TOOLS_DIR, { recursive: true, force: true });
256
- console.log('✅ Folder tools dibersihkan.');
371
+ console.log(
372
+ ` ${C.cyan}2.${C.reset} 🛡️ Toggle Safe Mode ${C.dim}(Sekarang: ${
373
+ safeMode ? 'Aktif' : 'Nonaktif'
374
+ })${C.reset}`
375
+ );
376
+
377
+ console.log(`\n ${C.bright}SYSTEM & INFO${C.reset}`);
378
+ console.log(
379
+ ` ${C.cyan}3.${C.reset} ⚙️ Maintenance & Update ${C.dim}(Update engine / Cleanup)${C.reset}`
380
+ );
381
+ console.log(
382
+ ` ${C.cyan}4.${C.reset} ❤️ Tentang Aplikasi ${C.dim}(Dukungan & Fitur)${C.reset}`
383
+ );
384
+ console.log(` ${C.cyan}0.${C.reset} 🚪 Keluar`);
385
+
386
+ console.log(` ${C.cyan}━${'━'.repeat(48)}${C.reset}`);
387
+
388
+ const choice = await askQuestion('\nPilih menu (0-4): ');
389
+
390
+ switch (choice) {
391
+ case '1':
392
+ await startDownload();
393
+ break;
394
+ case '2':
395
+ safeMode = !safeMode;
396
+ // Berikan feedback visual singkat sebelum refresh menu
397
+ console.log(
398
+ `\n${C.yellow} 🛡️ Safe Mode telah ${
399
+ safeMode ? 'DIAKTIFKAN' : 'DINONAKTIFKAN'
400
+ }${C.reset}`
401
+ );
402
+ setTimeout(() => mainMenu(), 800);
403
+ break;
404
+ case '3':
405
+ await systemMaintenance();
406
+ break;
407
+ case '4':
408
+ await showSupport();
409
+ break;
410
+ case '0':
411
+ console.log(`\n${C.cyan}━${'━'.repeat(48)}${C.reset}`);
412
+ console.log(
413
+ ` ${C.bright}${C.white}Terima kasih telah menggunakan MEDIA-DL!${C.reset}`
414
+ );
415
+ console.log(
416
+ ` ${C.green}✨ Semoga Anda sukses, jaya, dan sehat selalu! ✨${C.reset}`
417
+ );
418
+ console.log(`${C.cyan}━${'━'.repeat(48)}${C.reset}\n`);
419
+
420
+ // Memberikan jeda sebentar sebelum benar-benar menutup terminal
421
+ setTimeout(() => {
422
+ rl.close();
423
+ process.exit(0);
424
+ }, 1000);
425
+
426
+ break;
427
+ default:
428
+ // Jika salah input, tampilkan kembali menu
429
+ mainMenu();
430
+ break;
431
+ }
432
+ }
433
+
434
+ async function cleanUp() {
435
+ const conf = await askQuestion('Hapus semua file tools? (y/n): ');
436
+ if (conf.toLowerCase() === 'y')
437
+ fs.rmSync(TOOLS_DIR, { recursive: true, force: true });
438
+ }
439
+
440
+ function checkTools() {
441
+ return {
442
+ ytExists: fs.existsSync(YTDLP_PATH),
443
+ ffExists: fs.existsSync(FFMPEG_PATH),
444
+ allReady: fs.existsSync(YTDLP_PATH) && fs.existsSync(FFMPEG_PATH),
445
+ };
446
+ }
447
+
448
+ async function firstTimeSetup() {
449
+ while (true) {
450
+ const { ytExists, ffExists } = checkTools();
451
+ printHeader(
452
+ 'FIRST-TIME SETUP',
453
+ 'Komponen diperlukan untuk menjalankan aplikasi'
454
+ );
455
+
456
+ console.log(`${C.white}Status Instalasi:${C.reset}`);
457
+ console.log(
458
+ ` [${ytExists ? C.green + '✓' : C.red + '✗'}${
459
+ C.reset
460
+ }] Engine yt-dlp (Wajib)`
461
+ );
462
+ console.log(
463
+ ` [${ffExists ? C.green + '✓' : C.red + '✗'}${
464
+ C.reset
465
+ }] FFmpeg (Direkomendasikan)`
466
+ );
467
+
468
+ console.log(
469
+ `\n${C.yellow}Aplikasi belum siap digunakan. Pilih opsi:${C.reset}`
470
+ );
471
+ console.log(` ${C.cyan}1.${C.reset} Install Semua Komponen Otomatis`);
472
+ console.log(` ${C.cyan}0.${C.reset} Keluar dari Aplikasi`);
473
+
474
+ const choice = await askQuestion('\nPilih: ');
475
+
476
+ if (choice === '1') {
477
+ if (!ytExists) await installYtdlp();
478
+ if (!ffExists) await installFfmpeg();
479
+
480
+ const status = checkTools();
481
+ if (status.ytExists) {
482
+ console.log(
483
+ `\n${C.green}✨ Setup Selesai! Membuka Menu Utama...${C.reset}`
484
+ );
485
+ await new Promise((r) => setTimeout(r, 1500));
486
+ return mainMenu(); // Berhasil, lanjut ke menu utama
487
+ }
488
+ } else if (choice === '0') {
489
+ console.log('Menutup aplikasi...');
490
+ process.exit(0);
257
491
  }
258
- mainMenu();
492
+ }
493
+ }
494
+
495
+ async function systemMaintenance() {
496
+ let inMaintenance = true;
497
+
498
+ while (inMaintenance) {
499
+ const { ytExists, ffExists } = checkTools();
500
+ printHeader(
501
+ 'SYSTEM MAINTENANCE',
502
+ 'Update engine atau bersihkan file sistem'
503
+ );
504
+
505
+ console.log(`${C.white}Versi Terinstal:${C.reset}`);
506
+ console.log(
507
+ ` • yt-dlp : ${ytExists ? C.green + 'Ready' : C.red + 'Not Found'}${
508
+ C.reset
509
+ }`
510
+ );
511
+ console.log(
512
+ ` • FFmpeg : ${ffExists ? C.green + 'Ready' : C.red + 'Not Found'}${
513
+ C.reset
514
+ }`
515
+ );
516
+
517
+ console.log(`\n${C.bright}Opsi Pemeliharaan:${C.reset}`);
518
+ console.log(` ${C.cyan}1.${C.reset} Update / Reinstall Engines`);
519
+ console.log(` ${C.cyan}2.${C.reset} 🗑️ Hapus Semua Tools (Reset System)`);
520
+ console.log(` ${C.cyan}3.${C.reset} ⬅️ Kembali ke Menu Utama`);
521
+
522
+ const choice = await askQuestion('\nPilih tindakan: ');
523
+
524
+ switch (choice) {
525
+ case '1':
526
+ await installYtdlp();
527
+ await installFfmpeg();
528
+ await askQuestion('\nUpdate selesai. Tekan Enter...');
529
+ break;
530
+
531
+ case '2':
532
+ const confirm = await askQuestion(
533
+ `${C.bgRed}${C.white} KONFIRMASI ${C.reset} Hapus semua tools? (y/n): `
534
+ );
535
+ if (confirm.toLowerCase() === 'y') {
536
+ await cleanUp(); // Panggil fungsi penghapusan folder
537
+ console.log(
538
+ `${C.yellow}Sistem dibersihkan. Anda akan diarahkan ke Setup Wizard.${C.reset}`
539
+ );
540
+ await askQuestion('Tekan Enter...');
541
+ return bootstrap(); // Kembali ke pengecekan awal
542
+ }
543
+ break;
544
+
545
+ case '3':
546
+ inMaintenance = false;
547
+ return mainMenu();
548
+
549
+ default:
550
+ break;
551
+ }
552
+ }
553
+ }
554
+
555
+ // --- ENTRY POINT ---
556
+ async function bootstrap() {
557
+ const status = checkTools();
558
+
559
+ if (!status.allReady) {
560
+ // Jika ada yang kurang, masuk ke mode instalasi
561
+ await firstTimeSetup();
259
562
  } else {
260
- rl.close();
261
- process.exit(0);
563
+ // Jika semua siap, langsung ke menu download
564
+ mainMenu();
262
565
  }
263
566
  }
264
567
 
265
- mainMenu();
568
+ bootstrap();
569
+
package/bin/ui.js ADDED
@@ -0,0 +1,74 @@
1
+ const readline = require('readline');
2
+
3
+ const C = {
4
+ reset: '\x1b[0m',
5
+ bright: '\x1b[1m',
6
+ dim: '\x1b[2m',
7
+ cyan: '\x1b[36m',
8
+ green: '\x1b[32m',
9
+ yellow: '\x1b[33m',
10
+ red: '\x1b[31m',
11
+ magenta: '\x1b[35m',
12
+ blue: '\x1b[34m',
13
+ white: '\x1b[37m',
14
+ bgBlue: '\x1b[44m',
15
+ bgRed: '\x1b[41m',
16
+ };
17
+
18
+ function printHeader(title, summary = '') {
19
+ console.clear();
20
+ const width = 50;
21
+ const line = '━'.repeat(width);
22
+
23
+ console.log(`${C.cyan}┏${line}┓`);
24
+
25
+ // Baris Judul (Bright White)
26
+ console.log(
27
+ `${C.cyan}┃ ${C.bright}${C.white}${title.padEnd(width - 2)} ${C.cyan}┃`
28
+ );
29
+
30
+ // Baris Summary (Hanya muncul jika summary diisi)
31
+ if (summary) {
32
+ console.log(
33
+ `${C.cyan}┃ ${C.dim}${C.white}${summary.padEnd(width - 2)} ${C.cyan}┃`
34
+ );
35
+ }
36
+
37
+ console.log(`${C.cyan}┗${line}┛${C.reset}\n`);
38
+ }
39
+
40
+ const rl = readline.createInterface({
41
+ input: process.stdin,
42
+ output: process.stdout,
43
+ });
44
+
45
+ function askQuestion(query) {
46
+ return new Promise((resolve) =>
47
+ rl.question(`${C.bright}${C.yellow}❯ ${C.reset}${query}`, resolve)
48
+ );
49
+ }
50
+
51
+ function renderProgressBar(percent, speed, eta) {
52
+ const width = 30;
53
+ const complete = Math.round((percent / 100) * width);
54
+ const incomplete = width - complete;
55
+ const bar = `${C.green}${'█'.repeat(complete)}${C.dim}${'░'.repeat(
56
+ incomplete
57
+ )}${C.reset}`;
58
+
59
+ process.stdout.clearLine(0);
60
+ process.stdout.cursorTo(0);
61
+ process.stdout.write(
62
+ ` ${C.bright}${percent.toString().padStart(5)}% ${bar} ${C.cyan}${speed}${
63
+ C.reset
64
+ } | ${C.yellow}ETA: ${eta}${C.reset}`
65
+ );
66
+ }
67
+
68
+ module.exports = {
69
+ C,
70
+ rl,
71
+ printHeader,
72
+ askQuestion,
73
+ renderProgressBar,
74
+ };
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "media-dl",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "CLI Downloader video/audio lintas platform menggunakan yt-dlp.",
5
5
  "main": "bin/cli.js",
6
6
  "scripts": {
7
+ "dev": "npm un -g media-dl && npm i -g . && media-dl",
7
8
  "release": "npm version patch && npm publish",
8
9
  "release:minor": "npm version minor && npm publish",
9
10
  "release:major": "npm version major && npm publish"
package/project-info.txt DELETED
@@ -1,158 +0,0 @@
1
- === Project Structure ===
2
- 📁 bin/
3
- 📄 cli.js
4
- 📄 package.json
5
-
6
- === File Contents ===
7
-
8
- === File: package.json ===
9
- {
10
- "name": "media-dl",
11
- "version": "1.0.0",
12
- "description": "CLI Downloader video/audio lintas platform menggunakan yt-dlp.",
13
- "main": "bin/cli.js",
14
- "bin": {
15
- "media-dl": "bin/cli.js"
16
- },
17
- "publishConfig": {
18
- "access": "public"
19
- },
20
- "keywords": [
21
- "downloader",
22
- "youtube",
23
- "cross-platform",
24
- "media-dl"
25
- ],
26
- "author": "Ariska Hidayat",
27
- "license": "MIT",
28
- "dependencies": {},
29
- "engines": {
30
- "node": ">=14.0.0"
31
- }
32
- }
33
-
34
-
35
- === File: bin/cli.js ===
36
- 201~200~#!/usr/bin/env node
37
-
38
- const { spawn, execSync } = require('child_process');
39
- const readline = require('readline');
40
- const path = require('path');
41
- const fs = require('fs');
42
- const os = require('os');
43
-
44
- const rl = readline.createInterface({
45
- input: process.stdin,
46
- output: process.stdout
47
- });
48
-
49
- // Penentuan folder Tools berdasarkan OS (menyimpan di folder Home agar aman)
50
- const TOOLS_DIR = path.join(os.homedir(), '.media-dl');
51
- const isWindows = process.platform === 'win32';
52
-
53
- const YTDLP_PATH = path.join(TOOLS_DIR, isWindows ? 'yt-dlp.exe' : 'yt-dlp');
54
- const FFMPEG_PATH = path.join(TOOLS_DIR, isWindows ? 'ffmpeg.exe' : 'ffmpeg');
55
-
56
- if (!fs.existsSync(TOOLS_DIR)) fs.mkdirSync(TOOLS_DIR, { recursive: true });
57
-
58
- function askQuestion(query) {
59
- return new Promise(resolve => rl.question(query, resolve));
60
- }
61
-
62
- function checkTools() {
63
- return {
64
- ytExists: fs.existsSync(YTDLP_PATH),
65
- ffExists: fs.existsSync(FFMPEG_PATH)
66
- };
67
- }
68
-
69
- async function installTools() {
70
- console.log(`\n--- 🛠️ INSTALASI TOOLS (${process.platform.toUpperCase()}) ---`);
71
- const { ytExists } = checkTools();
72
-
73
- if (!ytExists) {
74
- console.log('⏳ Mengunduh yt-dlp...');
75
- try {
76
- const url = isWindows
77
- ? 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'
78
- : 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
79
-
80
- if (isWindows) {
81
- execSync(`powershell -Command "Invoke-WebRequest -Uri ${url} -OutFile '${YTDLP_PATH}'"`);
82
- } else {
83
- execSync(`curl -L ${url} -o "${YTDLP_PATH}"`);
84
- execSync(`chmod a+rx "${YTDLP_PATH}"`);
85
- }
86
- console.log('✅ yt-dlp berhasil diinstal.');
87
- } catch (e) {
88
- console.error('❌ Gagal menginstal yt-dlp. Pastikan koneksi internet stabil.');
89
- }
90
- } else {
91
- console.log('✅ yt-dlp sudah terpasang.');
92
- }
93
-
94
- console.log('\n📝 Catatan untuk FFmpeg:');
95
- console.log(`Silakan unduh ffmpeg manual dan letakkan di: ${FFMPEG_PATH}`);
96
- }
97
-
98
- async function startDownload() {
99
- const { ytExists, ffExists } = checkTools();
100
- if (!ytExists) {
101
- console.log('\n⚠️ Error: Jalankan menu Instalasi terlebih dahulu.');
102
- return mainMenu();
103
- }
104
-
105
- const videoURL = await askQuestion('\n🔗 Masukkan Link: ');
106
- console.log('\n[Format]\n1. Audio (MP3)\n2. Video (MP4)');
107
- const choice = await askQuestion('Pilihan: ');
108
-
109
- const outputDir = path.join(process.cwd(), 'downloads');
110
- if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
111
-
112
- const args = choice === '1'
113
- ? ['-x', '--audio-format', 'mp3', '--ffmpeg-location', FFMPEG_PATH, '-o', `${outputDir}/%(title)s.%(ext)s`, videoURL]
114
- : ['-f', 'mp4', '-o', `${outputDir}/%(title)s.%(ext)s`, videoURL];
115
-
116
- const download = spawn(YTDLP_PATH, args);
117
-
118
- download.stdout.on('data', (data) => process.stdout.write(data));
119
- download.on('close', (code) => {
120
- console.log(code === 0 ? `\n✅ SELESAI! Cek folder: ${outputDir}` : '\n❌ Gagal.');
121
- mainMenu();
122
- });
123
- }
124
-
125
- async function mainMenu() {
126
- const { ytExists, ffExists } = checkTools();
127
- console.log('\n==============================');
128
- console.log(' MEDIA-DL MANAGER ');
129
- console.log('==============================');
130
- console.log(`OS: ${process.platform} | yt-dlp: ${ytExists ? '✅' : '❌'} | ffmpeg: ${ffExists ? '✅' : '❌'}`);
131
- console.log('------------------------------');
132
- console.log('1. Download Media');
133
- console.log('2. Instal/Update yt-dlp');
134
- console.log('3. Keluar');
135
-
136
- const menuChoice = await askQuestion('\nPilih: ');
137
- if (menuChoice === '1') await startDownload();
138
- else if (menuChoice === '2') { await installTools(); mainMenu(); }
139
- else { rl.close(); process.exit(0); }
140
- }
141
-
142
- mainMenu();
143
- }
144
- })
145
- }
146
- }
147
- }
148
- }
149
- }
150
- }
151
- }
152
- }
153
- }
154
- }
155
- }
156
- }
157
- })
158
-