weebcli 1.0.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.
Files changed (54) hide show
  1. package/.github/workflows/releases.yml +39 -0
  2. package/LICENSE +400 -0
  3. package/README-EN.md +134 -0
  4. package/README.md +134 -0
  5. package/aur/.SRCINFO +16 -0
  6. package/aur/PKGBUILD +43 -0
  7. package/eslint.config.js +49 -0
  8. package/jsconfig.json +9 -0
  9. package/package.json +45 -0
  10. package/src/constants.js +13 -0
  11. package/src/functions/episodes.js +38 -0
  12. package/src/functions/time.js +27 -0
  13. package/src/functions/variables.js +3 -0
  14. package/src/i18n/en.json +351 -0
  15. package/src/i18n/index.js +80 -0
  16. package/src/i18n/tr.json +348 -0
  17. package/src/index.js +307 -0
  18. package/src/jsdoc.js +72 -0
  19. package/src/sources/allanime.js +195 -0
  20. package/src/sources/animecix.js +223 -0
  21. package/src/sources/animely.js +100 -0
  22. package/src/sources/handlers/allanime.js +318 -0
  23. package/src/sources/handlers/animecix.js +316 -0
  24. package/src/sources/handlers/animely.js +338 -0
  25. package/src/sources/handlers/base.js +391 -0
  26. package/src/sources/handlers/index.js +4 -0
  27. package/src/sources/index.js +80 -0
  28. package/src/utils/anilist.js +193 -0
  29. package/src/utils/data_manager.js +27 -0
  30. package/src/utils/discord.js +86 -0
  31. package/src/utils/download/concurrency.js +27 -0
  32. package/src/utils/download/download.js +485 -0
  33. package/src/utils/download/progress.js +84 -0
  34. package/src/utils/players/mpv.js +251 -0
  35. package/src/utils/players/vlc.js +120 -0
  36. package/src/utils/process_queue.js +121 -0
  37. package/src/utils/resume_watch.js +137 -0
  38. package/src/utils/search.js +39 -0
  39. package/src/utils/search_download.js +21 -0
  40. package/src/utils/speedtest.js +30 -0
  41. package/src/utils/spinner.js +7 -0
  42. package/src/utils/storage/cache.js +42 -0
  43. package/src/utils/storage/config.js +71 -0
  44. package/src/utils/storage/history.js +69 -0
  45. package/src/utils/storage/queue.js +43 -0
  46. package/src/utils/storage/watch_progress.js +104 -0
  47. package/src/utils/system.js +176 -0
  48. package/src/utils/ui/box.js +140 -0
  49. package/src/utils/ui/settings_ui.js +322 -0
  50. package/src/utils/ui/show_history.js +92 -0
  51. package/src/utils/ui/stats.js +67 -0
  52. package/start.js +21 -0
  53. package/tanitim-en.md +66 -0
  54. package/tanitim-tr.md +66 -0
