media-dl 2.0.0 → 2.1.1
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 +444 -137
- package/bin/ui.js +74 -0
- package/package.json +3 -1
- package/readme.md +88 -34
- 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,13 @@ function checkTools() {
|
|
|
32
27
|
};
|
|
33
28
|
}
|
|
34
29
|
|
|
30
|
+
// --- INSTALLERS ---
|
|
35
31
|
async function installYtdlp() {
|
|
36
|
-
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(TOOLS_DIR)) fs.mkdirSync(TOOLS_DIR, { recursive: true });
|
|
34
|
+
|
|
35
|
+
printHeader('INSTALL / UPDATE YT-DLP');
|
|
36
|
+
console.log(`${C.blue}⏳ Sedang mengunduh engine terbaru...${C.reset}`);
|
|
37
37
|
const url = isWindows
|
|
38
38
|
? 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'
|
|
39
39
|
: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
|
|
@@ -47,16 +47,23 @@ async function installYtdlp() {
|
|
|
47
47
|
execSync(`curl -L -# "${url}" -o "${YTDLP_PATH}"`, { stdio: 'inherit' });
|
|
48
48
|
execSync(`chmod a+rx "${YTDLP_PATH}"`);
|
|
49
49
|
}
|
|
50
|
-
console.log(
|
|
50
|
+
console.log(`\n${C.green}✅ yt-dlp berhasil dikonfigurasi!${C.reset}`);
|
|
51
51
|
} catch (e) {
|
|
52
|
-
console.error(
|
|
52
|
+
console.error(
|
|
53
|
+
`\n${C.red}❌ Gagal mengunduh. Periksa koneksi internet Anda.${C.reset}`
|
|
54
|
+
);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
async function installFfmpeg() {
|
|
57
|
-
|
|
59
|
+
printHeader('INSTALL FFmpeg');
|
|
60
|
+
console.log(
|
|
61
|
+
`${C.dim}FFmpeg diperlukan untuk kualitas 1080p+ dan konversi MP3.${C.reset}\n`
|
|
62
|
+
);
|
|
63
|
+
|
|
58
64
|
try {
|
|
59
65
|
if (isMac) {
|
|
66
|
+
console.log(`${C.blue}⏳ Mengunduh FFmpeg untuk macOS...${C.reset}`);
|
|
60
67
|
const zipPath = path.join(TOOLS_DIR, 'ffmpeg.zip');
|
|
61
68
|
execSync(
|
|
62
69
|
`curl -L -# "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o "${zipPath}"`,
|
|
@@ -65,47 +72,86 @@ async function installFfmpeg() {
|
|
|
65
72
|
execSync(`unzip -o "${zipPath}" -d "${TOOLS_DIR}"`, { stdio: 'inherit' });
|
|
66
73
|
execSync(`rm "${zipPath}"`);
|
|
67
74
|
execSync(`chmod a+rx "${FFMPEG_PATH}"`);
|
|
68
|
-
console.log(
|
|
75
|
+
console.log(`\n${C.green}✅ FFmpeg aktif di macOS.${C.reset}`);
|
|
76
|
+
} else if (isWindows) {
|
|
77
|
+
console.log(
|
|
78
|
+
`${C.yellow}ℹ️ Untuk Windows, disarankan mengunduh 'ffmpeg.exe' secara manual`
|
|
79
|
+
);
|
|
80
|
+
console.log(`dan letakkan di: ${C.white}${TOOLS_DIR}${C.reset}`);
|
|
69
81
|
}
|
|
70
82
|
} catch (e) {
|
|
71
|
-
console.error(
|
|
83
|
+
console.error(
|
|
84
|
+
`${C.red}❌ Gagal menginstal FFmpeg secara otomatis.${C.reset}`
|
|
85
|
+
);
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
function runSpawn(command, args) {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const proc = spawn(command, args);
|
|
92
|
+
let lastOutput = '';
|
|
93
|
+
|
|
94
|
+
proc.stdout.on('data', (data) => {
|
|
95
|
+
const output = data.toString();
|
|
96
|
+
// Regex untuk menangkap progress dari yt-dlp
|
|
97
|
+
const progressMatch = output.match(
|
|
98
|
+
/\[download\]\s+(\d+\.\d+)%\s+of\s+.*\s+at\s+([\d\w\./s]+)\s+ETA\s+([\d:]+)/
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (progressMatch) {
|
|
102
|
+
const [_, percent, speed, eta] = progressMatch;
|
|
103
|
+
renderProgressBar(parseFloat(percent), speed, eta);
|
|
104
|
+
} else {
|
|
105
|
+
// Jika bukan bar, print normal (misal: info merging/ffmpeg)
|
|
106
|
+
if (output.trim() && !output.includes('[download]')) {
|
|
107
|
+
process.stdout.write(`\n${C.dim}${output.trim()}${C.reset}\n`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
proc.stderr.on('data', (data) => {
|
|
113
|
+
const err = data.toString();
|
|
114
|
+
if (!err.includes('WARNING')) {
|
|
115
|
+
process.stdout.write(`\n${C.red}⚠️ ${err}${C.reset}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
proc.on('close', (code) => {
|
|
120
|
+
process.stdout.write('\n'); // Baris baru setelah selesai
|
|
121
|
+
resolve(code);
|
|
122
|
+
});
|
|
87
123
|
});
|
|
88
|
-
return Array.from(selected).join(',');
|
|
89
124
|
}
|
|
90
125
|
|
|
126
|
+
// --- DOWNLOAD ENGINE ---
|
|
91
127
|
async function startDownload() {
|
|
92
128
|
let { ytExists, ffExists } = checkTools();
|
|
93
129
|
if (!ytExists) {
|
|
94
|
-
console.log(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
130
|
+
console.log(
|
|
131
|
+
`\n${C.red}❌ Engine yt-dlp tidak ditemukan. Silakan pilih menu Update/Install.${C.reset}`
|
|
132
|
+
);
|
|
133
|
+
await askQuestion('Tekan Enter...');
|
|
134
|
+
return mainMenu();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!ffExists) {
|
|
138
|
+
console.log(
|
|
139
|
+
`${C.yellow}⚠️ Peringatan: FFmpeg tidak ditemukan. Video mungkin tidak tergabung dengan audio.${C.reset}`
|
|
140
|
+
);
|
|
141
|
+
const cont = await askQuestion('Lanjutkan saja? (y/n): ');
|
|
142
|
+
if (cont.toLowerCase() !== 'y') return mainMenu();
|
|
98
143
|
}
|
|
99
144
|
|
|
100
|
-
const videoURL = await askQuestion('
|
|
145
|
+
const videoURL = await askQuestion('Masukkan Link (Video/Playlist): ');
|
|
146
|
+
if (!videoURL) return mainMenu();
|
|
101
147
|
|
|
102
|
-
console.log(
|
|
148
|
+
console.log(`${C.dim}⏳ Menganalisa tautan...${C.reset}`);
|
|
103
149
|
let playlistInfo = { isPlaylist: false, title: '', items: [] };
|
|
104
150
|
|
|
105
151
|
try {
|
|
106
152
|
const rawInfo = execSync(
|
|
107
|
-
`"${YTDLP_PATH}" --flat-playlist --print "%(playlist_title)s|%(
|
|
108
|
-
{ encoding: 'utf-8' }
|
|
153
|
+
`"${YTDLP_PATH}" --flat-playlist --print "%(playlist_title)s|%(title)s" "${videoURL}"`,
|
|
154
|
+
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
109
155
|
);
|
|
110
156
|
const lines = rawInfo.trim().split('\n');
|
|
111
157
|
if (lines.length > 1 || videoURL.includes('playlist?list=')) {
|
|
@@ -117,149 +163,410 @@ async function startDownload() {
|
|
|
117
163
|
|
|
118
164
|
let playlistSelection = null;
|
|
119
165
|
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: '
|
|
166
|
+
console.log(
|
|
167
|
+
`\n${C.bgBlue}${C.bright} 📂 PLAYLIST TERDETEKSI: ${playlistInfo.title} ${C.reset}`
|
|
124
168
|
);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
169
|
+
playlistInfo.items.forEach((item, index) => {
|
|
170
|
+
console.log(
|
|
171
|
+
`${C.cyan}${(index + 1).toString().padStart(3, ' ')}.${C.reset} ${item}`
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
console.log(
|
|
175
|
+
`\n${C.dim}Contoh pilih nomor: 1,3,5-10 atau biarkan kosong untuk semua.${C.reset}`
|
|
128
176
|
);
|
|
177
|
+
const selectionInput = await askQuestion('Pilih nomor: ');
|
|
178
|
+
|
|
179
|
+
// Parsing nomor playlist
|
|
180
|
+
if (selectionInput) {
|
|
181
|
+
const selected = new Set();
|
|
182
|
+
selectionInput.split(',').forEach((p) => {
|
|
183
|
+
if (p.includes('-')) {
|
|
184
|
+
const [s, e] = p.split('-').map(Number);
|
|
185
|
+
for (let i = s; i <= e; i++)
|
|
186
|
+
if (i > 0 && i <= playlistInfo.items.length) selected.add(i);
|
|
187
|
+
} else {
|
|
188
|
+
const n = parseInt(p.trim());
|
|
189
|
+
if (n > 0 && n <= playlistInfo.items.length) selected.add(n);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
playlistSelection = Array.from(selected).join(',');
|
|
193
|
+
}
|
|
129
194
|
}
|
|
130
195
|
|
|
131
|
-
console.log(
|
|
132
|
-
console.log(
|
|
133
|
-
console.log(
|
|
196
|
+
console.log(`\n${C.bright} [ PILIH FORMAT ]${C.reset}`);
|
|
197
|
+
console.log(` ${C.green}1.${C.reset} Video (Kualitas Terbaik/MP4)`);
|
|
198
|
+
console.log(
|
|
199
|
+
` ${C.green}2.${C.reset} Audio Only (MP3) ${
|
|
200
|
+
ffExists ? C.green + '✅' : C.red + '❌ (Butuh FFmpeg)'
|
|
201
|
+
}`
|
|
202
|
+
);
|
|
134
203
|
const mode = await askQuestion('Pilihan: ');
|
|
135
204
|
|
|
136
205
|
const baseDir = path.join(os.homedir(), 'Downloads', 'media-dl');
|
|
137
206
|
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 });
|
|
207
|
+
let outputDir = path.join(baseDir, subFolder);
|
|
208
|
+
if (playlistInfo.isPlaylist)
|
|
209
|
+
outputDir = path.join(
|
|
210
|
+
outputDir,
|
|
211
|
+
playlistInfo.title.replace(/[\\/:"*?<>|]/g, '_')
|
|
212
|
+
);
|
|
213
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
147
214
|
|
|
148
|
-
const qtFormat =
|
|
149
|
-
'bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[vcodec^=avc1]/best';
|
|
150
215
|
let args = [
|
|
151
216
|
'--ffmpeg-location',
|
|
152
217
|
FFMPEG_PATH,
|
|
153
218
|
'-o',
|
|
154
|
-
`${
|
|
219
|
+
`${outputDir}/%(title).100s.%(ext)s`,
|
|
155
220
|
videoURL,
|
|
156
221
|
];
|
|
157
222
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
// Integrasi Safe Mode
|
|
224
|
+
if (safeMode) {
|
|
225
|
+
args.push(
|
|
226
|
+
'--ratelimit',
|
|
227
|
+
'5M',
|
|
228
|
+
'--sleep-interval',
|
|
229
|
+
'3',
|
|
230
|
+
'--max-sleep-interval',
|
|
231
|
+
'10',
|
|
232
|
+
'--user-agent',
|
|
233
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
|
|
234
|
+
);
|
|
162
235
|
}
|
|
163
236
|
|
|
237
|
+
if (playlistSelection) args.push('--playlist-items', playlistSelection);
|
|
238
|
+
else if (!playlistInfo.isPlaylist) args.push('--no-playlist');
|
|
239
|
+
|
|
164
240
|
if (mode === '2') {
|
|
165
241
|
if (!ffExists) {
|
|
166
|
-
console.log(
|
|
242
|
+
console.log(
|
|
243
|
+
`${C.red}❌ Error: Anda wajib menginstal FFmpeg untuk mengunduh audio.${C.reset}`
|
|
244
|
+
);
|
|
245
|
+
await askQuestion('Kembali...');
|
|
167
246
|
return mainMenu();
|
|
168
247
|
}
|
|
169
248
|
args.unshift('-x', '--audio-format', 'mp3');
|
|
170
249
|
} 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);
|
|
250
|
+
args.unshift(
|
|
251
|
+
'-f',
|
|
252
|
+
'bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/best[vcodec^=avc1]/best'
|
|
253
|
+
);
|
|
186
254
|
if (ffExists) args.unshift('--recode-video', 'mp4');
|
|
187
255
|
}
|
|
188
256
|
|
|
189
|
-
|
|
190
|
-
const download = spawn(YTDLP_PATH, args);
|
|
257
|
+
args.push('--no-mtime');
|
|
191
258
|
|
|
192
|
-
|
|
193
|
-
download.stderr.on('data', (data) => process.stderr.write(data));
|
|
259
|
+
console.log(`\n${C.bgBlue}${C.bright} 🚀 MEMULAI PROSES... ${C.reset}\n`);
|
|
194
260
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
261
|
+
const code = await runSpawn(YTDLP_PATH, args); // Menunggu sampai benar-benar selesai
|
|
262
|
+
|
|
263
|
+
if (code === 0) {
|
|
264
|
+
console.log(`\n${C.green}✨ SELESAI! Cek folder: ${outputDir}${C.reset}`);
|
|
265
|
+
// Auto-open folder
|
|
266
|
+
try {
|
|
198
267
|
execSync(
|
|
199
|
-
|
|
268
|
+
isWindows
|
|
269
|
+
? `explorer "${outputDir}"`
|
|
270
|
+
: isMac
|
|
271
|
+
? `open "${outputDir}"`
|
|
272
|
+
: `xdg-open "${outputDir}"`
|
|
200
273
|
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
});
|
|
274
|
+
} catch (e) {}
|
|
275
|
+
} else {
|
|
276
|
+
console.log(`\n${C.red}❌ Terjadi kesalahan saat mengunduh.${C.reset}`);
|
|
277
|
+
}
|
|
206
278
|
}
|
|
207
279
|
|
|
208
280
|
async function showSupport() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
console.log(
|
|
214
|
-
console.log(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
console.log(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
281
|
+
// Menggunakan 2 parameter: Judul dan Summary
|
|
282
|
+
printHeader('TENTANG APLIKASI', 'Media-DL Manager Pro v2.0.0 - 2026');
|
|
283
|
+
|
|
284
|
+
// --- SEKSI FITUR ---
|
|
285
|
+
console.log(` ${C.bright}${C.cyan}OVERVIEW${C.reset}`);
|
|
286
|
+
console.log(` Terima kasih telah memilih MEDIA-DL. Skrip ini dirancang`);
|
|
287
|
+
console.log(` untuk memudahkan manajemen unduhan media secara lokal.\n`);
|
|
288
|
+
|
|
289
|
+
console.log(` ${C.bright}${C.cyan}FITUR UNGGULAN${C.reset}`);
|
|
290
|
+
const features = [
|
|
291
|
+
{
|
|
292
|
+
icon: '✦',
|
|
293
|
+
title: 'High Quality',
|
|
294
|
+
desc: 'Mendukung hingga 4K & Audio 320kbps',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
icon: '✦',
|
|
298
|
+
title: 'Multi-Source',
|
|
299
|
+
desc: 'YouTube, TikTok, IG Reels, & Shorts',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
icon: '✦',
|
|
303
|
+
title: 'Batch Mode',
|
|
304
|
+
desc: 'Mendukung unduhan Playlist secara massal',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
icon: '✦',
|
|
308
|
+
title: 'Safe Guard',
|
|
309
|
+
desc: 'Mode proteksi agar akun/IP tidak terblokir',
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
features.forEach((f) => {
|
|
314
|
+
console.log(
|
|
315
|
+
` ${C.green}${f.icon}${C.reset} ${C.white}${f.title.padEnd(15)}${
|
|
316
|
+
C.reset
|
|
317
|
+
} ${C.dim}• ${f.desc}${C.reset}`
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
console.log('\n' + '─'.repeat(52));
|
|
322
|
+
|
|
323
|
+
// --- SEKSI DUKUNGAN ---
|
|
324
|
+
console.log(`\n ${C.bright}${C.magenta}DUKUNGAN & DONASI${C.reset}`);
|
|
325
|
+
console.log(` Dukungan Anda sangat membantu pengembang untuk terus`);
|
|
326
|
+
console.log(` memperbarui engine dan fitur aplikasi ini.\n`);
|
|
327
|
+
|
|
328
|
+
// Menampilkan Link dengan label background agar menonjol
|
|
329
|
+
const links = [
|
|
330
|
+
{ label: ' ☕ BELI KOPI ', url: 'https://app.midtrans.com/coffee' },
|
|
331
|
+
{ label: ' 🍕 BELI PIZZA', url: 'https://app.midtrans.com/pizza' },
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
links.forEach((l) => {
|
|
335
|
+
console.log(
|
|
336
|
+
` ${C.bgBlue}${C.white}${l.label}${C.reset} ${C.blue}➜${C.reset} ${C.dim}${l.url}${C.reset}`
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
console.log(`\n${C.cyan}${'━'.repeat(52)}${C.reset}`);
|
|
341
|
+
|
|
342
|
+
await askQuestion('Tekan Enter untuk kembali ke Menu Utama...');
|
|
222
343
|
mainMenu();
|
|
223
344
|
}
|
|
224
345
|
|
|
225
346
|
async function mainMenu() {
|
|
226
347
|
const { ytExists, ffExists } = checkTools();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
348
|
+
|
|
349
|
+
// Menggunakan 2 parameter: Judul dan Summary status singkat
|
|
350
|
+
printHeader('MEDIA-DL PRO 2026', 'Pusat Kendali Unduhan Media Lokal');
|
|
351
|
+
|
|
352
|
+
// --- SEKSI DASHBOARD (INFO SISTEM) ---
|
|
353
|
+
const statusYt = ytExists
|
|
354
|
+
? `${C.green}Ready${C.reset}`
|
|
355
|
+
: `${C.red}Not Found${C.reset}`;
|
|
356
|
+
const statusFf = ffExists
|
|
357
|
+
? `${C.green}Ready${C.reset}`
|
|
358
|
+
: `${C.yellow}Missing${C.reset}`;
|
|
359
|
+
const safeBadge = safeMode
|
|
360
|
+
? `${C.bgBlue}${C.white} ON ${C.reset}`
|
|
361
|
+
: `${C.bgRed}${C.white} OFF ${C.reset}`;
|
|
362
|
+
|
|
363
|
+
console.log(` ${C.bright}SYSTEM STATUS${C.reset}`);
|
|
364
|
+
console.log(` 🤖 Engine : [ ${statusYt} ] | 🎬 FFmpeg : [ ${statusFf} ]`);
|
|
365
|
+
console.log(` 🛡️ Safe Mode Guard : ${safeBadge}\n`);
|
|
366
|
+
|
|
367
|
+
console.log(` ${C.cyan}━${'━'.repeat(48)}${C.reset}`);
|
|
368
|
+
|
|
369
|
+
// --- SEKSI NAVIGASI ---
|
|
370
|
+
console.log(` ${C.bright}MAIN SERVICES${C.reset}`);
|
|
230
371
|
console.log(
|
|
231
|
-
`
|
|
232
|
-
ytExists ? '✅' : '❌'
|
|
233
|
-
} | ffmpeg: ${ffExists ? '✅' : '❌'}`
|
|
372
|
+
` ${C.cyan}1.${C.reset} 📥 Download Media ${C.dim}(Video, Music, Playlist)${C.reset}`
|
|
234
373
|
);
|
|
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
|
-
|
|
374
|
+
console.log(
|
|
375
|
+
` ${C.cyan}2.${C.reset} 🛡️ Toggle Safe Mode ${C.dim}(Sekarang: ${
|
|
376
|
+
safeMode ? 'Aktif' : 'Nonaktif'
|
|
377
|
+
})${C.reset}`
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
console.log(`\n ${C.bright}SYSTEM & INFO${C.reset}`);
|
|
381
|
+
console.log(
|
|
382
|
+
` ${C.cyan}3.${C.reset} ⚙️ Maintenance & Update ${C.dim}(Update engine / Cleanup)${C.reset}`
|
|
383
|
+
);
|
|
384
|
+
console.log(
|
|
385
|
+
` ${C.cyan}4.${C.reset} ❤️ Tentang Aplikasi ${C.dim}(Dukungan & Fitur)${C.reset}`
|
|
386
|
+
);
|
|
387
|
+
console.log(` ${C.cyan}0.${C.reset} 🚪 Keluar`);
|
|
388
|
+
|
|
389
|
+
console.log(` ${C.cyan}━${'━'.repeat(48)}${C.reset}`);
|
|
390
|
+
|
|
391
|
+
const choice = await askQuestion('\nPilih menu (0-4): ');
|
|
392
|
+
|
|
393
|
+
switch (choice) {
|
|
394
|
+
case '1':
|
|
395
|
+
await startDownload();
|
|
396
|
+
break;
|
|
397
|
+
case '2':
|
|
398
|
+
safeMode = !safeMode;
|
|
399
|
+
// Berikan feedback visual singkat sebelum refresh menu
|
|
400
|
+
console.log(
|
|
401
|
+
`\n${C.yellow} 🛡️ Safe Mode telah ${
|
|
402
|
+
safeMode ? 'DIAKTIFKAN' : 'DINONAKTIFKAN'
|
|
403
|
+
}${C.reset}`
|
|
404
|
+
);
|
|
405
|
+
setTimeout(() => mainMenu(), 800);
|
|
406
|
+
break;
|
|
407
|
+
case '3':
|
|
408
|
+
await systemMaintenance();
|
|
409
|
+
break;
|
|
410
|
+
case '4':
|
|
411
|
+
await showSupport();
|
|
412
|
+
break;
|
|
413
|
+
case '0':
|
|
414
|
+
console.log(`\n${C.cyan}━${'━'.repeat(48)}${C.reset}`);
|
|
415
|
+
console.log(
|
|
416
|
+
` ${C.bright}${C.white}Terima kasih telah menggunakan MEDIA-DL!${C.reset}`
|
|
417
|
+
);
|
|
418
|
+
console.log(
|
|
419
|
+
` ${C.green}✨ Semoga Anda sukses, jaya, dan sehat selalu! ✨${C.reset}`
|
|
420
|
+
);
|
|
421
|
+
console.log(`${C.cyan}━${'━'.repeat(48)}${C.reset}\n`);
|
|
422
|
+
|
|
423
|
+
// Memberikan jeda sebentar sebelum benar-benar menutup terminal
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
rl.close();
|
|
426
|
+
process.exit(0);
|
|
427
|
+
}, 1000);
|
|
428
|
+
|
|
429
|
+
break;
|
|
430
|
+
default:
|
|
431
|
+
// Jika salah input, tampilkan kembali menu
|
|
432
|
+
mainMenu();
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function cleanUp() {
|
|
438
|
+
const conf = await askQuestion('Hapus semua file tools? (y/n): ');
|
|
439
|
+
if (conf.toLowerCase() === 'y')
|
|
440
|
+
fs.rmSync(TOOLS_DIR, { recursive: true, force: true });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function checkTools() {
|
|
444
|
+
return {
|
|
445
|
+
ytExists: fs.existsSync(YTDLP_PATH),
|
|
446
|
+
ffExists: fs.existsSync(FFMPEG_PATH),
|
|
447
|
+
allReady: fs.existsSync(YTDLP_PATH) && fs.existsSync(FFMPEG_PATH),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function firstTimeSetup() {
|
|
452
|
+
while (true) {
|
|
453
|
+
const { ytExists, ffExists } = checkTools();
|
|
454
|
+
printHeader(
|
|
455
|
+
'FIRST-TIME SETUP',
|
|
456
|
+
'Komponen diperlukan untuk menjalankan aplikasi'
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
console.log(`${C.white}Status Instalasi:${C.reset}`);
|
|
460
|
+
console.log(
|
|
461
|
+
` [${ytExists ? C.green + '✓' : C.red + '✗'}${
|
|
462
|
+
C.reset
|
|
463
|
+
}] Engine yt-dlp (Wajib)`
|
|
464
|
+
);
|
|
465
|
+
console.log(
|
|
466
|
+
` [${ffExists ? C.green + '✓' : C.red + '✗'}${
|
|
467
|
+
C.reset
|
|
468
|
+
}] FFmpeg (Direkomendasikan)`
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
console.log(
|
|
472
|
+
`\n${C.yellow}Aplikasi belum siap digunakan. Pilih opsi:${C.reset}`
|
|
473
|
+
);
|
|
474
|
+
console.log(` ${C.cyan}1.${C.reset} Install Semua Komponen Otomatis`);
|
|
475
|
+
console.log(` ${C.cyan}0.${C.reset} Keluar dari Aplikasi`);
|
|
476
|
+
|
|
477
|
+
const choice = await askQuestion('\nPilih: ');
|
|
478
|
+
|
|
479
|
+
if (choice === '1') {
|
|
480
|
+
if (!ytExists) await installYtdlp();
|
|
481
|
+
if (!ffExists) await installFfmpeg();
|
|
482
|
+
|
|
483
|
+
const status = checkTools();
|
|
484
|
+
if (status.ytExists) {
|
|
485
|
+
console.log(
|
|
486
|
+
`\n${C.green}✨ Setup Selesai! Membuka Menu Utama...${C.reset}`
|
|
487
|
+
);
|
|
488
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
489
|
+
return mainMenu(); // Berhasil, lanjut ke menu utama
|
|
490
|
+
}
|
|
491
|
+
} else if (choice === '0') {
|
|
492
|
+
console.log('Menutup aplikasi...');
|
|
493
|
+
process.exit(0);
|
|
257
494
|
}
|
|
258
|
-
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function systemMaintenance() {
|
|
499
|
+
let inMaintenance = true;
|
|
500
|
+
|
|
501
|
+
while (inMaintenance) {
|
|
502
|
+
const { ytExists, ffExists } = checkTools();
|
|
503
|
+
printHeader(
|
|
504
|
+
'SYSTEM MAINTENANCE',
|
|
505
|
+
'Update engine atau bersihkan file sistem'
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
console.log(`${C.white}Versi Terinstal:${C.reset}`);
|
|
509
|
+
console.log(
|
|
510
|
+
` • yt-dlp : ${ytExists ? C.green + 'Ready' : C.red + 'Not Found'}${
|
|
511
|
+
C.reset
|
|
512
|
+
}`
|
|
513
|
+
);
|
|
514
|
+
console.log(
|
|
515
|
+
` • FFmpeg : ${ffExists ? C.green + 'Ready' : C.red + 'Not Found'}${
|
|
516
|
+
C.reset
|
|
517
|
+
}`
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
console.log(`\n${C.bright}Opsi Pemeliharaan:${C.reset}`);
|
|
521
|
+
console.log(` ${C.cyan}1.${C.reset} Update / Reinstall Engines`);
|
|
522
|
+
console.log(` ${C.cyan}2.${C.reset} 🗑️ Hapus Semua Tools (Reset System)`);
|
|
523
|
+
console.log(` ${C.cyan}3.${C.reset} ⬅️ Kembali ke Menu Utama`);
|
|
524
|
+
|
|
525
|
+
const choice = await askQuestion('\nPilih tindakan: ');
|
|
526
|
+
|
|
527
|
+
switch (choice) {
|
|
528
|
+
case '1':
|
|
529
|
+
await installYtdlp();
|
|
530
|
+
await installFfmpeg();
|
|
531
|
+
await askQuestion('\nUpdate selesai. Tekan Enter...');
|
|
532
|
+
break;
|
|
533
|
+
|
|
534
|
+
case '2':
|
|
535
|
+
const confirm = await askQuestion(
|
|
536
|
+
`${C.bgRed}${C.white} KONFIRMASI ${C.reset} Hapus semua tools? (y/n): `
|
|
537
|
+
);
|
|
538
|
+
if (confirm.toLowerCase() === 'y') {
|
|
539
|
+
await cleanUp(); // Panggil fungsi penghapusan folder
|
|
540
|
+
console.log(
|
|
541
|
+
`${C.yellow}Sistem dibersihkan. Anda akan diarahkan ke Setup Wizard.${C.reset}`
|
|
542
|
+
);
|
|
543
|
+
await askQuestion('Tekan Enter...');
|
|
544
|
+
return bootstrap(); // Kembali ke pengecekan awal
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case '3':
|
|
549
|
+
inMaintenance = false;
|
|
550
|
+
return mainMenu();
|
|
551
|
+
|
|
552
|
+
default:
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// --- ENTRY POINT ---
|
|
559
|
+
async function bootstrap() {
|
|
560
|
+
const status = checkTools();
|
|
561
|
+
|
|
562
|
+
if (!status.allReady) {
|
|
563
|
+
// Jika ada yang kurang, masuk ke mode instalasi
|
|
564
|
+
await firstTimeSetup();
|
|
259
565
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
566
|
+
// Jika semua siap, langsung ke menu download
|
|
567
|
+
mainMenu();
|
|
262
568
|
}
|
|
263
569
|
}
|
|
264
570
|
|
|
265
|
-
|
|
571
|
+
bootstrap();
|
|
572
|
+
|
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,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "media-dl",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
|
8
|
+
"start": "npm un -g media-dl && npm i -g media-dl@latest && media-dl",
|
|
7
9
|
"release": "npm version patch && npm publish",
|
|
8
10
|
"release:minor": "npm version minor && npm publish",
|
|
9
11
|
"release:major": "npm version major && npm publish"
|
package/readme.md
CHANGED
|
@@ -1,64 +1,118 @@
|
|
|
1
|
-
# Media-DL
|
|
1
|
+
# 🚀 Media-DL Pro 2026
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**The Ultimate Cross-Platform Media Engine Manager**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Media-DL Pro adalah *cli-wrapper* canggih berbasis `yt-dlp` yang dirancang untuk pengguna profesional yang membutuhkan kecepatan, keteraturan, dan keamanan akun. Bukan sekadar pengunduh, ini adalah manajer media lokal yang cerdas.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🛡️ Mengapa Media-DL Pro?
|
|
10
|
+
|
|
11
|
+
### 1. Safe Mode Guard™ (Exclusive)
|
|
12
|
+
|
|
13
|
+
Jangan biarkan IP atau akun Anda terblokir. Media-DL dilengkapi dengan mode proteksi yang mensimulasikan perilaku manusia melalui:
|
|
14
|
+
|
|
15
|
+
*
|
|
16
|
+
|
|
17
|
+
**Smart Rate Limiting**: Membatasi kecepatan unduh hingga 5MB/s.
|
|
18
|
+
|
|
19
|
+
*
|
|
20
|
+
|
|
21
|
+
**Randomized Sleep**: Interval jeda otomatis 3-10 detik antar unduhan.
|
|
22
|
+
|
|
23
|
+
*
|
|
24
|
+
|
|
25
|
+
**Custom User-Agents**: Menggunakan identitas browser modern untuk menghindari deteksi bot.
|
|
12
26
|
|
|
13
|
-
|
|
27
|
+
### 2. Dashboard Status Real-Time
|
|
28
|
+
|
|
29
|
+
Pantau kesehatan sistem Anda secara langsung. Aplikasi mendeteksi secara otomatis ketersediaan `yt-dlp` dan `FFmpeg` untuk memastikan hasil unduhan maksimal.
|
|
30
|
+
|
|
31
|
+
### 3. Pengunduhan Massal & Selektif
|
|
32
|
+
|
|
33
|
+
Mendukung unduhan playlist secara penuh dengan fitur seleksi item yang presisi (Contoh: `1,3,5-10`).
|
|
34
|
+
|
|
35
|
+
### 4. Optimalisasi Format Otomatis
|
|
36
|
+
|
|
37
|
+
*
|
|
38
|
+
|
|
39
|
+
**Video**: Konversi cerdas ke kontainer MP4 dengan codec `AVC1` agar kompatibel dengan perangkat seluler dan editor profesional.
|
|
40
|
+
|
|
41
|
+
*
|
|
42
|
+
|
|
43
|
+
**Audio**: Ekstraksi MP3 kualitas tinggi (320kbps) langsung ke folder musik Anda.
|
|
44
|
+
|
|
45
|
+
---
|
|
14
46
|
|
|
15
|
-
|
|
47
|
+
## 📦 Instalasi Cepat
|
|
16
48
|
|
|
17
49
|
```bash
|
|
50
|
+
# Pastikan Node.js sudah terinstal
|
|
18
51
|
npm install -g media-dl
|
|
19
52
|
|
|
20
53
|
```
|
|
21
54
|
|
|
22
|
-
|
|
55
|
+
Cukup ketik `media-dl` di terminal Anda untuk memulai.
|
|
23
56
|
|
|
24
|
-
|
|
57
|
+
---
|
|
25
58
|
|
|
26
|
-
|
|
27
|
-
media-dl
|
|
59
|
+
## 🛠️ Navigasi Sistem
|
|
28
60
|
|
|
29
|
-
|
|
61
|
+
Aplikasi ini memiliki pusat kendali yang terorganisir:
|
|
62
|
+
|
|
63
|
+
1.
|
|
64
|
+
|
|
65
|
+
**📥 Main Service**: Layanan unduh media tunggal atau playlist.
|
|
30
66
|
|
|
31
|
-
|
|
67
|
+
1.
|
|
32
68
|
|
|
33
|
-
|
|
34
|
-
2. **Pilih Format**: Video MP4 (QuickTime Compatible) atau Audio MP3.
|
|
35
|
-
3. **Seleksi Playlist**: Masukkan angka video yang diinginkan (Contoh: `1,3-5`) atau tekan `Enter` untuk semua.
|
|
36
|
-
4. **Auto-Open**: Folder tujuan akan otomatis terbuka setelah proses selesai.
|
|
69
|
+
**🛡️ Toggle Safe Mode**: Aktifkan/nonaktifkan perlindungan keamanan secara instan.
|
|
37
70
|
|
|
38
|
-
|
|
71
|
+
1.
|
|
72
|
+
|
|
73
|
+
**⚙️ Maintenance**: Perbarui engine otomatis atau bersihkan sistem dari file sampah.
|
|
74
|
+
|
|
75
|
+
1.
|
|
76
|
+
|
|
77
|
+
**❤️ About**: Informasi pengembang dan daftar fitur lengkap.
|
|
78
|
+
|
|
79
|
+
---
|
|
39
80
|
|
|
40
|
-
|
|
81
|
+
## 📂 Struktur Penyimpanan
|
|
41
82
|
|
|
42
|
-
|
|
43
|
-
* **Video**: `~/Downloads/media-dl/video/`
|
|
44
|
-
* **Audio**: `~/Downloads/media-dl/audio/`
|
|
83
|
+
Media Anda akan tersimpan secara otomatis dengan struktur profesional di direktori pengguna:
|
|
45
84
|
|
|
46
|
-
|
|
85
|
+
*
|
|
47
86
|
|
|
48
|
-
|
|
49
|
-
* **FFmpeg**: Diperlukan untuk konversi tingkat lanjut. Gunakan **Menu 3** di dalam aplikasi untuk instalasi otomatis (macOS).
|
|
87
|
+
**Video**: `~/Downloads/media-dl/video/`
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
*
|
|
52
90
|
|
|
53
|
-
|
|
91
|
+
**Audio**: `~/Downloads/media-dl/audio/`
|
|
54
92
|
|
|
55
|
-
*
|
|
56
|
-
|
|
93
|
+
*
|
|
94
|
+
|
|
95
|
+
**Playlist**: Akan dibuatkan sub-folder otomatis sesuai judul playlist agar tetap rapi.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## ❤️ Dukung Pengembangan
|
|
100
|
+
|
|
101
|
+
Media-DL Pro dikembangkan oleh **Ariska Hidayat** dan tersedia secara gratis. Dukungan Anda membantu kami terus memperbarui engine untuk mengatasi perubahan algoritma platform media.
|
|
102
|
+
|
|
103
|
+
| Layanan | Link Donasi |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| **☕ Traktir Kopi** | <br>[Donasi via Midtrans](https://app.midtrans.com/coffee)
|
|
106
|
+
|
|
107
|
+
|
|
|
108
|
+
| **🍕 Beli Pizza** | <br>[Donasi via Midtrans](https://app.midtrans.com/pizza)
|
|
109
|
+
|
|
110
|
+
|
|
|
57
111
|
|
|
58
112
|
---
|
|
59
113
|
|
|
60
114
|
## 📄 Lisensi
|
|
61
115
|
|
|
62
|
-
|
|
116
|
+
Didistribusikan di bawah **Lisensi MIT**. Dibuat dengan ❤️ untuk komunitas open source.
|
|
63
117
|
|
|
64
|
-
---
|
|
118
|
+
---
|
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
|
-
|