ytdwn 1.1.2 → 1.1.3

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/README.md CHANGED
@@ -24,12 +24,12 @@
24
24
  - 📁 **Custom Folders** - Set a default download directory
25
25
  - 🚀 **Fast Downloads** - Parallel fragment downloading
26
26
  - 🎨 **Beautiful UI** - Gradient banner, spinners, progress bars
27
- - 📦 **Auto-Setup** - Downloads yt-dlp binary automatically
27
+ - 📦 **Auto-Setup** - Downloads yt-dlp binary and uses static FFmpeg automatically
28
28
 
29
29
  ## 📋 Requirements
30
30
 
31
31
  - [Node.js](https://nodejs.org) >= 18.0.0 or [Bun](https://bun.sh) >= 1.0.0
32
- - [FFmpeg](https://ffmpeg.org) (for video merging and audio conversion)
32
+ - *FFmpeg is handled automatically via static binaries*
33
33
 
34
34
  ## 🚀 Quick Start
35
35
 
package/dist/index.js CHANGED
@@ -1,4 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
20
 
3
21
  // index.ts
4
22
  import { program } from "commander";
@@ -7,7 +25,7 @@ import { program } from "commander";
7
25
  import { join } from "path";
8
26
  var APP_NAME = "ytdwn";
9
27
  var APP_TAGLINE = "YouTube to MP3/MP4 • Fast & Simple";
10
- var APP_VERSION = "1.1.2";
28
+ var APP_VERSION = "1.1.3";
11
29
  var DEFAULT_AUDIO_FORMAT = "mp3";
12
30
  var DEFAULT_AUDIO_QUALITY = "0";
13
31
  var CONCURRENT_FRAGMENTS = "8";
@@ -141,6 +159,7 @@ var SettingsServiceLive = Layer.succeed(SettingsService, {
141
159
  // src/services/BinaryService.ts
142
160
  import { Effect as Effect4, Context as Context2, Layer as Layer2 } from "effect";
143
161
  import YTDlpWrapModule from "yt-dlp-wrap";
162
+ import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
144
163
  import { platform } from "os";
145
164
  import { join as join3 } from "path";
146
165
 
@@ -158,6 +177,7 @@ var fetchBinaryWithRetry = (url, maxRetries = 2) => Effect3.tryPromise({
158
177
  }).pipe(Effect3.retry(Schedule.recurs(maxRetries).pipe(Schedule.addDelay(() => "1 second"))));
159
178
 
160
179
  // src/services/BinaryService.ts
180
+ var ffmpegPath = ffmpegInstaller.path;
161
181
  var YTDlpWrap = YTDlpWrapModule.default ?? YTDlpWrapModule;
162
182
  var BINARY_NAMES = {
163
183
  win32: ["yt-dlp.exe"],
@@ -231,6 +251,25 @@ var BinaryServiceLive = Layer2.effect(BinaryService, Effect4.gen(function* () {
231
251
  message: "yt-dlp binary not found. Run 'ytdwn prepare' first."
232
252
  }));
233
253
  });
254
+ const requireFFmpeg = Effect4.gen(function* () {
255
+ const systemPath = yield* Effect4.tryPromise({
256
+ try: async () => {
257
+ const { exec } = await import("child_process");
258
+ const { promisify } = await import("util");
259
+ const execAsync = promisify(exec);
260
+ const cmd = process.platform === "win32" ? "where ffmpeg" : "which ffmpeg";
261
+ const { stdout } = await execAsync(cmd);
262
+ const outStr = String(stdout);
263
+ const firstLine = outStr.trim().split(`
264
+ `)[0];
265
+ return firstLine ? firstLine.trim() : null;
266
+ },
267
+ catch: () => new Error("FFmpeg lookup failed")
268
+ }).pipe(Effect4.orElseSucceed(() => null));
269
+ if (systemPath)
270
+ return systemPath;
271
+ return ffmpegPath;
272
+ });
234
273
  const getYtDlpWrap = Effect4.gen(function* () {
235
274
  const binaryPath = yield* requireBinary;
236
275
  return new YTDlpWrap(binaryPath);
@@ -253,15 +292,13 @@ var BinaryServiceLive = Layer2.effect(BinaryService, Effect4.gen(function* () {
253
292
  const asset = yield* pickAsset(release);
254
293
  const binaryPath = join3(BIN_DIR, asset.name);
255
294
  const exists = yield* isExecutable(binaryPath);
256
- if (exists) {
257
- yield* settings.setCachedBinaryPath(binaryPath).pipe(Effect4.ignore);
258
- return binaryPath;
259
- }
260
- yield* ensureDirectory(BIN_DIR);
261
- const data = yield* fetchBinaryWithRetry(asset.browser_download_url);
262
- yield* writeFileBinary(binaryPath, data);
263
- if (platform() !== "win32") {
264
- yield* makeExecutable(binaryPath);
295
+ if (!exists) {
296
+ yield* ensureDirectory(BIN_DIR);
297
+ const data = yield* fetchBinaryWithRetry(asset.browser_download_url);
298
+ yield* writeFileBinary(binaryPath, data);
299
+ if (platform() !== "win32") {
300
+ yield* makeExecutable(binaryPath);
301
+ }
265
302
  }
266
303
  yield* settings.setCachedBinaryPath(binaryPath).pipe(Effect4.ignore);
267
304
  return binaryPath;
@@ -270,39 +307,13 @@ var BinaryServiceLive = Layer2.effect(BinaryService, Effect4.gen(function* () {
270
307
  findBinary,
271
308
  requireBinary,
272
309
  getYtDlpWrap,
273
- downloadLatestBinary
310
+ downloadLatestBinary,
311
+ requireFFmpeg
274
312
  };
275
313
  }));
276
314
 
277
315
  // src/services/DownloadService.ts
278
- import { Effect as Effect6, Context as Context3, Layer as Layer3 } from "effect";
279
-
280
- // src/timestamp.ts
281
- import { Effect as Effect5 } from "effect";
282
- var TIME_FORMAT_ERROR = "Invalid time format. Use MM:SS or HH:MM:SS (e.g. 0:02 or 01:23:45).";
283
- var RANGE_FORMAT_ERROR = "Range must be in 'start-end' format (e.g. 0:02-23:10).";
284
- var pad = (value) => value.padStart(2, "0");
285
- function normalizeTimeParts(parts) {
286
- if (parts.length === 2) {
287
- return ["0", parts[0] ?? "0", parts[1] ?? "0"];
288
- }
289
- return [parts[0] ?? "0", parts[1] ?? "0", parts[2] ?? "0"];
290
- }
291
- function parseTimestamp(raw) {
292
- const parts = raw.split(":").map((p) => p.trim());
293
- if (parts.length < 2 || parts.length > 3) {
294
- throw new Error(TIME_FORMAT_ERROR);
295
- }
296
- const [h, m, s] = normalizeTimeParts(parts);
297
- return `${pad(h)}:${pad(m)}:${pad(s)}`;
298
- }
299
- function parseClipRange(range) {
300
- const parts = range.split("-").map((p) => p.trim());
301
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
302
- throw new Error(RANGE_FORMAT_ERROR);
303
- }
304
- return `${parseTimestamp(parts[0])}-${parseTimestamp(parts[1])}`;
305
- }
316
+ import { Effect as Effect5, Context as Context3, Layer as Layer3 } from "effect";
306
317
 
307
318
  // src/colors.ts
308
319
  import pc from "picocolors";
@@ -331,6 +342,9 @@ var c = {
331
342
  };
332
343
 
333
344
  // src/services/DownloadService.ts
345
+ import * as fs from "fs";
346
+ import * as path from "path";
347
+ import { spawn } from "child_process";
334
348
  var SPINNER_FRAMES = [
335
349
  "⠋",
336
350
  "⠙",
@@ -416,7 +430,7 @@ function mapExitCodeToError(code, url) {
416
430
  }
417
431
  var VIDEO_FORMATS = ["mp4", "mkv", "webm", "avi", "mov"];
418
432
  var isVideoFormat = (format) => VIDEO_FORMATS.includes(format.toLowerCase());
419
- function buildArgs(url, options, downloadDir) {
433
+ function buildArgs(url, options, downloadDir, ffmpegPath2) {
420
434
  const format = options.format.toLowerCase();
421
435
  const isVideo = isVideoFormat(format);
422
436
  const baseArgs = [
@@ -428,8 +442,12 @@ function buildArgs(url, options, downloadDir) {
428
442
  "--progress",
429
443
  "--concurrent-fragments",
430
444
  CONCURRENT_FRAGMENTS,
431
- "--no-check-certificates"
445
+ "--no-check-certificates",
446
+ "--restrict-filenames"
432
447
  ];
448
+ if (ffmpegPath2) {
449
+ baseArgs.push("--ffmpeg-location", ffmpegPath2);
450
+ }
433
451
  const formatArgs = isVideo ? [
434
452
  "-f",
435
453
  "bestvideo+bestaudio/best",
@@ -445,14 +463,15 @@ function buildArgs(url, options, downloadDir) {
445
463
  DEFAULT_AUDIO_QUALITY,
446
464
  "--prefer-free-formats"
447
465
  ];
448
- const clipArgs = options.clip ? ["--download-sections", `*${parseClipRange(options.clip)}`] : [];
466
+ const clipArgs = [];
449
467
  return [...baseArgs, ...formatArgs, ...clipArgs];
450
468
  }
451
469
 
452
470
  class DownloadService extends Context3.Tag("DownloadService")() {
453
471
  }
454
- function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
455
- return Effect6.async((resume) => {
472
+ function executeDownload(ytDlpWrap, args, downloadDir, url, options) {
473
+ const quiet = options.quiet ?? false;
474
+ return Effect5.async((resume) => {
456
475
  const spinner = createSpinner("Getting ready...", quiet);
457
476
  const state = {
458
477
  phase: "init",
@@ -469,7 +488,87 @@ function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
469
488
  if (!quiet)
470
489
  clearLine();
471
490
  };
491
+ const parseDurationSeconds = (timeStr) => {
492
+ const parts = timeStr.split(":").map(Number);
493
+ if (parts.length === 3)
494
+ return (parts[0] || 0) * 3600 + (parts[1] || 0) * 60 + (parts[2] || 0);
495
+ if (parts.length === 2)
496
+ return (parts[0] || 0) * 60 + (parts[1] || 0);
497
+ return parts[0] || 0;
498
+ };
499
+ const parseSizeToBytes = (sizeStr) => {
500
+ const match = sizeStr.match(/([\d.]+)\s*([KMG]i?B)/i);
501
+ if (!match || !match[1] || !match[2])
502
+ return 0;
503
+ const val = parseFloat(match[1]);
504
+ const unit = match[2].toUpperCase();
505
+ const multipliers = {
506
+ KB: 1024,
507
+ KIB: 1024,
508
+ MB: 1024 * 1024,
509
+ MIB: 1024 * 1024,
510
+ GB: 1024 * 1024 * 1024,
511
+ GIB: 1024 * 1024 * 1024
512
+ };
513
+ return val * (multipliers[unit] || 1);
514
+ };
515
+ const formatSpeed = (bytesPerSec) => {
516
+ if (bytesPerSec > 1024 * 1024)
517
+ return `${(bytesPerSec / (1024 * 1024)).toFixed(2)}MiB/s`;
518
+ if (bytesPerSec > 1024)
519
+ return `${(bytesPerSec / 1024).toFixed(2)}KiB/s`;
520
+ return `${bytesPerSec.toFixed(0)}B/s`;
521
+ };
522
+ let lastBytes = 0;
523
+ let lastTime = Date.now();
524
+ let currentSpeedStr;
525
+ let totalClipDuration = 0;
526
+ if (options.clip) {
527
+ const parts = options.clip.split("-");
528
+ if (parts.length === 2 && parts[0] && parts[1]) {
529
+ const start = parseDurationSeconds(parts[0]);
530
+ const end = parseDurationSeconds(parts[1]);
531
+ totalClipDuration = end - start;
532
+ }
533
+ }
472
534
  const emitter = ytDlpWrap.exec(args);
535
+ if (emitter.ytDlpProcess?.stderr) {
536
+ emitter.ytDlpProcess.stderr.on("data", (data) => {
537
+ const text = data.toString();
538
+ if (text.includes("frame=") && text.includes("time=")) {
539
+ if (state.phase === "init") {
540
+ state.phase = "downloading";
541
+ spinner.stop();
542
+ }
543
+ const timeMatch = text.match(/time=([\d:.]+)/);
544
+ const sizeMatch = text.match(/(?:Lsize|size)=\s*([\d.]+\s*[KMG]i?B)/i);
545
+ if (timeMatch?.[1]) {
546
+ const currentTime = parseDurationSeconds(timeMatch[1]);
547
+ let percent = 0;
548
+ if (totalClipDuration > 0) {
549
+ percent = currentTime / totalClipDuration * 100;
550
+ if (percent > 100)
551
+ percent = 100;
552
+ }
553
+ if (sizeMatch?.[1]) {
554
+ const currentBytes = parseSizeToBytes(sizeMatch[1]);
555
+ const now = Date.now();
556
+ const deltaMs = now - lastTime;
557
+ if (deltaMs > 500) {
558
+ const deltaBytes = currentBytes - lastBytes;
559
+ if (deltaBytes > 0) {
560
+ const bytesPerSec = deltaBytes / deltaMs * 1000;
561
+ currentSpeedStr = formatSpeed(bytesPerSec);
562
+ }
563
+ lastBytes = currentBytes;
564
+ lastTime = now;
565
+ }
566
+ }
567
+ renderProgress(percent, currentSpeedStr, quiet);
568
+ }
569
+ }
570
+ });
571
+ }
473
572
  emitter.on("progress", (progress) => {
474
573
  if (state.phase === "init") {
475
574
  state.phase = "downloading";
@@ -483,12 +582,25 @@ function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
483
582
  state.outputBuffer += `[${eventType}] ${eventData}
484
583
  `;
485
584
  if (eventType === "ExtractAudio" || eventType === "Merger") {
486
- const match = eventData.match(/Destination:\s*(.+)/);
585
+ let match = eventData.match(/Destination:\s*(.+)/);
586
+ if (!match) {
587
+ match = eventData.match(/Merging formats into "(.+)"/);
588
+ }
487
589
  if (match?.[1]) {
488
590
  state.fileName = match[1].split("/").pop() || null;
489
591
  }
490
592
  }
593
+ if (eventType === "info" && eventData.includes("Downloading 1 time ranges")) {
594
+ if (state.phase === "init") {
595
+ state.phase = "downloading";
596
+ spinner.stop();
597
+ }
598
+ }
491
599
  if (eventType === "download") {
600
+ if (state.phase === "init") {
601
+ state.phase = "downloading";
602
+ spinner.stop();
603
+ }
492
604
  const destMatch = eventData.match(/Destination:\s*(.+)/);
493
605
  if (destMatch?.[1]) {
494
606
  state.fileName = destMatch[1].split("/").pop() || null;
@@ -497,6 +609,18 @@ function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
497
609
  if (sizeMatch?.[1]) {
498
610
  state.fileSize = sizeMatch[1];
499
611
  }
612
+ const cleanData = eventData.replace(/\u001b\[.*?m/g, "");
613
+ const percentMatch = cleanData.match(/(\d+(?:\.\d+)?)%/);
614
+ if (percentMatch?.[1]) {
615
+ if (state.phase !== "downloading") {
616
+ state.phase = "downloading";
617
+ spinner.stop();
618
+ }
619
+ const percent = parseFloat(percentMatch[1]);
620
+ const speedMatch = cleanData.match(/at\s+([~\d.]+\s*[KMG]i?B\/s)/);
621
+ const speed = speedMatch?.[1];
622
+ renderProgress(percent, speed || undefined, quiet);
623
+ }
500
624
  }
501
625
  if ((eventType === "ExtractAudio" || eventType === "ffmpeg") && state.phase !== "converting") {
502
626
  state.phase = "converting";
@@ -521,7 +645,7 @@ function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
521
645
  emitter.on("error", (error) => {
522
646
  cleanup();
523
647
  const outputError = mapOutputToError(String(error), url);
524
- resume(Effect6.fail(outputError ?? new BinaryExecutionError({
648
+ resume(Effect5.fail(outputError ?? new BinaryExecutionError({
525
649
  exitCode: -1,
526
650
  message: String(error)
527
651
  })));
@@ -530,15 +654,15 @@ function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
530
654
  cleanup();
531
655
  const outputError = mapOutputToError(state.outputBuffer, url);
532
656
  if (outputError) {
533
- resume(Effect6.fail(outputError));
657
+ resume(Effect5.fail(outputError));
534
658
  return;
535
659
  }
536
660
  const exitCode = emitter.ytDlpProcess?.exitCode;
537
661
  if (exitCode !== null && exitCode !== undefined && exitCode !== 0) {
538
- resume(Effect6.fail(mapExitCodeToError(exitCode, url)));
662
+ resume(Effect5.fail(mapExitCodeToError(exitCode, url)));
539
663
  return;
540
664
  }
541
- resume(Effect6.succeed({
665
+ resume(Effect5.succeed({
542
666
  filePath: downloadDir,
543
667
  fileName: state.fileName || "audio",
544
668
  fileSize: state.fileSize || undefined
@@ -546,18 +670,76 @@ function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
546
670
  });
547
671
  });
548
672
  }
549
- var DownloadServiceLive = Layer3.effect(DownloadService, Effect6.gen(function* () {
673
+ var DownloadServiceLive = Layer3.effect(DownloadService, Effect5.gen(function* () {
550
674
  const binary = yield* BinaryService;
551
675
  const settings = yield* SettingsService;
552
676
  return {
553
- download: (url, options) => Effect6.gen(function* () {
554
- const [downloadDir, ytDlpWrap] = yield* Effect6.all([
555
- settings.getDownloadDir.pipe(Effect6.flatMap((dir) => ensureDirectory(dir))),
556
- binary.getYtDlpWrap
677
+ download: (url, options) => Effect5.gen(function* () {
678
+ const [downloadDir, ytDlpWrap, ffmpegPath2] = yield* Effect5.all([
679
+ settings.getDownloadDir.pipe(Effect5.flatMap((dir) => ensureDirectory(dir))),
680
+ binary.getYtDlpWrap,
681
+ binary.requireFFmpeg
557
682
  ]);
558
683
  const quiet = options.quiet ?? false;
559
- const args = buildArgs(url, options, downloadDir);
560
- return yield* executeDownload(ytDlpWrap, args, downloadDir, url, quiet);
684
+ const args = buildArgs(url, options, downloadDir, ffmpegPath2);
685
+ const result = yield* executeDownload(ytDlpWrap, args, downloadDir, url, options);
686
+ if (options.clip && ffmpegPath2 && result.fileName && result.filePath) {
687
+ const fullFilePath = path.join(result.filePath, result.fileName);
688
+ if (!fs.existsSync(fullFilePath)) {
689
+ if (!quiet)
690
+ console.error(c.error(`
691
+ Could not find full file for cutting: ${fullFilePath}`));
692
+ return result;
693
+ }
694
+ const parts = options.clip.split("-");
695
+ if (parts.length === 2 && parts[0] && parts[1]) {
696
+ const startTime = parts[0];
697
+ const endTime = parts[1];
698
+ const ext = path.extname(result.fileName);
699
+ const baseName = path.basename(result.fileName, ext);
700
+ const clipFileName = `${baseName}_clip${ext}`;
701
+ const clipFilePath = path.join(result.filePath, clipFileName);
702
+ if (!quiet)
703
+ console.log(c.dim(`
704
+ Cutting clip: ${startTime} to ${endTime}...`));
705
+ yield* Effect5.async((resume) => {
706
+ const ffmpeg = spawn(ffmpegPath2, [
707
+ "-i",
708
+ fullFilePath,
709
+ "-ss",
710
+ startTime,
711
+ "-to",
712
+ endTime,
713
+ "-c",
714
+ "copy",
715
+ "-y",
716
+ clipFilePath
717
+ ]);
718
+ ffmpeg.on("close", (code) => {
719
+ if (code === 0) {
720
+ try {
721
+ fs.unlinkSync(fullFilePath);
722
+ } catch (e) {}
723
+ result.fileName = clipFileName;
724
+ result.fileSize = undefined;
725
+ resume(Effect5.void);
726
+ } else {
727
+ resume(Effect5.fail(new BinaryExecutionError({
728
+ exitCode: code || -1,
729
+ message: "Failed to cut clip with ffmpeg"
730
+ })));
731
+ }
732
+ });
733
+ ffmpeg.on("error", (err) => {
734
+ resume(Effect5.fail(new BinaryExecutionError({
735
+ exitCode: -1,
736
+ message: `FFmpeg spawn error: ${err.message}`
737
+ })));
738
+ });
739
+ });
740
+ }
741
+ }
742
+ return result;
561
743
  })
562
744
  };
563
745
  }));
@@ -567,8 +749,8 @@ var AppLive = DownloadServiceLive.pipe(Layer4.provide(BinaryServiceLive), Layer4
567
749
  var SettingsLive = SettingsServiceLive;
568
750
  var BinaryLive = BinaryServiceLive.pipe(Layer4.provide(SettingsServiceLive));
569
751
  // src/cli/errors.ts
570
- import { Effect as Effect7, Match, Console, pipe } from "effect";
571
- var formatAppError = (error) => Match.value(error).pipe(Match.tag("BinaryNotFoundError", (e) => e.message), Match.tag("VideoNotFoundError", () => "Video not found or private"), Match.tag("InvalidUrlError", () => "Invalid URL format"), Match.tag("AgeRestrictedError", () => "Age-restricted video (login required)"), Match.tag("ConnectionError", (e) => e.message), Match.tag("BinaryExecutionError", (e) => e.message), Match.tag("BinaryDownloadError", () => "Failed to download yt-dlp binary"), Match.tag("NetworkError", (e) => e.message), Match.tag("FileWriteError", () => "Failed to write file"), Match.tag("DirectoryCreateError", () => "Failed to create directory"), Match.exhaustive);
752
+ import { Effect as Effect6, Match, Console, pipe } from "effect";
753
+ var formatAppError = (error) => Match.value(error).pipe(Match.tag("BinaryNotFoundError", (e) => e.message), Match.tag("VideoNotFoundError", () => "Video not found or private"), Match.tag("InvalidUrlError", () => "Invalid URL format"), Match.tag("AgeRestrictedError", () => "Age-restricted video (login required)"), Match.tag("ConnectionError", (e) => e.message), Match.tag("BinaryExecutionError", (e) => e.message), Match.tag("BinaryDownloadError", () => "Failed to download yt-dlp binary"), Match.tag("DownloadError", (e) => `Download failed: ${e.url}`), Match.tag("NetworkError", (e) => e.message), Match.tag("FileWriteError", () => "Failed to write file"), Match.tag("DirectoryCreateError", () => "Failed to create directory"), Match.exhaustive);
572
754
  var isAppError = (error) => error !== null && typeof error === "object" && ("_tag" in error) && typeof error._tag === "string";
573
755
  var formatError = (error) => {
574
756
  if (isAppError(error))
@@ -577,10 +759,10 @@ var formatError = (error) => {
577
759
  return error.message;
578
760
  return String(error);
579
761
  };
580
- var withErrorHandler = (effect) => pipe(effect, Effect7.catchAll((error) => pipe(Console.error(`${c.sym.error} ${c.error("Error:")} ${formatError(error)}`), Effect7.flatMap(() => Effect7.sync(() => process.exit(1))))));
581
- var runCommand = (effect, layer) => Effect7.runPromise(pipe(effect, Effect7.provide(layer), withErrorHandler));
762
+ var withErrorHandler = (effect) => pipe(effect, Effect6.catchAll((error) => pipe(Console.error(`${c.sym.error} ${c.error("Error:")} ${formatError(error)}`), Effect6.flatMap(() => Effect6.sync(() => process.exit(1))))));
763
+ var runCommand = (effect, layer) => Effect6.runPromise(pipe(effect, Effect6.provide(layer), withErrorHandler));
582
764
  // src/cli/commands.ts
583
- import { Effect as Effect8 } from "effect";
765
+ import { Effect as Effect7 } from "effect";
584
766
  // src/banner.ts
585
767
  import cfonts from "cfonts";
586
768
  import gradient from "gradient-string";
@@ -607,40 +789,41 @@ function showBanner() {
607
789
  }
608
790
 
609
791
  // src/cli/commands.ts
610
- var log = (message) => Effect8.sync(() => console.log(message));
792
+ var log = (message) => Effect7.sync(() => console.log(message));
611
793
  var logSuccess = (message) => log(`${c.sym.success} ${c.success(message)}`);
612
794
  var logInfo = (message) => log(c.info(message));
613
795
  var logDim = (message) => log(c.dim(message));
614
- var prepareCommand = Effect8.gen(function* () {
796
+ var prepareCommand = Effect7.gen(function* () {
615
797
  const binary = yield* BinaryService;
616
798
  const existing = yield* binary.findBinary;
617
- if (existing) {
799
+ const ffmpeg = yield* binary.requireFFmpeg;
800
+ if (existing && ffmpeg) {
618
801
  yield* logSuccess("Ready");
619
802
  return;
620
803
  }
621
- yield* logDim("Downloading yt-dlp...");
804
+ yield* logDim("Downloading dependencies...");
622
805
  yield* binary.downloadLatestBinary;
623
806
  yield* logSuccess("Ready");
624
807
  });
625
- var setFolderCommand = (path, options) => Effect8.gen(function* () {
808
+ var setFolderCommand = (path2, options) => Effect7.gen(function* () {
626
809
  const settings = yield* SettingsService;
627
810
  if (options?.reset) {
628
811
  yield* settings.resetDownloadDir;
629
812
  yield* logSuccess("Reset to current directory");
630
813
  return;
631
814
  }
632
- if (path) {
633
- yield* settings.setDownloadDir(path);
634
- yield* log(`${c.sym.success} ${c.info(path)}`);
815
+ if (path2) {
816
+ yield* settings.setDownloadDir(path2);
817
+ yield* log(`${c.sym.success} ${c.info(path2)}`);
635
818
  } else {
636
819
  const dir = yield* settings.getDownloadDir;
637
820
  yield* logInfo(dir);
638
821
  }
639
822
  });
640
- var downloadCommand = (url, options) => Effect8.gen(function* () {
823
+ var downloadCommand = (url, options) => Effect7.gen(function* () {
641
824
  const quiet = options.quiet ?? false;
642
825
  if (!quiet) {
643
- yield* Effect8.sync(() => showBanner());
826
+ yield* Effect7.sync(() => showBanner());
644
827
  yield* log(`${c.dim("URL:")} ${c.info(url)}
645
828
  `);
646
829
  }
@@ -674,7 +857,7 @@ var configureCli = () => {
674
857
  }
675
858
  });
676
859
  program.command("prepare").description("Download yt-dlp binary").action(() => runCommand(prepareCommand, BinaryLive));
677
- program.command("setDefaultFolder [path]").description("Set or view default download folder").option("-r, --reset", "Reset to current directory").action((path, opts) => runCommand(setFolderCommand(path, opts), SettingsLive));
860
+ program.command("setDefaultFolder [path]").description("Set or view default download folder").option("-r, --reset", "Reset to current directory").action((path2, opts) => runCommand(setFolderCommand(path2, opts), SettingsLive));
678
861
  return program;
679
862
  };
680
863
  var main = () => {
@@ -1,6 +1,6 @@
1
1
  import { Effect, Layer } from "effect";
2
- import { BinaryNotFoundError, VideoNotFoundError, InvalidUrlError, AgeRestrictedError, ConnectionError, BinaryExecutionError, BinaryDownloadError, NetworkError, FileWriteError, DirectoryCreateError } from "../lib/errors";
3
- export type AppError = BinaryNotFoundError | VideoNotFoundError | InvalidUrlError | AgeRestrictedError | ConnectionError | BinaryExecutionError | BinaryDownloadError | NetworkError | FileWriteError | DirectoryCreateError;
2
+ import { BinaryNotFoundError, VideoNotFoundError, InvalidUrlError, AgeRestrictedError, ConnectionError, BinaryExecutionError, BinaryDownloadError, DownloadError, NetworkError, FileWriteError, DirectoryCreateError } from "../lib/errors";
3
+ export type AppError = BinaryNotFoundError | VideoNotFoundError | InvalidUrlError | AgeRestrictedError | ConnectionError | BinaryExecutionError | BinaryDownloadError | DownloadError | NetworkError | FileWriteError | DirectoryCreateError;
4
4
  export declare const formatError: (error: unknown) => string;
5
5
  /**
6
6
  * Runs an effect with error handling. Main entry point for CLI commands.
@@ -1,6 +1,6 @@
1
1
  export declare const APP_NAME = "ytdwn";
2
2
  export declare const APP_TAGLINE = "YouTube to MP3/MP4 \u2022 Fast & Simple";
3
- export declare const APP_VERSION = "1.1.2";
3
+ export declare const APP_VERSION = "1.1.3";
4
4
  export declare const DEFAULT_AUDIO_FORMAT = "mp3";
5
5
  export declare const DEFAULT_AUDIO_QUALITY = "0";
6
6
  export declare const DEFAULT_VIDEO_FORMAT = "mp4";
@@ -5,9 +5,10 @@ import { BinaryNotFoundError, BinaryDownloadError, type DownloadError, type File
5
5
  import { SettingsService } from "./SettingsService";
6
6
  declare const BinaryService_base: Context.TagClass<BinaryService, "BinaryService", {
7
7
  readonly findBinary: Effect.Effect<string | null>;
8
- readonly requireBinary: Effect.Effect<string, BinaryNotFoundError>;
8
+ readonly requireFFmpeg: Effect.Effect<string>;
9
9
  readonly getYtDlpWrap: Effect.Effect<InstanceType<typeof YTDlpWrap>, BinaryNotFoundError>;
10
10
  readonly downloadLatestBinary: Effect.Effect<string, BinaryDownloadError | DownloadError | FileWriteError | DirectoryCreateError>;
11
+ readonly requireBinary: Effect.Effect<string, BinaryNotFoundError>;
11
12
  }>;
12
13
  export declare class BinaryService extends BinaryService_base {
13
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ytdwn",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "A fast and simple CLI tool to download audio and video from YouTube",
5
5
  "author": {
6
6
  "name": "Batikan Kutluer",
@@ -54,6 +54,7 @@
54
54
  "prepublishOnly": "bun run build"
55
55
  },
56
56
  "dependencies": {
57
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
57
58
  "cfonts": "^3.3.1",
58
59
  "commander": "^14.0.2",
59
60
  "effect": "^3.19.14",
@@ -66,4 +67,4 @@
66
67
  "@types/node": "^22.0.0",
67
68
  "typescript": "^5.7.0"
68
69
  }
69
- }
70
+ }