@@ -0,0 +1,86 @@
1
+ // @ts-check
2
+ import RPC from "discord-rpc";
3
+ import { DISCORD_ID, LOGO_URL } from "../constants.js";
4
+ import { t } from "../i18n/index.js";
5
+
6
+ const clientId = DISCORD_ID;
7
+ let client;
8
+ let startTime = Date.now();
9
+ let isReady = false;
10
+
11
+ export async function initDiscordRpc() {
12
+ try {
13
+ client = new RPC.Client({ transport: "ipc" });
14
+
15
+ client.on("ready", () => {
16
+ isReady = true;
17
+ setActivity("Browsing menu");
18
+ });
19
+
20
+ await client.login({ clientId }).catch(() => {});
21
+ } catch {
22
+
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @param {string} details
28
+ * @param {string} [state]
29
+ */
30
+ export function setActivity(details, state) {
31
+ if (!client || !isReady) return;
32
+
33
+ try {
34
+ client.request("SET_ACTIVITY", {
35
+ pid: process.pid,
36
+ activity: {
37
+ details: details,
38
+ state: state,
39
+ timestamps: { start: startTime },
40
+ assets: {
41
+ large_image: LOGO_URL,
42
+ large_text: "Animely CLI"
43
+ },
44
+ buttons: [{ label: t("discord.download"), url: "https://github.com/ewgsta/animely" }]
45
+ }
46
+ }).catch(() => {});
47
+ } catch {
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @param {object} options
53
+ * @param {string} options.animeName
54
+ * @param {string} options.animeImage
55
+ * @param {number} options.episode
56
+ * @param {number} options.totalEpisodes
57
+ */
58
+ export function setWatchingActivity({ animeName, animeImage, episode, totalEpisodes }) {
59
+ if (!client || !isReady) return;
60
+
61
+ const isValidUrl = animeImage && animeImage.startsWith("https://");
62
+ const largeImage = isValidUrl ? animeImage : LOGO_URL;
63
+
64
+ try {
65
+ const activity = {
66
+ details: animeName,
67
+ state: `(${episode}/${totalEpisodes})`,
68
+ timestamps: {
69
+ start: Date.now()
70
+ },
71
+ assets: {
72
+ large_image: largeImage,
73
+ large_text: animeName,
74
+ small_image: LOGO_URL,
75
+ small_text: "Animely"
76
+ },
77
+ buttons: [{ label: t("discord.download"), url: "https://github.com/ewgsta/animely" }]
78
+ };
79
+
80
+ client.request("SET_ACTIVITY", {
81
+ pid: process.pid,
82
+ activity: activity
83
+ }).catch(() => {});
84
+ } catch {
85
+ }
86
+ }
@@ -0,0 +1,27 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @template T
5
+ * @param {(() => Promise<T>)[]} tasks
6
+ * @param {number} limit
7
+ * @returns {Promise<T[]>}
8
+ */
9
+ export async function batch(tasks, limit) {
10
+ const results = [];
11
+ const executing = [];
12
+
13
+ for (const task of tasks) {
14
+ const p = task().then(result => {
15
+ executing.splice(executing.indexOf(p), 1);
16
+ return result;
17
+ });
18
+ results.push(p);
19
+ executing.push(p);
20
+
21
+ if (executing.length >= limit) {
22
+ await Promise.race(executing);
23
+ }
24
+ }
25
+
26
+ return Promise.all(results);
27
+ }
@@ -0,0 +1,485 @@
1
+ // @ts-check
2
+ import axios from "axios";
3
+ import bytes from "bytes";
4
+ import chalk from "chalk";
5
+ import fs from "fs";
6
+ import mime from "mime-types";
7
+ import { pipeline } from "stream/promises";
8
+ import ffmpeg from "fluent-ffmpeg";
9
+ import ffmpegPath from "ffmpeg-static";
10
+ import { spawn } from "child_process";
11
+ import { getConfig } from "../storage/config.js";
12
+ import { commandExists, installPackage } from "../system.js";
13
+ import { t } from "../../i18n/index.js";
14
+
15
+ // @ts-ignore
16
+ ffmpeg.setFfmpegPath(ffmpegPath);
17
+
18
+ /** @type {boolean|null} */
19
+ let ytDlpAvailable = null;
20
+
21
+ /**
22
+ * @param {string} url
23
+ * @param {string} outputPath
24
+ * @param {{ silent?: boolean, onProgress?: (data: { percent: string, downloaded: number, total: number, speed: number, eta: number }) => void }} [options]
25
+ */
26
+ export async function download(url, outputPath, options = { silent: false }) {
27
+ if (!url || typeof url !== "string") {
28
+ throw new Error(t("errors.invalidUrl"));
29
+ }
30
+
31
+ if (url.includes(".m3u8")) {
32
+ return downloadM3U8(url, outputPath, options);
33
+ }
34
+
35
+ const config = getConfig();
36
+ if (config.useAria2) {
37
+ return downloadWithAria2(url, outputPath, options);
38
+ }
39
+
40
+ let startByte = 0;
41
+
42
+
43
+ /**
44
+ * @param {string} url
45
+ * @param {string} outputPath
46
+ * @param {{ silent?: boolean, onProgress?: (data: any) => void }} [options]
47
+ */
48
+ async function downloadWithAria2(url, outputPath, options = { silent: false }) {
49
+ const __dirname = outputPath.substring(0, outputPath.lastIndexOf("\\"));
50
+ const filename = outputPath.substring(outputPath.lastIndexOf("\\") + 1) + ".mp4";
51
+
52
+ const config = getConfig();
53
+ const connections = String(config.aria2Connections || 16);
54
+
55
+ if (fs.existsSync(outputPath + ".mp4")) {
56
+ const existingStats = fs.statSync(outputPath + ".mp4");
57
+ if (existingStats.size > 0) {
58
+ if (!options.silent) console.log(chalk.green(t("errors.fileExistsAria2", { path: outputPath + ".mp4" })));
59
+ if (options.onProgress) options.onProgress({ percent: "100", downloaded: existingStats.size, total: existingStats.size, speed: 0, eta: 0 });
60
+ return;
61
+ }
62
+ }
63
+
64
+ if (!options.silent) console.log(chalk.cyan("\n" + t("errors.aria2Starting")));
65
+
66
+ return new Promise((resolve, reject) => {
67
+ const aria2 = spawn("aria2c", [
68
+ "-x", connections,
69
+ "-s", connections,
70
+ "-k", "1M",
71
+ "-d", __dirname,
72
+ "-o", filename,
73
+ url
74
+ ]);
75
+
76
+ aria2.stdout.on("data", (data) => {
77
+ const output = data.toString();
78
+ const percentMatch = output.match(/\((\d+)%\)/);
79
+ const speedMatch = output.match(/DL:([\w.]+(?:Ki|Mi|Gi)?B)/);
80
+
81
+ if (percentMatch) {
82
+ const percent = percentMatch[1];
83
+ const speedRaw = speedMatch ? speedMatch[1] : "0B";
84
+
85
+ if (options.onProgress) {
86
+ options.onProgress({ percent: percent, downloaded: 0, total: 0, speed: 0, eta: 0 });
87
+ }
88
+
89
+ if (!options.silent) {
90
+ const barLength = 30;
91
+ const filledLength = Math.floor((parseInt(percent) / 100) * barLength);
92
+ const progressBar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
93
+ const out = t("progress.aria2Progress", { bar: progressBar, percent, speed: speedRaw });
94
+ process.stdout.clearLine(0);
95
+ process.stdout.cursorTo(0);
96
+ process.stdout.write(chalk.gray(out));
97
+ }
98
+ }
99
+ });
100
+
101
+ aria2.on("close", (code) => {
102
+ if (code === 0) {
103
+ if (!options.silent) {
104
+ process.stdout.write("\n");
105
+ console.log(chalk.green(t("errors.downloadCompleted", { path: outputPath + ".mp4" })));
106
+ }
107
+ resolve();
108
+ } else {
109
+ reject(new Error(t("errors.aria2Error", { code })));
110
+ }
111
+ });
112
+
113
+ aria2.on("error", (err) => {
114
+ reject(new Error(t("errors.aria2StartFailed", { message: err.message })));
115
+ });
116
+ });
117
+ }
118
+
119
+
120
+ let totalLength = 0;
121
+ let extension = "";
122
+ let fullPath = "";
123
+
124
+ try {
125
+ const headResponse = await axios.head(url, { timeout: 10000 });
126
+ if (headResponse.status === 200) {
127
+ const contentType = headResponse.headers["content-type"] || "video/mp4";
128
+ extension = mime.extension(contentType) || "mp4";
129
+ totalLength = parseInt(headResponse.headers["content-length"] || "0", 10);
130
+ fullPath = `${outputPath}.${extension}`;
131
+
132
+ if (fs.existsSync(fullPath)) {
133
+ const stats = fs.statSync(fullPath);
134
+ const existingSize = stats.size;
135
+
136
+ if (totalLength > 0 && existingSize === totalLength) {
137
+ if (!options.silent) console.log(chalk.green(t("errors.fileExists", { path: fullPath })));
138
+ if (options.onProgress) options.onProgress({ percent: "100", downloaded: totalLength, total: totalLength, speed: 0, eta: 0 });
139
+ return;
140
+ }
141
+
142
+ if (totalLength > 0 && existingSize < totalLength) {
143
+ startByte = existingSize;
144
+ if (!options.silent) console.log(chalk.yellow(t("errors.resumingDownload", { downloaded: bytes(startByte), total: bytes(totalLength) })));
145
+ }
146
+ }
147
+ }
148
+ } catch (error) {
149
+ }
150
+
151
+ let response;
152
+ try {
153
+ const headers = {};
154
+ if (startByte > 0) {
155
+ headers["Range"] = `bytes=${startByte}-`;
156
+ }
157
+
158
+ response = await axios({
159
+ method: "get",
160
+ url,
161
+ responseType: "stream",
162
+ timeout: 30000,
163
+ headers
164
+ });
165
+ } catch (error) {
166
+ if (error.code === "ENOTFOUND") {
167
+ throw new Error(t("errors.noInternet"));
168
+ } else if (error.code === "ECONNABORTED") {
169
+ throw new Error(t("errors.timeout"));
170
+ }
171
+ throw new Error(t("errors.downloadFailed", { message: error.message }));
172
+ }
173
+
174
+ if (response.status !== 200 && response.status !== 206) {
175
+ throw new Error(t("errors.serverError", { status: response.status, statusText: response.statusText }));
176
+ }
177
+
178
+
179
+ const isResuming = response.status === 206;
180
+ if (startByte > 0 && !isResuming) {
181
+ startByte = 0;
182
+ }
183
+
184
+ /** @type {string} */
185
+ const contentType = response.headers["content-type"] || "video/mp4";
186
+ if (!extension) {
187
+ extension = mime.extension(contentType) || "mp4";
188
+ }
189
+ if (!fullPath) {
190
+ fullPath = `${outputPath}.${extension}`;
191
+ }
192
+
193
+ if (isResuming) {
194
+ const contentRange = response.headers["content-range"];
195
+ if (contentRange) {
196
+ const match = contentRange.match(/\/(\d+)$/);
197
+ if (match) {
198
+ totalLength = parseInt(match[1], 10);
199
+ }
200
+ } else {
201
+ totalLength = startByte + parseInt(response.headers["content-length"] || "0", 10);
202
+ }
203
+ } else {
204
+ totalLength = parseInt(response.headers["content-length"] || "0", 10);
205
+ }
206
+
207
+ let downloaded = startByte;
208
+ let lastDownloaded = startByte;
209
+ let lastTime = Date.now();
210
+
211
+ if (!options.silent) {
212
+ if (!isResuming) console.log(chalk.cyan("\n" + t("progress.downloadStarting")));
213
+
214
+ if (totalLength) {
215
+ const percent = ((downloaded / totalLength) * 100).toFixed(1);
216
+ const msg = t("progress.downloadingPercent", { percent, downloaded: bytes(downloaded), total: bytes(totalLength) });
217
+ if (process.stdout.isTTY) {
218
+ process.stdout.clearLine(0);
219
+ process.stdout.cursorTo(0);
220
+ process.stdout.write(chalk.gray(msg));
221
+ } else {
222
+ process.stdout.write(chalk.gray(`\r${msg}`));
223
+ }
224
+ } else {
225
+ const msg = t("progress.uploading", { downloaded: bytes(downloaded) });
226
+ if (process.stdout.isTTY) {
227
+ process.stdout.clearLine(0);
228
+ process.stdout.cursorTo(0);
229
+ process.stdout.write(chalk.gray(msg));
230
+ } else {
231
+ process.stdout.write(chalk.gray(`\r${msg}`));
232
+ }
233
+ }
234
+ }
235
+
236
+
237
+ response.data.on("data", (/** @type {any[]} */ chunk) => {
238
+ downloaded += chunk.length;
239
+
240
+ const now = Date.now();
241
+ const timeDiff = now - lastTime;
242
+ if (timeDiff >= 1000) {
243
+ const speed = (downloaded - lastDownloaded) / (timeDiff / 1000);
244
+ const remaining = totalLength - downloaded;
245
+ const eta = speed > 0 ? Math.ceil(remaining / speed) : 0;
246
+
247
+ lastTime = now;
248
+ lastDownloaded = downloaded;
249
+
250
+ if (totalLength) {
251
+ const percentValue = (downloaded / totalLength) * 100;
252
+ const percent = percentValue.toFixed(1);
253
+
254
+ if (options.onProgress) {
255
+ options.onProgress({ percent, downloaded, total: totalLength, speed, eta });
256
+ }
257
+
258
+ if (!options.silent) {
259
+ const barLength = 30;
260
+ const filledLength = Math.floor((percentValue / 100) * barLength);
261
+ const progressBar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
262
+
263
+ const h = Math.floor(eta / 3600);
264
+ const m = Math.floor((eta % 3600) / 60);
265
+ const s = eta % 60;
266
+ const etaStr = h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
267
+
268
+ const output = t("progress.progressBar", { bar: progressBar, percent, downloaded: bytes(downloaded), total: bytes(totalLength), speed: bytes(speed), eta: etaStr });
269
+ process.stdout.write(`\x1b[2K\x1b[0G${chalk.gray(output)}`);
270
+ }
271
+ } else {
272
+ if (options.onProgress) {
273
+ options.onProgress({ percent: "0", downloaded, total: 0, speed, eta: 0 });
274
+ }
275
+
276
+ if (!options.silent) {
277
+ const output = t("progress.uploadingSpeed", { downloaded: bytes(downloaded), speed: bytes(speed) });
278
+ process.stdout.write(`\x1b[2K\x1b[0G${chalk.gray(output)}`);
279
+ }
280
+ }
281
+ }
282
+ });
283
+
284
+ try {
285
+ const writer = fs.createWriteStream(fullPath, { flags: isResuming ? 'a' : 'w' });
286
+ await pipeline(response.data, writer);
287
+
288
+ if (!options.silent) {
289
+ process.stdout.write("\n");
290
+ console.log(chalk.green(t("errors.fileSaved", { path: fullPath })));
291
+ }
292
+ } catch (error) {
293
+ if (!isResuming && fs.existsSync(fullPath)) {
294
+ fs.unlinkSync(fullPath);
295
+ }
296
+ throw new Error(t("errors.fileWriteError", { message: error.message }));
297
+ }
298
+ }
299
+
300
+
301
+ /**
302
+ * M3U8
303
+ * @param {string} url
304
+ * @param {string} outputPath
305
+ * @param {{ silent?: boolean, onProgress?: (data: { percent: string, downloaded: number, total: number, speed: number, eta: number }) => void }} [options]
306
+ */
307
+ async function downloadM3U8(url, outputPath, options = { silent: false }) {
308
+ const fullPath = `${outputPath}.mp4`;
309
+
310
+ if (fs.existsSync(fullPath)) {
311
+ if (!options.silent) console.log(chalk.green(t("errors.fileExistsM3U8", { path: fullPath })));
312
+ if (options.onProgress) options.onProgress({ percent: "100", downloaded: 0, total: 0, speed: 0, eta: 0 });
313
+ return;
314
+ }
315
+
316
+ const config = getConfig();
317
+
318
+ if (config.useYtDlp) {
319
+ if (ytDlpAvailable === null) {
320
+ ytDlpAvailable = commandExists("yt-dlp");
321
+
322
+ if (!ytDlpAvailable) {
323
+ if (!options.silent) {
324
+ console.log(chalk.yellow("\n" + t("errors.ytDlpNotFound")));
325
+ console.log(chalk.cyan(t("errors.ytDlpInstalling")));
326
+ }
327
+
328
+ const installed = installPackage("yt-dlp");
329
+ if (installed) {
330
+ ytDlpAvailable = true;
331
+ if (!options.silent) console.log(chalk.green(t("errors.ytDlpInstalled")));
332
+ } else {
333
+ ytDlpAvailable = false;
334
+ if (!options.silent) console.log(chalk.yellow(t("errors.ytDlpInstallFailed")));
335
+ }
336
+ }
337
+ }
338
+
339
+ if (ytDlpAvailable) {
340
+ try {
341
+ await downloadM3U8WithYtDlp(url, outputPath, options);
342
+ return;
343
+ } catch (error) {
344
+ if (!options.silent) {
345
+ console.log(chalk.yellow("\n" + t("errors.ytDlpFailed")));
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ if (!options.silent) console.log(chalk.cyan("\n" + t("errors.m3u8Processing")));
352
+
353
+ return new Promise((resolve, reject) => {
354
+ ffmpeg(url)
355
+ .on('error', (err) => {
356
+ reject(new Error(t("errors.ffmpegError", { message: err.message })));
357
+ })
358
+ .on('progress', (progress) => {
359
+ const percent = progress.percent ? progress.percent.toFixed(1) : "0";
360
+ const downloadedBytes = (progress.targetSize || 0) * 1024;
361
+
362
+ if (options.onProgress) {
363
+ options.onProgress({ percent, downloaded: downloadedBytes, total: 0, speed: 0, eta: 0 });
364
+ }
365
+
366
+ if (!options.silent) {
367
+ const output = t("progress.processingPercent", { percent, downloaded: bytes(downloadedBytes) });
368
+ process.stdout.clearLine(0);
369
+ process.stdout.cursorTo(0);
370
+ process.stdout.write(`\x1b[2K\x1b[0G${chalk.gray(output)}`);
371
+ }
372
+ })
373
+ .on('end', () => {
374
+ if (!options.silent) {
375
+ process.stdout.write("\n");
376
+ console.log(chalk.green(t("errors.fileSaved", { path: fullPath })));
377
+ }
378
+ resolve();
379
+ })
380
+ .outputOptions('-c copy')
381
+ .outputOptions('-bsf:a aac_adtstoasc')
382
+ .save(fullPath);
383
+ });
384
+ }
385
+
386
+ /**
387
+ * @param {string} url
388
+ * @param {string} outputPath
389
+ * @param {{ silent?: boolean, onProgress?: (data: { percent: string, downloaded: number, total: number, speed: number, eta: number }) => void }} [options]
390
+ */
391
+ async function downloadM3U8WithYtDlp(url, outputPath, options = { silent: false }) {
392
+ const fullPath = `${outputPath}.mp4`;
393
+ const config = getConfig();
394
+ const connections = String(config.ytDlpConnections || 16);
395
+
396
+ if (!options.silent) console.log(chalk.cyan("\n" + t("errors.ytDlpStarting")));
397
+
398
+ return new Promise((resolve, reject) => {
399
+ const ytDlp = spawn("yt-dlp", [
400
+ "--no-warnings",
401
+ "--no-playlist",
402
+ "-N", connections,
403
+ "--fragment-retries", "infinite",
404
+ "--no-skip-unavailable-fragments",
405
+ "-o", fullPath,
406
+ url
407
+ ]);
408
+
409
+ ytDlp.stdout.on("data", (data) => {
410
+ const output = data.toString();
411
+
412
+ // Progress parsing: [download] 45.2% of ~50.00MiB at 2.50MiB/s ETA 00:15
413
+ const progressMatch = output.match(/\[download\]\s+(\d+\.?\d*)%/);
414
+ const speedMatch = output.match(/at\s+([\d.]+\s*\w+\/s)/);
415
+ const etaMatch = output.match(/ETA\s+(\d+:\d+)/);
416
+
417
+ if (progressMatch) {
418
+ const percent = progressMatch[1];
419
+ const speed = speedMatch ? speedMatch[1] : "";
420
+ const eta = etaMatch ? etaMatch[1] : "";
421
+
422
+ if (options.onProgress) {
423
+ options.onProgress({ percent, downloaded: 0, total: 0, speed: 0, eta: 0 });
424
+ }
425
+
426
+ if (!options.silent) {
427
+ const barLength = 30;
428
+ const percentNum = parseFloat(percent);
429
+ const filledLength = Math.floor((percentNum / 100) * barLength);
430
+ const progressBar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
431
+ const out = t("progress.ytDlpProgress", { bar: progressBar, percent, speed, eta });
432
+ process.stdout.write(`\x1b[2K\x1b[0G${chalk.gray(out)}`);
433
+ }
434
+ }
435
+ });
436
+
437
+ ytDlp.stderr.on("data", (data) => {
438
+ const output = data.toString();
439
+ const progressMatch = output.match(/\[download\]\s+(\d+\.?\d*)%/);
440
+ if (progressMatch && options.onProgress) {
441
+ options.onProgress({ percent: progressMatch[1], downloaded: 0, total: 0, speed: 0, eta: 0 });
442
+ }
443
+ });
444
+
445
+ ytDlp.on("close", (code) => {
446
+ if (code === 0) {
447
+ if (!options.silent) {
448
+ process.stdout.write("\n");
449
+ console.log(chalk.green(t("errors.downloadCompleted", { path: fullPath })));
450
+ }
451
+ resolve();
452
+ } else {
453
+ reject(new Error(t("errors.ytDlpError", { code })));
454
+ }
455
+ });
456
+
457
+ ytDlp.on("error", (err) => {
458
+ reject(new Error(t("errors.ytDlpStartFailed", { message: err.message })));
459
+ });
460
+ });
461
+ }
462
+
463
+ /**
464
+ * @param {string} url
465
+ * @param {string} outputPath
466
+ * @param {{ silent?: boolean, onProgress?: (data: any) => void }} [options]
467
+ * @param {{ count: number, delay: number }} [retryOptions]
468
+ */
469
+ export async function dl(url, outputPath, options, retryOptions = { count: 3, delay: 3000 }) {
470
+ let attempt = 0;
471
+ while (attempt <= retryOptions.count) {
472
+ try {
473
+ await download(url, outputPath, options);
474
+ return;
475
+ } catch (error) {
476
+ attempt++;
477
+ if (attempt > retryOptions.count) throw error;
478
+
479
+ if (!options?.silent) {
480
+ console.log(chalk.yellow("\n" + t("errors.retrying", { attempt, total: retryOptions.count })));
481
+ }
482
+ await new Promise(resolve => setTimeout(resolve, retryOptions.delay));
483
+ }
484
+ }
485
+ }
@@ -0,0 +1,84 @@
1
+ import bytes from "bytes";
2
+
3
+ export class ProgressBar {
4
+ constructor() {
5
+ this.items = new Map();
6
+ this.lastLineCount = 0;
7
+ }
8
+
9
+ /**
10
+ * @param {string|number} id
11
+ * @param {object} data
12
+ * @param {string} [data.name]
13
+ * @param {number} [data.percent]
14
+ * @param {string} [data.status]
15
+ * @param {number} [data.speed]
16
+ * @param {number} [data.eta]
17
+ * @param {number} [data.downloaded]
18
+ * @param {number} [data.total]
19
+ */
20
+ update(id, data) {
21
+ const existing = this.items.get(id) || {
22
+ name: '',
23
+ percent: 0,
24
+ status: 'Bekliyor',
25
+ speed: 0,
26
+ eta: 0,
27
+ downloaded: 0,
28
+ total: 0
29
+ };
30
+ this.items.set(id, { ...existing, ...data });
31
+ this.render();
32
+ }
33
+
34
+ render() {
35
+ const activeItems = Array.from(this.items.values())
36
+ .filter(data => data.status === 'İndiriliyor');
37
+
38
+ if (activeItems.length === 0 && this.lastLineCount === 0) return;
39
+
40
+ const output = activeItems.map(data => {
41
+ const width = 20;
42
+ const percent = typeof data.percent === 'number' && !isNaN(data.percent) ? data.percent : 0;
43
+ const filled = Math.floor((percent / 100) * width);
44
+ const progressBar = "█".repeat(filled) + "░".repeat(width - filled);
45
+
46
+ const name = data.name.length > 25 ? data.name.substring(0, 22) + "..." : data.name.padEnd(25);
47
+
48
+ const eta = data.eta || 0;
49
+ const h = Math.floor(eta / 3600);
50
+ const m = Math.floor((eta % 3600) / 60);
51
+ const s = eta % 60;
52
+ const etaStr = h > 0 ? `${h}s ${m}dk ${s}sn` : m > 0 ? `${m}dk ${s}sn` : `${s}sn`;
53
+
54
+ const speedStr = data.speed ? bytes(data.speed) + '/s' : '0B/s';
55
+ const downloadedStr = data.downloaded ? bytes(data.downloaded) : '0B';
56
+ const totalStr = data.total ? bytes(data.total) : '0B';
57
+
58
+ return `${name}: [${progressBar}] %${percent.toFixed(1)} (${downloadedStr} / ${totalStr}) - ${speedStr} - kalan: ${etaStr}`;
59
+ }).join("\n");
60
+
61
+ if (this.lastLineCount > 0) {
62
+ process.stdout.moveCursor(0, -this.lastLineCount);
63
+ process.stdout.cursorTo(0);
64
+ process.stdout.clearScreenDown();
65
+ }
66
+
67
+ if (activeItems.length > 0) {
68
+ console.log(output);
69
+ this.lastLineCount = output.split('\n').length;
70
+ } else {
71
+ this.lastLineCount = 0;
72
+ }
73
+ }
74
+
75
+ clear() {
76
+ if (this.lastLineCount > 0) {
77
+ process.stdout.moveCursor(0, -this.lastLineCount);
78
+ process.stdout.cursorTo(0);
79
+ process.stdout.clearScreenDown();
80
+ this.lastLineCount = 0;
81
+ }
82
+ this.items.clear();
83
+ }
84
+ }