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 +441 -137
- package/bin/ui.js +74 -0
- package/package.json +2 -1
- package/project-info.txt +0 -158
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
|
-
|
|
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
|
-
|
|
18
|
+
// State Aplikasi
|
|
19
|
+
let safeMode = true;
|
|
23
20
|
|
|
24
|
-
|
|
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
|
-
|
|
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(
|
|
47
|
+
console.log(`\n${C.green}✅ yt-dlp berhasil dikonfigurasi!${C.reset}`);
|
|
51
48
|
} catch (e) {
|
|
52
|
-
console.error(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
80
|
+
console.error(
|
|
81
|
+
`${C.red}❌ Gagal menginstal FFmpeg secara otomatis.${C.reset}`
|
|
82
|
+
);
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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('
|
|
142
|
+
const videoURL = await askQuestion('Masukkan Link (Video/Playlist): ');
|
|
143
|
+
if (!videoURL) return mainMenu();
|
|
101
144
|
|
|
102
|
-
console.log(
|
|
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|%(
|
|
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(
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(
|
|
132
|
-
console.log(
|
|
133
|
-
console.log(
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
`${
|
|
216
|
+
`${outputDir}/%(title).100s.%(ext)s`,
|
|
155
217
|
videoURL,
|
|
156
218
|
];
|
|
157
219
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
190
|
-
const download = spawn(YTDLP_PATH, args);
|
|
254
|
+
args.push('--no-mtime');
|
|
191
255
|
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
265
|
+
isWindows
|
|
266
|
+
? `explorer "${outputDir}"`
|
|
267
|
+
: isMac
|
|
268
|
+
? `open "${outputDir}"`
|
|
269
|
+
: `xdg-open "${outputDir}"`
|
|
200
270
|
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
console.log(
|
|
214
|
-
console.log(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
console.log(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
563
|
+
// Jika semua siap, langsung ke menu download
|
|
564
|
+
mainMenu();
|
|
262
565
|
}
|
|
263
566
|
}
|
|
264
567
|
|
|
265
|
-
|
|
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.
|
|
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
|
-
|