@jnzlab/easy-ytdlp 1.1.3 → 1.2.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/CHANGELOG.md +22 -1
- package/dist/cli.js +359 -173
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.2.0 - 2026-09-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- When a download or metadata fetch fails because YouTube's JS challenge could
|
|
8
|
+
not be solved, easy-ytdlp now offers to install Deno for you
|
|
9
|
+
(`curl -fsSL https://deno.land/install.sh | sh`, or the PowerShell one-liner
|
|
10
|
+
on Windows) and retries automatically once it is installed. Declining prints
|
|
11
|
+
the command so you can install it yourself and come back.
|
|
12
|
+
- yt-dlp is now pointed at a Deno binary in `~/.deno/bin` even when that
|
|
13
|
+
directory is not yet on `PATH` (which is the case right after installing it).
|
|
14
|
+
|
|
15
|
+
## 1.1.4 - 2026-09-08
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
6
18
|
|
|
19
|
+
- Interactive mode no longer fails with "Failed to fetch metadata for any of
|
|
20
|
+
the provided URLs" on videos that have no pre-merged format. `yt-dlp-wrap`
|
|
21
|
+
silently added `-f best` to the metadata call, which YouTube now rejects for
|
|
22
|
+
many videos; the metadata fetch passes `--ignore-no-formats-error` and its
|
|
23
|
+
own format flag instead. When every URL fails, the underlying yt-dlp error is
|
|
24
|
+
now shown instead of a generic message.
|
|
25
|
+
- `--version` now reads the version from `package.json` instead of a
|
|
26
|
+
hardcoded constant that had drifted from the published version.
|
|
27
|
+
- Removed an accidental dependency of the package on itself.
|
|
7
28
|
- Pasting a bare YouTube playlist link no longer fails/hangs at startup. The
|
|
8
29
|
metadata fetch now uses `--flat-playlist` for playlist URLs (fast, one small
|
|
9
30
|
line per video) and shows the playlist title + video count instead of dumping
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import * as
|
|
3
|
+
import * as p4 from "@clack/prompts";
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/binary.ts
|
|
@@ -252,16 +252,179 @@ function needsFfmpeg(mode, extras) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// src/youtube-compat.ts
|
|
255
|
-
import { accessSync, constants as
|
|
256
|
-
import { join as
|
|
255
|
+
import { accessSync as accessSync2, constants as constants4, readdirSync } from "fs";
|
|
256
|
+
import { join as join4 } from "path";
|
|
257
|
+
|
|
258
|
+
// src/deno.ts
|
|
259
|
+
import { spawn } from "child_process";
|
|
260
|
+
import { accessSync, constants as constants3 } from "fs";
|
|
261
|
+
import { homedir, platform as platform3 } from "os";
|
|
262
|
+
import { delimiter as delimiter2, dirname, join as join3 } from "path";
|
|
263
|
+
import * as p2 from "@clack/prompts";
|
|
264
|
+
|
|
265
|
+
// src/ui.ts
|
|
266
|
+
import * as p from "@clack/prompts";
|
|
267
|
+
function contentWidth(pad = 8) {
|
|
268
|
+
const cols = process.stdout.columns ?? 80;
|
|
269
|
+
return Math.max(40, cols - pad);
|
|
270
|
+
}
|
|
271
|
+
function wrapText(text2, width = contentWidth()) {
|
|
272
|
+
return text2.split(/\r?\n/).flatMap((line) => wrapLine(line, width)).join("\n");
|
|
273
|
+
}
|
|
274
|
+
function wrapLine(line, width) {
|
|
275
|
+
if (line.length <= width) return [line || " "];
|
|
276
|
+
const out = [];
|
|
277
|
+
let rest = line;
|
|
278
|
+
while (rest.length > width) {
|
|
279
|
+
let breakAt = rest.lastIndexOf(" ", width);
|
|
280
|
+
if (breakAt < Math.floor(width * 0.5)) breakAt = width;
|
|
281
|
+
out.push(rest.slice(0, breakAt).trimEnd());
|
|
282
|
+
rest = rest.slice(breakAt).trimStart();
|
|
283
|
+
}
|
|
284
|
+
if (rest.length) out.push(rest);
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
function showNote(body, title) {
|
|
288
|
+
p.note(wrapText(body, contentWidth(14)), title);
|
|
289
|
+
}
|
|
290
|
+
function showSaved(paths3) {
|
|
291
|
+
if (paths3.length === 0) return;
|
|
292
|
+
if (paths3.length === 1) {
|
|
293
|
+
p.log.success(paths3[0]);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
p.log.success(`Saved ${paths3.length} files`);
|
|
297
|
+
for (const filePath of paths3) {
|
|
298
|
+
p.log.info(filePath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function showCommand(command) {
|
|
302
|
+
p.log.step("yt-dlp command");
|
|
303
|
+
for (const line of wrapText(command, contentWidth(4)).split("\n")) {
|
|
304
|
+
console.log(` ${line}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/deno.ts
|
|
309
|
+
var INSTALL_SH = "curl -fsSL https://deno.land/install.sh | sh";
|
|
310
|
+
var INSTALL_PS = "irm https://deno.land/install.ps1 | iex";
|
|
311
|
+
function isJsRuntimeError(message) {
|
|
312
|
+
const lower = message.toLowerCase();
|
|
313
|
+
return lower.includes("http error 403") || lower.includes("403: forbidden") || lower.includes("javascript runtime") || lower.includes("js challenge") || lower.includes("n challenge");
|
|
314
|
+
}
|
|
315
|
+
function denoInstallCommand() {
|
|
316
|
+
return platform3() === "win32" ? INSTALL_PS : INSTALL_SH;
|
|
317
|
+
}
|
|
318
|
+
function isExecutable2(filePath) {
|
|
319
|
+
try {
|
|
320
|
+
accessSync(filePath, constants3.X_OK);
|
|
321
|
+
return true;
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function denoFilename() {
|
|
327
|
+
return platform3() === "win32" ? "deno.exe" : "deno";
|
|
328
|
+
}
|
|
329
|
+
function knownDenoDirs() {
|
|
330
|
+
const dirs = [];
|
|
331
|
+
if (process.env.DENO_INSTALL) dirs.push(join3(process.env.DENO_INSTALL, "bin"));
|
|
332
|
+
if (process.env.DENO_INSTALL_ROOT) dirs.push(process.env.DENO_INSTALL_ROOT);
|
|
333
|
+
const home = homedir();
|
|
334
|
+
if (home) dirs.push(join3(home, ".deno", "bin"));
|
|
335
|
+
return dirs;
|
|
336
|
+
}
|
|
337
|
+
function findDeno() {
|
|
338
|
+
const name = denoFilename();
|
|
339
|
+
const dirs = [
|
|
340
|
+
...(process.env.PATH ?? "").split(delimiter2).filter(Boolean),
|
|
341
|
+
...knownDenoDirs()
|
|
342
|
+
];
|
|
343
|
+
for (const dir of dirs) {
|
|
344
|
+
const candidate = join3(dir, name);
|
|
345
|
+
if (isExecutable2(candidate)) return candidate;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
function ensureDenoOnPath(denoPath) {
|
|
350
|
+
const dir = dirname(denoPath);
|
|
351
|
+
const entries = (process.env.PATH ?? "").split(delimiter2);
|
|
352
|
+
if (entries.includes(dir)) return;
|
|
353
|
+
process.env.PATH = [dir, ...entries].filter(Boolean).join(delimiter2);
|
|
354
|
+
}
|
|
355
|
+
function showManualInstructions() {
|
|
356
|
+
showNote(
|
|
357
|
+
[
|
|
358
|
+
"Install Deno yourself with:",
|
|
359
|
+
"",
|
|
360
|
+
` ${denoInstallCommand()}`,
|
|
361
|
+
"",
|
|
362
|
+
"Then re-run easy-ytdlp \u2014 it will pick Deno up automatically."
|
|
363
|
+
].join("\n"),
|
|
364
|
+
"Install Deno"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
function runInstallCommand() {
|
|
368
|
+
const isWindows = platform3() === "win32";
|
|
369
|
+
const command = isWindows ? "powershell.exe" : "sh";
|
|
370
|
+
const args = isWindows ? ["-NoProfile", "-Command", INSTALL_PS] : ["-c", INSTALL_SH];
|
|
371
|
+
return new Promise((resolve) => {
|
|
372
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
373
|
+
child.on("error", (err) => {
|
|
374
|
+
p2.log.error(`Could not run the installer: ${err.message}`);
|
|
375
|
+
resolve(false);
|
|
376
|
+
});
|
|
377
|
+
child.on("close", (code) => resolve(code === 0));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async function offerDenoInstall(options = {}) {
|
|
381
|
+
const existing = findDeno();
|
|
382
|
+
if (existing) {
|
|
383
|
+
ensureDenoOnPath(existing);
|
|
384
|
+
p2.log.info(`Found Deno at ${existing} \u2014 using it for this run.`);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (options.canPrompt === false || !process.stdin.isTTY) {
|
|
388
|
+
showManualInstructions();
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
p2.log.info(`Install command: ${denoInstallCommand()}`);
|
|
392
|
+
const answer = await p2.confirm({
|
|
393
|
+
message: "Install Deno now?",
|
|
394
|
+
initialValue: true
|
|
395
|
+
});
|
|
396
|
+
if (p2.isCancel(answer) || !answer) {
|
|
397
|
+
showManualInstructions();
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
p2.log.step("Installing Deno\u2026");
|
|
401
|
+
const ok = await runInstallCommand();
|
|
402
|
+
if (!ok) {
|
|
403
|
+
p2.log.error("Deno installation failed.");
|
|
404
|
+
showManualInstructions();
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
const installed = findDeno();
|
|
408
|
+
if (!installed) {
|
|
409
|
+
p2.log.warn(
|
|
410
|
+
"The installer finished but no deno binary was found. Open a new terminal and re-run easy-ytdlp."
|
|
411
|
+
);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
ensureDenoOnPath(installed);
|
|
415
|
+
p2.log.success(`Deno installed: ${installed}`);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/youtube-compat.ts
|
|
257
420
|
var MIN_RECOMMENDED_MAJOR = 22;
|
|
258
421
|
function nodeMajor(version) {
|
|
259
422
|
const m = version.replace(/^v/, "").split(".")[0];
|
|
260
423
|
return Number(m) || 0;
|
|
261
424
|
}
|
|
262
|
-
function
|
|
425
|
+
function isExecutable3(filePath) {
|
|
263
426
|
try {
|
|
264
|
-
|
|
427
|
+
accessSync2(filePath, constants4.X_OK);
|
|
265
428
|
return true;
|
|
266
429
|
} catch {
|
|
267
430
|
return false;
|
|
@@ -274,7 +437,7 @@ function resolveNodeForYtDlp() {
|
|
|
274
437
|
}
|
|
275
438
|
const nvmDir = process.env.NVM_DIR;
|
|
276
439
|
if (nvmDir) {
|
|
277
|
-
const versionsRoot =
|
|
440
|
+
const versionsRoot = join4(nvmDir, "versions", "node");
|
|
278
441
|
try {
|
|
279
442
|
const dirs = readdirSync(versionsRoot).filter((name) => /^v\d+\.\d+\.\d+$/.test(name)).sort((a, b) => {
|
|
280
443
|
const pa = a.slice(1).split(".").map(Number);
|
|
@@ -288,8 +451,8 @@ function resolveNodeForYtDlp() {
|
|
|
288
451
|
for (const dir of dirs) {
|
|
289
452
|
const major = nodeMajor(dir);
|
|
290
453
|
if (major < MIN_RECOMMENDED_MAJOR) continue;
|
|
291
|
-
const candidate =
|
|
292
|
-
if (
|
|
454
|
+
const candidate = join4(versionsRoot, dir, "bin", "node");
|
|
455
|
+
if (isExecutable3(candidate)) {
|
|
293
456
|
return { path: candidate, major };
|
|
294
457
|
}
|
|
295
458
|
}
|
|
@@ -300,7 +463,9 @@ function resolveNodeForYtDlp() {
|
|
|
300
463
|
}
|
|
301
464
|
function youtubeCompatFlags() {
|
|
302
465
|
const { path } = resolveNodeForYtDlp();
|
|
466
|
+
const deno = findDeno();
|
|
303
467
|
return [
|
|
468
|
+
...deno ? ["--js-runtimes", `deno:${deno}`] : [],
|
|
304
469
|
"--js-runtimes",
|
|
305
470
|
`node:${path}`,
|
|
306
471
|
// Fallback if the cached binary's bundled EJS scripts are missing/outdated
|
|
@@ -326,14 +491,15 @@ function humanizeError(raw) {
|
|
|
326
491
|
`yt-dlp said: ${raw.trim()}`
|
|
327
492
|
].join("\n");
|
|
328
493
|
}
|
|
329
|
-
if (
|
|
494
|
+
if (isJsRuntimeError(lower)) {
|
|
330
495
|
return [
|
|
331
496
|
"YouTube blocked the download (often HTTP 403) \u2014 usually because a JavaScript runtime is needed to solve YouTube challenges.",
|
|
332
497
|
"",
|
|
333
|
-
"easy-ytdlp
|
|
498
|
+
"easy-ytdlp enables Node automatically, but yt-dlp solves these challenges most reliably with Deno.",
|
|
499
|
+
"",
|
|
500
|
+
"If Deno does not help, also try:",
|
|
334
501
|
" 1. Update the yt-dlp binary: easy-ytdlp update-binary",
|
|
335
502
|
" 2. Use Node 22+ (recommended by yt-dlp for the JS solver)",
|
|
336
|
-
" 3. Or install Deno: https://deno.land (yt-dlp\u2019s preferred runtime)",
|
|
337
503
|
"",
|
|
338
504
|
"More detail: https://github.com/yt-dlp/yt-dlp/wiki/EJS",
|
|
339
505
|
"",
|
|
@@ -391,8 +557,15 @@ function isPlaylistOnlyUrl(url) {
|
|
|
391
557
|
return true;
|
|
392
558
|
}
|
|
393
559
|
function metadataArgs(url) {
|
|
394
|
-
const scopeFlags = isPlaylistOnlyUrl(url) ? ["--flat-playlist"
|
|
395
|
-
return [
|
|
560
|
+
const scopeFlags = isPlaylistOnlyUrl(url) ? ["--flat-playlist"] : ["--no-playlist"];
|
|
561
|
+
return [
|
|
562
|
+
...youtubeCompatFlags(),
|
|
563
|
+
...scopeFlags,
|
|
564
|
+
"--ignore-no-formats-error",
|
|
565
|
+
"-f",
|
|
566
|
+
"b",
|
|
567
|
+
url
|
|
568
|
+
];
|
|
396
569
|
}
|
|
397
570
|
function normalizePlaylistMeta(raw, url) {
|
|
398
571
|
const entries = Array.isArray(raw) ? raw : raw && typeof raw === "object" ? [raw] : [];
|
|
@@ -753,16 +926,16 @@ function looksLikeUrl(s) {
|
|
|
753
926
|
}
|
|
754
927
|
|
|
755
928
|
// src/questions.ts
|
|
756
|
-
import * as
|
|
757
|
-
import { homedir } from "os";
|
|
758
|
-
import { join as
|
|
929
|
+
import * as p3 from "@clack/prompts";
|
|
930
|
+
import { homedir as homedir2 } from "os";
|
|
931
|
+
import { join as join6 } from "path";
|
|
759
932
|
|
|
760
933
|
// src/preferences.ts
|
|
761
934
|
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
762
|
-
import { dirname, join as
|
|
935
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
763
936
|
import envPaths2 from "env-paths";
|
|
764
937
|
var paths2 = envPaths2("easy-ytdlp");
|
|
765
|
-
var PREFS_PATH =
|
|
938
|
+
var PREFS_PATH = join5(paths2.config, "preferences.json");
|
|
766
939
|
async function loadPreferences() {
|
|
767
940
|
try {
|
|
768
941
|
const raw = await readFile(PREFS_PATH, "utf-8");
|
|
@@ -773,61 +946,18 @@ async function loadPreferences() {
|
|
|
773
946
|
}
|
|
774
947
|
}
|
|
775
948
|
async function savePreferences(prefs) {
|
|
776
|
-
await mkdir2(
|
|
949
|
+
await mkdir2(dirname2(PREFS_PATH), { recursive: true });
|
|
777
950
|
await writeFile(PREFS_PATH, `${JSON.stringify(prefs, null, 2)}
|
|
778
951
|
`, "utf-8");
|
|
779
952
|
}
|
|
780
953
|
|
|
781
|
-
// src/ui.ts
|
|
782
|
-
import * as p from "@clack/prompts";
|
|
783
|
-
function contentWidth(pad = 8) {
|
|
784
|
-
const cols = process.stdout.columns ?? 80;
|
|
785
|
-
return Math.max(40, cols - pad);
|
|
786
|
-
}
|
|
787
|
-
function wrapText(text2, width = contentWidth()) {
|
|
788
|
-
return text2.split(/\r?\n/).flatMap((line) => wrapLine(line, width)).join("\n");
|
|
789
|
-
}
|
|
790
|
-
function wrapLine(line, width) {
|
|
791
|
-
if (line.length <= width) return [line || " "];
|
|
792
|
-
const out = [];
|
|
793
|
-
let rest = line;
|
|
794
|
-
while (rest.length > width) {
|
|
795
|
-
let breakAt = rest.lastIndexOf(" ", width);
|
|
796
|
-
if (breakAt < Math.floor(width * 0.5)) breakAt = width;
|
|
797
|
-
out.push(rest.slice(0, breakAt).trimEnd());
|
|
798
|
-
rest = rest.slice(breakAt).trimStart();
|
|
799
|
-
}
|
|
800
|
-
if (rest.length) out.push(rest);
|
|
801
|
-
return out;
|
|
802
|
-
}
|
|
803
|
-
function showNote(body, title) {
|
|
804
|
-
p.note(wrapText(body, contentWidth(14)), title);
|
|
805
|
-
}
|
|
806
|
-
function showSaved(paths3) {
|
|
807
|
-
if (paths3.length === 0) return;
|
|
808
|
-
if (paths3.length === 1) {
|
|
809
|
-
p.log.success(paths3[0]);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
p.log.success(`Saved ${paths3.length} files`);
|
|
813
|
-
for (const filePath of paths3) {
|
|
814
|
-
p.log.info(filePath);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
function showCommand(command) {
|
|
818
|
-
p.log.step("yt-dlp command");
|
|
819
|
-
for (const line of wrapText(command, contentWidth(4)).split("\n")) {
|
|
820
|
-
console.log(` ${line}`);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
954
|
// src/questions.ts
|
|
825
|
-
function
|
|
826
|
-
return
|
|
955
|
+
function isCancel3(value) {
|
|
956
|
+
return p3.isCancel(value);
|
|
827
957
|
}
|
|
828
958
|
function exitOnCancel(value) {
|
|
829
|
-
if (
|
|
830
|
-
|
|
959
|
+
if (isCancel3(value)) {
|
|
960
|
+
p3.cancel("Cancelled.");
|
|
831
961
|
process.exit(0);
|
|
832
962
|
}
|
|
833
963
|
}
|
|
@@ -937,13 +1067,13 @@ async function promptUrls(initial) {
|
|
|
937
1067
|
if (initial && initial.length > 0) {
|
|
938
1068
|
for (const u of initial) {
|
|
939
1069
|
if (!looksLikeUrl2(u)) {
|
|
940
|
-
|
|
1070
|
+
p3.log.error(`Invalid URL: ${u}`);
|
|
941
1071
|
process.exit(1);
|
|
942
1072
|
}
|
|
943
1073
|
}
|
|
944
1074
|
return initial;
|
|
945
1075
|
}
|
|
946
|
-
const input = await
|
|
1076
|
+
const input = await p3.text({
|
|
947
1077
|
message: "Paste video URL(s)",
|
|
948
1078
|
placeholder: "One or more URLs, separated by spaces or commas",
|
|
949
1079
|
initialValue: "",
|
|
@@ -960,7 +1090,7 @@ async function promptUrls(initial) {
|
|
|
960
1090
|
return splitUrlList(String(input).trim());
|
|
961
1091
|
}
|
|
962
1092
|
async function askQuestions(url, meta) {
|
|
963
|
-
const mode = await
|
|
1093
|
+
const mode = await p3.select({
|
|
964
1094
|
message: "What do you want to download?",
|
|
965
1095
|
options: [
|
|
966
1096
|
{ value: "video", label: "Video (with audio)" },
|
|
@@ -995,13 +1125,13 @@ async function askQuestions(url, meta) {
|
|
|
995
1125
|
label: "Choose from available resolutions\u2026"
|
|
996
1126
|
});
|
|
997
1127
|
}
|
|
998
|
-
const q = await
|
|
1128
|
+
const q = await p3.select({
|
|
999
1129
|
message: "Video quality?",
|
|
1000
1130
|
options: qualityOpts
|
|
1001
1131
|
});
|
|
1002
1132
|
exitOnCancel(q);
|
|
1003
1133
|
if (q === "pick") {
|
|
1004
|
-
const picked = await
|
|
1134
|
+
const picked = await p3.select({
|
|
1005
1135
|
message: "Available resolutions",
|
|
1006
1136
|
options: resolutions.map((h) => ({
|
|
1007
1137
|
value: String(h),
|
|
@@ -1017,7 +1147,7 @@ async function askQuestions(url, meta) {
|
|
|
1017
1147
|
}
|
|
1018
1148
|
}
|
|
1019
1149
|
if (selectedMode === "audio") {
|
|
1020
|
-
const fmt = await
|
|
1150
|
+
const fmt = await p3.select({
|
|
1021
1151
|
message: "Audio format?",
|
|
1022
1152
|
options: [
|
|
1023
1153
|
{ value: "mp3", label: "mp3" },
|
|
@@ -1030,7 +1160,7 @@ async function askQuestions(url, meta) {
|
|
|
1030
1160
|
});
|
|
1031
1161
|
exitOnCancel(fmt);
|
|
1032
1162
|
audioFormat = fmt;
|
|
1033
|
-
const aq = await
|
|
1163
|
+
const aq = await p3.select({
|
|
1034
1164
|
message: "Audio quality?",
|
|
1035
1165
|
options: [
|
|
1036
1166
|
{ value: "best", label: "Best" },
|
|
@@ -1044,10 +1174,10 @@ async function askQuestions(url, meta) {
|
|
|
1044
1174
|
subMode = "write";
|
|
1045
1175
|
const langs = availableSubtitleLangs(meta);
|
|
1046
1176
|
if (langs.length === 0) {
|
|
1047
|
-
|
|
1177
|
+
p3.log.warn('No subtitle languages found in metadata \u2014 will request "all".');
|
|
1048
1178
|
subLangs = ["all"];
|
|
1049
1179
|
} else {
|
|
1050
|
-
const picked = await
|
|
1180
|
+
const picked = await p3.multiselect({
|
|
1051
1181
|
message: "Which subtitle languages?",
|
|
1052
1182
|
options: [
|
|
1053
1183
|
{ value: "all", label: "All languages" },
|
|
@@ -1061,8 +1191,8 @@ async function askQuestions(url, meta) {
|
|
|
1061
1191
|
}
|
|
1062
1192
|
}
|
|
1063
1193
|
const prefs = await loadPreferences();
|
|
1064
|
-
const defaultDir = prefs.outputDir ??
|
|
1065
|
-
const outDir = await
|
|
1194
|
+
const defaultDir = prefs.outputDir ?? join6(homedir2(), "Downloads");
|
|
1195
|
+
const outDir = await p3.text({
|
|
1066
1196
|
message: "Destination folder",
|
|
1067
1197
|
initialValue: defaultDir,
|
|
1068
1198
|
validate: (v) => !v?.trim() ? "Folder is required" : void 0
|
|
@@ -1070,13 +1200,13 @@ async function askQuestions(url, meta) {
|
|
|
1070
1200
|
exitOnCancel(outDir);
|
|
1071
1201
|
const outputDir = String(outDir).trim();
|
|
1072
1202
|
await savePreferences({ ...prefs, outputDir });
|
|
1073
|
-
const customize = selectedMode === "subs-only" || selectedMode === "thumbnail-only" ? false : await
|
|
1203
|
+
const customize = selectedMode === "subs-only" || selectedMode === "thumbnail-only" ? false : await p3.confirm({
|
|
1074
1204
|
message: "Customize advanced options?",
|
|
1075
1205
|
initialValue: false
|
|
1076
1206
|
});
|
|
1077
1207
|
exitOnCancel(customize);
|
|
1078
1208
|
if (customize && (selectedMode === "video" || selectedMode === "video-only")) {
|
|
1079
|
-
const c = await
|
|
1209
|
+
const c = await p3.select({
|
|
1080
1210
|
message: "Container preference?",
|
|
1081
1211
|
options: [
|
|
1082
1212
|
{ value: "best", label: "Best available" },
|
|
@@ -1089,7 +1219,7 @@ async function askQuestions(url, meta) {
|
|
|
1089
1219
|
container = c;
|
|
1090
1220
|
}
|
|
1091
1221
|
if (customize && selectedMode !== "thumbnail-only") {
|
|
1092
|
-
const wantSubs = await
|
|
1222
|
+
const wantSubs = await p3.confirm({
|
|
1093
1223
|
message: "Download subtitles?",
|
|
1094
1224
|
initialValue: false
|
|
1095
1225
|
});
|
|
@@ -1097,10 +1227,10 @@ async function askQuestions(url, meta) {
|
|
|
1097
1227
|
if (wantSubs) {
|
|
1098
1228
|
const langs = availableSubtitleLangs(meta);
|
|
1099
1229
|
if (langs.length === 0) {
|
|
1100
|
-
|
|
1230
|
+
p3.log.warn('No subtitle languages found \u2014 will request "all".');
|
|
1101
1231
|
subLangs = ["all"];
|
|
1102
1232
|
} else {
|
|
1103
|
-
const picked = await
|
|
1233
|
+
const picked = await p3.multiselect({
|
|
1104
1234
|
message: "Which subtitle languages?",
|
|
1105
1235
|
options: [
|
|
1106
1236
|
{ value: "all", label: "All languages" },
|
|
@@ -1112,7 +1242,7 @@ async function askQuestions(url, meta) {
|
|
|
1112
1242
|
const sel = picked;
|
|
1113
1243
|
subLangs = sel.includes("all") ? ["all"] : sel;
|
|
1114
1244
|
}
|
|
1115
|
-
const how = await
|
|
1245
|
+
const how = await p3.select({
|
|
1116
1246
|
message: "How should subtitles be saved?",
|
|
1117
1247
|
options: [
|
|
1118
1248
|
{ value: "embed", label: "Embed in the video" },
|
|
@@ -1124,7 +1254,7 @@ async function askQuestions(url, meta) {
|
|
|
1124
1254
|
subMode = how;
|
|
1125
1255
|
}
|
|
1126
1256
|
if (isPlaylistUrl(url, meta)) {
|
|
1127
|
-
const pl = await
|
|
1257
|
+
const pl = await p3.select({
|
|
1128
1258
|
message: "This URL is part of a playlist. What should we download?",
|
|
1129
1259
|
options: [
|
|
1130
1260
|
{ value: "single", label: "Just this video" },
|
|
@@ -1138,7 +1268,7 @@ async function askQuestions(url, meta) {
|
|
|
1138
1268
|
} else if (pl === "all") {
|
|
1139
1269
|
playlist = { kind: "all" };
|
|
1140
1270
|
} else {
|
|
1141
|
-
const start = await
|
|
1271
|
+
const start = await p3.text({
|
|
1142
1272
|
message: "Playlist start index (1-based)",
|
|
1143
1273
|
initialValue: "1",
|
|
1144
1274
|
validate: (v) => {
|
|
@@ -1147,7 +1277,7 @@ async function askQuestions(url, meta) {
|
|
|
1147
1277
|
}
|
|
1148
1278
|
});
|
|
1149
1279
|
exitOnCancel(start);
|
|
1150
|
-
const stop = await
|
|
1280
|
+
const stop = await p3.text({
|
|
1151
1281
|
message: "Playlist stop index (inclusive)",
|
|
1152
1282
|
initialValue: String(meta.playlist_count ?? 10),
|
|
1153
1283
|
validate: (v) => {
|
|
@@ -1163,7 +1293,7 @@ async function askQuestions(url, meta) {
|
|
|
1163
1293
|
};
|
|
1164
1294
|
}
|
|
1165
1295
|
}
|
|
1166
|
-
const pickedFilename = await
|
|
1296
|
+
const pickedFilename = await p3.select({
|
|
1167
1297
|
message: "Filename style?",
|
|
1168
1298
|
options: [
|
|
1169
1299
|
{ value: "title", label: "Title only" },
|
|
@@ -1179,7 +1309,7 @@ async function askQuestions(url, meta) {
|
|
|
1179
1309
|
});
|
|
1180
1310
|
exitOnCancel(pickedFilename);
|
|
1181
1311
|
filenamePreset = pickedFilename;
|
|
1182
|
-
const e = await
|
|
1312
|
+
const e = await p3.multiselect({
|
|
1183
1313
|
message: "Extras (optional)",
|
|
1184
1314
|
options: [
|
|
1185
1315
|
{
|
|
@@ -1221,9 +1351,19 @@ async function askQuestions(url, meta) {
|
|
|
1221
1351
|
}
|
|
1222
1352
|
|
|
1223
1353
|
// src/cli.ts
|
|
1224
|
-
|
|
1354
|
+
function readCliVersion() {
|
|
1355
|
+
try {
|
|
1356
|
+
const pkg = JSON.parse(
|
|
1357
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
1358
|
+
);
|
|
1359
|
+
return pkg.version ?? "0.0.0";
|
|
1360
|
+
} catch {
|
|
1361
|
+
return "0.0.0";
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
var CLI_VERSION = readCliVersion();
|
|
1225
1365
|
function fail(message) {
|
|
1226
|
-
|
|
1366
|
+
p4.log.error(message);
|
|
1227
1367
|
process.exit(1);
|
|
1228
1368
|
}
|
|
1229
1369
|
function pick(value, allowed, name, fallback) {
|
|
@@ -1326,13 +1466,13 @@ function displayFlagsForAnswers(answers) {
|
|
|
1326
1466
|
}
|
|
1327
1467
|
function showCommandForAnswers(answers) {
|
|
1328
1468
|
if (answers.urls.length > 1) {
|
|
1329
|
-
|
|
1469
|
+
p4.log.info("Showing the command for the first URL. It will be repeated for each URL.");
|
|
1330
1470
|
}
|
|
1331
1471
|
showCommand(formatCommand(displayFlagsForAnswers(answers)));
|
|
1332
1472
|
}
|
|
1333
1473
|
async function confirmStart(answers) {
|
|
1334
1474
|
while (true) {
|
|
1335
|
-
const action = await
|
|
1475
|
+
const action = await p4.select({
|
|
1336
1476
|
message: "Ready?",
|
|
1337
1477
|
options: [
|
|
1338
1478
|
{ value: "start", label: "Start download" },
|
|
@@ -1341,8 +1481,8 @@ async function confirmStart(answers) {
|
|
|
1341
1481
|
{ value: "cancel", label: "Cancel" }
|
|
1342
1482
|
]
|
|
1343
1483
|
});
|
|
1344
|
-
if (
|
|
1345
|
-
|
|
1484
|
+
if (p4.isCancel(action) || action === "cancel") {
|
|
1485
|
+
p4.cancel("Cancelled.");
|
|
1346
1486
|
process.exit(0);
|
|
1347
1487
|
}
|
|
1348
1488
|
if (action === "show-command") {
|
|
@@ -1363,41 +1503,143 @@ async function ensureFfmpegIfNeeded(answers) {
|
|
|
1363
1503
|
}
|
|
1364
1504
|
const status = await checkFfmpeg();
|
|
1365
1505
|
if (status.ok) return;
|
|
1366
|
-
|
|
1506
|
+
p4.log.warn(
|
|
1367
1507
|
[
|
|
1368
1508
|
"ffmpeg/ffprobe not found on PATH.",
|
|
1369
1509
|
ffmpegInstallHint()
|
|
1370
1510
|
].join("\n\n")
|
|
1371
1511
|
);
|
|
1372
|
-
const cont = await
|
|
1512
|
+
const cont = await p4.confirm({
|
|
1373
1513
|
message: "Continue anyway? (download may fail at merge/extract)",
|
|
1374
1514
|
initialValue: false
|
|
1375
1515
|
});
|
|
1376
|
-
if (
|
|
1377
|
-
|
|
1516
|
+
if (p4.isCancel(cont) || !cont) {
|
|
1517
|
+
p4.cancel("Cancelled.");
|
|
1378
1518
|
process.exit(0);
|
|
1379
1519
|
}
|
|
1380
1520
|
}
|
|
1381
1521
|
async function runDownloads(ytDlp, answers) {
|
|
1382
|
-
const
|
|
1522
|
+
const filepaths = [];
|
|
1523
|
+
const failures = [];
|
|
1383
1524
|
for (let i = 0; i < answers.urls.length; i++) {
|
|
1384
1525
|
const url = answers.urls[i];
|
|
1385
1526
|
const label = answers.urls.length > 1 ? `${i + 1} of ${answers.urls.length}` : void 0;
|
|
1386
1527
|
const flags = buildFlags({ ...answers, urls: [url] });
|
|
1387
|
-
if (label)
|
|
1528
|
+
if (label) p4.log.info(`Download ${label}`);
|
|
1388
1529
|
try {
|
|
1389
1530
|
const result = await runDownload(ytDlp, flags, { label });
|
|
1390
|
-
|
|
1531
|
+
filepaths.push(...result.filepaths);
|
|
1391
1532
|
} catch (err) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1533
|
+
failures.push({
|
|
1534
|
+
url,
|
|
1535
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return { filepaths, failures };
|
|
1540
|
+
}
|
|
1541
|
+
async function gatherMetadata(ytDlp, urls, spinner2) {
|
|
1542
|
+
for (let attempt = 0; ; attempt++) {
|
|
1543
|
+
spinner2.start(
|
|
1544
|
+
attempt === 0 ? "Fetching video info\u2026" : "Fetching video info with Deno\u2026"
|
|
1545
|
+
);
|
|
1546
|
+
const metaResults = await Promise.allSettled(
|
|
1547
|
+
urls.map((u) => fetchMetadata(ytDlp, u))
|
|
1548
|
+
);
|
|
1549
|
+
const videoInfos = [];
|
|
1550
|
+
const failedUrls = [];
|
|
1551
|
+
for (let i = 0; i < metaResults.length; i++) {
|
|
1552
|
+
const r = metaResults[i];
|
|
1553
|
+
const u = urls[i];
|
|
1554
|
+
if (r.status === "fulfilled") {
|
|
1555
|
+
const m = r.value;
|
|
1556
|
+
const isPlaylist = m._type === "playlist";
|
|
1557
|
+
videoInfos.push({
|
|
1558
|
+
url: u,
|
|
1559
|
+
meta: m,
|
|
1560
|
+
title: m.title ?? "Unknown title",
|
|
1561
|
+
uploader: m.uploader ?? "Unknown uploader",
|
|
1562
|
+
duration: isPlaylist ? `${m.playlist_count ?? "?"} videos` : formatDuration(m.duration)
|
|
1563
|
+
});
|
|
1564
|
+
} else {
|
|
1565
|
+
failedUrls.push(u);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (videoInfos.length > 0) {
|
|
1569
|
+
spinner2.stop("Metadata loaded");
|
|
1570
|
+
return { videoInfos, failedUrls };
|
|
1571
|
+
}
|
|
1572
|
+
spinner2.stop("Could not fetch metadata");
|
|
1573
|
+
const reasons = [
|
|
1574
|
+
...new Set(
|
|
1575
|
+
metaResults.filter((r) => r.status === "rejected").map(
|
|
1576
|
+
(r) => r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
1577
|
+
)
|
|
1578
|
+
)
|
|
1579
|
+
];
|
|
1580
|
+
p4.log.error(
|
|
1581
|
+
[
|
|
1582
|
+
"Failed to fetch metadata for any of the provided URLs.",
|
|
1583
|
+
...reasons
|
|
1584
|
+
].join("\n\n")
|
|
1585
|
+
);
|
|
1586
|
+
if (attempt === 0 && reasons.some(isJsRuntimeError) && await offerDenoInstall()) {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
process.exit(1);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function reportSaved(filepaths, answers) {
|
|
1593
|
+
if (filepaths.length > 0) {
|
|
1594
|
+
showSaved(filepaths);
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
p4.log.success("Done. (No filepath printed \u2014 check your output folder.)");
|
|
1598
|
+
p4.log.info(`Output folder: ${answers.outputDir}`);
|
|
1599
|
+
}
|
|
1600
|
+
async function downloadAndReport(ytDlp, answers, options = {}) {
|
|
1601
|
+
const allPaths = [];
|
|
1602
|
+
let pending = answers.urls;
|
|
1603
|
+
for (let attempt = 0; ; attempt++) {
|
|
1604
|
+
p4.log.info(
|
|
1605
|
+
attempt === 0 ? "Starting download\u2026" : "Retrying the failed downloads with Deno\u2026"
|
|
1606
|
+
);
|
|
1607
|
+
const { filepaths, failures } = await runDownloads(ytDlp, {
|
|
1608
|
+
...answers,
|
|
1609
|
+
urls: pending
|
|
1610
|
+
});
|
|
1611
|
+
allPaths.push(...filepaths);
|
|
1612
|
+
if (failures.length === 0) {
|
|
1613
|
+
reportSaved(allPaths, answers);
|
|
1614
|
+
p4.outro("Finished");
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
for (const failure of failures) {
|
|
1618
|
+
p4.log.error(
|
|
1619
|
+
answers.urls.length > 1 ? `${failure.url}
|
|
1620
|
+
${failure.message}` : failure.message
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const jsRuntimeFailures = failures.filter(
|
|
1624
|
+
(f) => isJsRuntimeError(f.message)
|
|
1625
|
+
);
|
|
1626
|
+
if (attempt === 0 && jsRuntimeFailures.length > 0 && await offerDenoInstall({ canPrompt: options.canPrompt })) {
|
|
1627
|
+
pending = jsRuntimeFailures.map((f) => f.url);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
if (allPaths.length > 0) {
|
|
1631
|
+
reportSaved(allPaths, answers);
|
|
1632
|
+
p4.log.warn(`${failures.length} download(s) failed.`);
|
|
1633
|
+
p4.outro("Finished with errors");
|
|
1634
|
+
return;
|
|
1394
1635
|
}
|
|
1636
|
+
p4.outro("Failed");
|
|
1637
|
+
process.exit(1);
|
|
1395
1638
|
}
|
|
1396
|
-
return allPaths;
|
|
1397
1639
|
}
|
|
1398
1640
|
async function runWizard(urlsArg, opts = {}) {
|
|
1399
|
-
|
|
1400
|
-
const spinner2 =
|
|
1641
|
+
p4.intro("easy-ytdlp");
|
|
1642
|
+
const spinner2 = p4.spinner();
|
|
1401
1643
|
spinner2.start("Preparing yt-dlp binary\u2026");
|
|
1402
1644
|
let ytDlp;
|
|
1403
1645
|
try {
|
|
@@ -1409,7 +1651,7 @@ async function runWizard(urlsArg, opts = {}) {
|
|
|
1409
1651
|
spinner2.stop("yt-dlp ready");
|
|
1410
1652
|
} catch (err) {
|
|
1411
1653
|
spinner2.stop("Binary setup failed");
|
|
1412
|
-
|
|
1654
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1413
1655
|
process.exit(1);
|
|
1414
1656
|
}
|
|
1415
1657
|
const urls = await promptUrls(urlsArg);
|
|
@@ -1420,52 +1662,10 @@ async function runWizard(urlsArg, opts = {}) {
|
|
|
1420
1662
|
showCommandForAnswers(answers2);
|
|
1421
1663
|
}
|
|
1422
1664
|
await ensureFfmpegIfNeeded(answers2);
|
|
1423
|
-
|
|
1424
|
-
try {
|
|
1425
|
-
const filepaths = await runDownloads(ytDlp, answers2);
|
|
1426
|
-
if (filepaths.length > 0) {
|
|
1427
|
-
showSaved(filepaths);
|
|
1428
|
-
} else {
|
|
1429
|
-
p3.log.success("Done. (No filepath printed \u2014 check your output folder.)");
|
|
1430
|
-
p3.log.info(`Output folder: ${answers2.outputDir}`);
|
|
1431
|
-
}
|
|
1432
|
-
p3.outro("Finished");
|
|
1433
|
-
} catch (err) {
|
|
1434
|
-
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1435
|
-
p3.outro("Failed");
|
|
1436
|
-
process.exit(1);
|
|
1437
|
-
}
|
|
1665
|
+
await downloadAndReport(ytDlp, answers2, { canPrompt: false });
|
|
1438
1666
|
return;
|
|
1439
1667
|
}
|
|
1440
|
-
|
|
1441
|
-
const metaResults = await Promise.allSettled(
|
|
1442
|
-
urls.map((u) => fetchMetadata(ytDlp, u))
|
|
1443
|
-
);
|
|
1444
|
-
const videoInfos = [];
|
|
1445
|
-
const failedUrls = [];
|
|
1446
|
-
for (let i = 0; i < metaResults.length; i++) {
|
|
1447
|
-
const r = metaResults[i];
|
|
1448
|
-
const u = urls[i];
|
|
1449
|
-
if (r.status === "fulfilled") {
|
|
1450
|
-
const m = r.value;
|
|
1451
|
-
const isPlaylist = m._type === "playlist";
|
|
1452
|
-
videoInfos.push({
|
|
1453
|
-
url: u,
|
|
1454
|
-
meta: m,
|
|
1455
|
-
title: m.title ?? "Unknown title",
|
|
1456
|
-
uploader: m.uploader ?? "Unknown uploader",
|
|
1457
|
-
duration: isPlaylist ? `${m.playlist_count ?? "?"} videos` : formatDuration(m.duration)
|
|
1458
|
-
});
|
|
1459
|
-
} else {
|
|
1460
|
-
failedUrls.push(u);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
if (videoInfos.length === 0) {
|
|
1464
|
-
spinner2.stop("Could not fetch metadata");
|
|
1465
|
-
p3.log.error("Failed to fetch metadata for any of the provided URLs.");
|
|
1466
|
-
process.exit(1);
|
|
1467
|
-
}
|
|
1468
|
-
spinner2.stop("Metadata loaded");
|
|
1668
|
+
const { videoInfos, failedUrls } = await gatherMetadata(ytDlp, urls, spinner2);
|
|
1469
1669
|
if (videoInfos.length === 1) {
|
|
1470
1670
|
const v = videoInfos[0];
|
|
1471
1671
|
showNote(`${v.title}
|
|
@@ -1478,13 +1678,13 @@ by ${v.uploader} \xB7 ${v.duration}`, "Found");
|
|
|
1478
1678
|
showNote(lines.join("\n\n"), `Found (${videoInfos.length} videos)`);
|
|
1479
1679
|
}
|
|
1480
1680
|
if (failedUrls.length > 0) {
|
|
1481
|
-
|
|
1681
|
+
p4.log.warn(
|
|
1482
1682
|
`Could not fetch metadata for ${failedUrls.length} URL(s). They will still be downloaded with shared settings.`
|
|
1483
1683
|
);
|
|
1484
1684
|
}
|
|
1485
1685
|
const primary = videoInfos[0];
|
|
1486
1686
|
if (urls.length > 1) {
|
|
1487
|
-
|
|
1687
|
+
p4.log.info(`Using shared settings based on: ${primary.title}`);
|
|
1488
1688
|
}
|
|
1489
1689
|
let answers;
|
|
1490
1690
|
while (true) {
|
|
@@ -1500,33 +1700,19 @@ by ${v.uploader} \xB7 ${v.duration}`, "Found");
|
|
|
1500
1700
|
if (action === "start") break;
|
|
1501
1701
|
}
|
|
1502
1702
|
await ensureFfmpegIfNeeded(answers);
|
|
1503
|
-
|
|
1504
|
-
try {
|
|
1505
|
-
const filepaths = await runDownloads(ytDlp, answers);
|
|
1506
|
-
if (filepaths.length > 0) {
|
|
1507
|
-
showSaved(filepaths);
|
|
1508
|
-
} else {
|
|
1509
|
-
p3.log.success("Done. (No filepath printed \u2014 check your output folder.)");
|
|
1510
|
-
p3.log.info(`Output folder: ${answers.outputDir}`);
|
|
1511
|
-
}
|
|
1512
|
-
p3.outro("Finished");
|
|
1513
|
-
} catch (err) {
|
|
1514
|
-
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1515
|
-
p3.outro("Failed");
|
|
1516
|
-
process.exit(1);
|
|
1517
|
-
}
|
|
1703
|
+
await downloadAndReport(ytDlp, answers);
|
|
1518
1704
|
}
|
|
1519
1705
|
async function runUpdateBinary() {
|
|
1520
|
-
|
|
1521
|
-
const spinner2 =
|
|
1706
|
+
p4.intro("easy-ytdlp update-binary");
|
|
1707
|
+
const spinner2 = p4.spinner();
|
|
1522
1708
|
spinner2.start("Refreshing yt-dlp binary\u2026");
|
|
1523
1709
|
try {
|
|
1524
1710
|
const path = await updateBinary((msg) => spinner2.message(msg));
|
|
1525
1711
|
spinner2.stop(`Updated: ${path}`);
|
|
1526
|
-
|
|
1712
|
+
p4.outro("Binary update complete");
|
|
1527
1713
|
} catch (err) {
|
|
1528
1714
|
spinner2.stop("Update failed");
|
|
1529
|
-
|
|
1715
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1530
1716
|
process.exit(1);
|
|
1531
1717
|
}
|
|
1532
1718
|
}
|
|
@@ -1550,7 +1736,7 @@ program.name("easy-ytdlp").description(
|
|
|
1550
1736
|
content = readFileSync(options.batchFile, "utf-8");
|
|
1551
1737
|
} catch (err) {
|
|
1552
1738
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1553
|
-
|
|
1739
|
+
p4.log.error(`Could not read batch file "${options.batchFile}": ${detail}`);
|
|
1554
1740
|
process.exit(1);
|
|
1555
1741
|
}
|
|
1556
1742
|
const fileUrls = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jnzlab/easy-ytdlp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A user-friendly interactive CLI wrapper for yt-dlp — no Python or flag memorization required",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@clack/prompts": "^0.10.0",
|
|
50
|
-
"@jnzlab/easy-ytdlp": "^1.1.0",
|
|
51
50
|
"cli-progress": "^3.12.0",
|
|
52
51
|
"commander": "^13.1.0",
|
|
53
52
|
"env-paths": "^3.0.0",
|