@jnzlab/easy-ytdlp 1.1.2 → 1.1.4

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 (3) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cli.js +209 -134
  3. package/package.json +1 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.4 - 2026-09-08
4
+
5
+ ### Fixed
6
+
7
+ - Interactive mode no longer fails with "Failed to fetch metadata for any of
8
+ the provided URLs" on videos that have no pre-merged format. `yt-dlp-wrap`
9
+ silently added `-f best` to the metadata call, which YouTube now rejects for
10
+ many videos; the metadata fetch passes `--ignore-no-formats-error` and its
11
+ own format flag instead. When every URL fails, the underlying yt-dlp error is
12
+ now shown instead of a generic message.
13
+ - `--version` now reads the version from `package.json` instead of a
14
+ hardcoded constant that had drifted from the published version.
15
+ - Removed an accidental dependency of the package on itself.
16
+ - Pasting a bare YouTube playlist link no longer fails/hangs at startup. The
17
+ metadata fetch now uses `--flat-playlist` for playlist URLs (fast, one small
18
+ line per video) and shows the playlist title + video count instead of dumping
19
+ full metadata for every video. "Just this video" on a bare playlist URL now
20
+ downloads only the first entry (`--playlist-items 1`) instead of the whole
21
+ playlist.
22
+
3
23
  ## 1.1.0 - 2026-07-27
4
24
 
5
25
  ### Added
package/dist/cli.js CHANGED
@@ -179,129 +179,6 @@ async function createYtDlp(options = {}) {
179
179
  return new YTDlpWrap(binPath);
180
180
  }
181
181
 
182
- // src/builder.ts
183
- var FILENAME_TEMPLATES = {
184
- title: "%(title)s.%(ext)s",
185
- "title-channel": "%(title)s [%(uploader)s].%(ext)s",
186
- "title-date": "%(upload_date)s - %(title)s.%(ext)s"
187
- };
188
- function audioQualityValue(q) {
189
- return q === "good" ? "5" : "0";
190
- }
191
- function videoQualityFlags(quality) {
192
- if (!quality || quality === "best") {
193
- return ["-f", "bv*+ba/b"];
194
- }
195
- if (typeof quality === "object" && "height" in quality) {
196
- const h = quality.height;
197
- return ["-f", `bv*[height=${h}]+ba/b`];
198
- }
199
- return ["-S", `res:${quality}`, "-f", "bv*+ba/b"];
200
- }
201
- function containerFlags(container) {
202
- if (!container || container === "best") return [];
203
- return ["--merge-output-format", container];
204
- }
205
- function buildFlags(answers) {
206
- const flags = [];
207
- switch (answers.mode) {
208
- case "video":
209
- flags.push(...videoQualityFlags(answers.videoQuality));
210
- flags.push(...containerFlags(answers.container));
211
- break;
212
- case "video-only":
213
- if (answers.videoQuality && answers.videoQuality !== "best" && typeof answers.videoQuality !== "object") {
214
- flags.push("-S", `res:${answers.videoQuality}`, "-f", "bv");
215
- } else if (typeof answers.videoQuality === "object" && answers.videoQuality?.height) {
216
- flags.push("-f", `bv[height=${answers.videoQuality.height}]`);
217
- } else {
218
- flags.push("-f", "bv");
219
- }
220
- flags.push(...containerFlags(answers.container));
221
- break;
222
- case "audio":
223
- flags.push(
224
- "-x",
225
- "--audio-format",
226
- answers.audioFormat ?? "best",
227
- "--audio-quality",
228
- audioQualityValue(answers.audioQuality)
229
- );
230
- break;
231
- case "subs-only":
232
- flags.push("--skip-download", "--write-subs");
233
- break;
234
- case "thumbnail-only":
235
- flags.push("--skip-download", "--write-thumbnail");
236
- break;
237
- }
238
- if (answers.mode === "subs-only") {
239
- const langs = answers.subtitles.languages.length > 0 ? answers.subtitles.languages.join(",") : "all";
240
- flags.push("--sub-langs", langs);
241
- } else if (answers.subtitles.mode !== "none" && answers.mode !== "thumbnail-only") {
242
- const langs = answers.subtitles.languages.length > 0 ? answers.subtitles.languages.join(",") : "all";
243
- flags.push("--sub-langs", langs);
244
- if (answers.subtitles.mode === "write" || answers.subtitles.mode === "both") {
245
- flags.push("--write-subs");
246
- }
247
- if (answers.subtitles.mode === "embed" || answers.subtitles.mode === "both") {
248
- flags.push("--embed-subs");
249
- }
250
- }
251
- switch (answers.playlist.kind) {
252
- case "single":
253
- flags.push("--no-playlist");
254
- break;
255
- case "all":
256
- flags.push("--yes-playlist");
257
- break;
258
- case "range":
259
- flags.push(
260
- "--yes-playlist",
261
- "-I",
262
- `${answers.playlist.start}:${answers.playlist.stop}`
263
- );
264
- break;
265
- }
266
- flags.push("-P", answers.outputDir);
267
- flags.push("-o", FILENAME_TEMPLATES[answers.filenamePreset]);
268
- if (answers.embedThumbnail && answers.mode !== "thumbnail-only") {
269
- flags.push("--embed-thumbnail");
270
- }
271
- if (answers.embedMetadata) {
272
- flags.push("--embed-metadata");
273
- }
274
- if (answers.sponsorBlock) {
275
- flags.push("--sponsorblock-remove", "default");
276
- }
277
- flags.push("--print", "after_move:filepath");
278
- flags.push(...answers.urls);
279
- if (answers.urls.length > 1) {
280
- flags.unshift("--ignore-errors");
281
- }
282
- return flags;
283
- }
284
- function formatCommand(flags) {
285
- const quoted = flags.map(
286
- (f) => /[\s"'\\]/.test(f) ? JSON.stringify(f) : f
287
- );
288
- const parts = ["yt-dlp"];
289
- for (let i = 0; i < quoted.length; i++) {
290
- const cur = quoted[i];
291
- const next = quoted[i + 1];
292
- if (cur.startsWith("-") && next && !next.startsWith("-") && !looksLikeUrl(next)) {
293
- parts.push(`${cur} ${next}`);
294
- i++;
295
- } else {
296
- parts.push(cur);
297
- }
298
- }
299
- return parts.join("\n ");
300
- }
301
- function looksLikeUrl(s) {
302
- return /^https?:\/\//i.test(s) || s.startsWith('"http');
303
- }
304
-
305
182
  // src/downloader.ts
306
183
  import cliProgress from "cli-progress";
307
184
 
@@ -492,13 +369,63 @@ yt-dlp said: ${raw.trim()}`;
492
369
  }
493
370
  return raw.trim() || "Download failed for an unknown reason.";
494
371
  }
372
+ var YOUTUBE_HOST_RE = /(\.|^)youtube\.com$/i;
373
+ var YOUTUBE_SHORT_HOST_RE = /(\.|^)youtu\.be$/i;
374
+ var VIDEO_ID_RE = /^[A-Za-z0-9_-]{11}$/;
375
+ function isPlaylistOnlyUrl(url) {
376
+ let u;
377
+ try {
378
+ u = new URL(url);
379
+ } catch {
380
+ return false;
381
+ }
382
+ const host = u.hostname.toLowerCase();
383
+ const isYoutube = YOUTUBE_HOST_RE.test(host) || YOUTUBE_SHORT_HOST_RE.test(host);
384
+ if (!isYoutube || !u.searchParams.has("list")) return false;
385
+ const v = u.searchParams.get("v") ?? "";
386
+ if (VIDEO_ID_RE.test(v)) return false;
387
+ if (host.endsWith("youtu.be") && u.pathname.length > 1) return false;
388
+ if (/^\/(?:shorts|embed|live)\/[A-Za-z0-9_-]{11}/.test(u.pathname)) {
389
+ return false;
390
+ }
391
+ return true;
392
+ }
393
+ function metadataArgs(url) {
394
+ const scopeFlags = isPlaylistOnlyUrl(url) ? ["--flat-playlist"] : ["--no-playlist"];
395
+ return [
396
+ ...youtubeCompatFlags(),
397
+ ...scopeFlags,
398
+ "--ignore-no-formats-error",
399
+ "-f",
400
+ "b",
401
+ url
402
+ ];
403
+ }
404
+ function normalizePlaylistMeta(raw, url) {
405
+ const entries = Array.isArray(raw) ? raw : raw && typeof raw === "object" ? [raw] : [];
406
+ const first = entries[0];
407
+ if (!first) {
408
+ return { _type: "playlist", webpage_url: url, playlist_count: 0, entries: [] };
409
+ }
410
+ const count = first.playlist_count ?? first.n_entries ?? entries.length;
411
+ return {
412
+ _type: "playlist",
413
+ id: first.playlist_id ?? first.id,
414
+ title: first.playlist_title ?? first.playlist ?? first.title,
415
+ uploader: first.playlist_uploader,
416
+ playlist: first.playlist_title ?? first.playlist,
417
+ playlist_id: first.playlist_id,
418
+ playlist_count: count,
419
+ n_entries: count,
420
+ webpage_url: url,
421
+ entries
422
+ };
423
+ }
495
424
  async function fetchMetadata(ytDlp, url) {
425
+ const playlistOnly = isPlaylistOnlyUrl(url);
496
426
  try {
497
- return await ytDlp.getVideoInfo([
498
- ...youtubeCompatFlags(),
499
- "--no-playlist",
500
- url
501
- ]);
427
+ const raw = await ytDlp.getVideoInfo(metadataArgs(url));
428
+ return playlistOnly ? normalizePlaylistMeta(raw, url) : raw;
502
429
  } catch (err) {
503
430
  const msg = err instanceof Error ? err.message : String(err);
504
431
  throw new Error(humanizeError(msg));
@@ -694,9 +621,7 @@ async function runDownload(ytDlp, flags, options = {}) {
694
621
  }
695
622
  if (code !== 0 && code !== null) {
696
623
  reject(
697
- new Error(
698
- humanizeError(lastError || `yt-dlp exited with code ${code}`)
699
- )
624
+ new Error(humanizeError(lastError || `yt-dlp exited with code ${code}`))
700
625
  );
701
626
  return;
702
627
  }
@@ -705,6 +630,135 @@ async function runDownload(ytDlp, flags, options = {}) {
705
630
  });
706
631
  }
707
632
 
633
+ // src/builder.ts
634
+ var FILENAME_TEMPLATES = {
635
+ title: "%(title)s.%(ext)s",
636
+ "title-channel": "%(title)s [%(uploader)s].%(ext)s",
637
+ "title-date": "%(upload_date)s - %(title)s.%(ext)s"
638
+ };
639
+ function audioQualityValue(q) {
640
+ return q === "good" ? "5" : "0";
641
+ }
642
+ function videoQualityFlags(quality) {
643
+ if (!quality || quality === "best") {
644
+ return ["-f", "bv*+ba/b"];
645
+ }
646
+ if (typeof quality === "object" && "height" in quality) {
647
+ const h = quality.height;
648
+ return ["-f", `bv*[height=${h}]+ba/b`];
649
+ }
650
+ return ["-S", `res:${quality}`, "-f", "bv*+ba/b"];
651
+ }
652
+ function containerFlags(container) {
653
+ if (!container || container === "best") return [];
654
+ return ["--merge-output-format", container];
655
+ }
656
+ function buildFlags(answers) {
657
+ const flags = [];
658
+ switch (answers.mode) {
659
+ case "video":
660
+ flags.push(...videoQualityFlags(answers.videoQuality));
661
+ flags.push(...containerFlags(answers.container));
662
+ break;
663
+ case "video-only":
664
+ if (answers.videoQuality && answers.videoQuality !== "best" && typeof answers.videoQuality !== "object") {
665
+ flags.push("-S", `res:${answers.videoQuality}`, "-f", "bv");
666
+ } else if (typeof answers.videoQuality === "object" && answers.videoQuality?.height) {
667
+ flags.push("-f", `bv[height=${answers.videoQuality.height}]`);
668
+ } else {
669
+ flags.push("-f", "bv");
670
+ }
671
+ flags.push(...containerFlags(answers.container));
672
+ break;
673
+ case "audio":
674
+ flags.push(
675
+ "-x",
676
+ "--audio-format",
677
+ answers.audioFormat ?? "best",
678
+ "--audio-quality",
679
+ audioQualityValue(answers.audioQuality)
680
+ );
681
+ break;
682
+ case "subs-only":
683
+ flags.push("--skip-download", "--write-subs");
684
+ break;
685
+ case "thumbnail-only":
686
+ flags.push("--skip-download", "--write-thumbnail");
687
+ break;
688
+ }
689
+ if (answers.mode === "subs-only") {
690
+ const langs = answers.subtitles.languages.length > 0 ? answers.subtitles.languages.join(",") : "all";
691
+ flags.push("--sub-langs", langs);
692
+ } else if (answers.subtitles.mode !== "none" && answers.mode !== "thumbnail-only") {
693
+ const langs = answers.subtitles.languages.length > 0 ? answers.subtitles.languages.join(",") : "all";
694
+ flags.push("--sub-langs", langs);
695
+ if (answers.subtitles.mode === "write" || answers.subtitles.mode === "both") {
696
+ flags.push("--write-subs");
697
+ }
698
+ if (answers.subtitles.mode === "embed" || answers.subtitles.mode === "both") {
699
+ flags.push("--embed-subs");
700
+ }
701
+ }
702
+ switch (answers.playlist.kind) {
703
+ case "single": {
704
+ const firstUrl = answers.urls[0];
705
+ if (firstUrl && isPlaylistOnlyUrl(firstUrl)) {
706
+ flags.push("--playlist-items", "1");
707
+ } else {
708
+ flags.push("--no-playlist");
709
+ }
710
+ break;
711
+ }
712
+ case "all":
713
+ flags.push("--yes-playlist");
714
+ break;
715
+ case "range":
716
+ flags.push(
717
+ "--yes-playlist",
718
+ "-I",
719
+ `${answers.playlist.start}:${answers.playlist.stop}`
720
+ );
721
+ break;
722
+ }
723
+ flags.push("-P", answers.outputDir);
724
+ flags.push("-o", FILENAME_TEMPLATES[answers.filenamePreset]);
725
+ if (answers.embedThumbnail && answers.mode !== "thumbnail-only") {
726
+ flags.push("--embed-thumbnail");
727
+ }
728
+ if (answers.embedMetadata) {
729
+ flags.push("--embed-metadata");
730
+ }
731
+ if (answers.sponsorBlock) {
732
+ flags.push("--sponsorblock-remove", "default");
733
+ }
734
+ flags.push("--print", "after_move:filepath");
735
+ flags.push(...answers.urls);
736
+ if (answers.urls.length > 1) {
737
+ flags.unshift("--ignore-errors");
738
+ }
739
+ return flags;
740
+ }
741
+ function formatCommand(flags) {
742
+ const quoted = flags.map(
743
+ (f) => /[\s"'\\]/.test(f) ? JSON.stringify(f) : f
744
+ );
745
+ const parts = ["yt-dlp"];
746
+ for (let i = 0; i < quoted.length; i++) {
747
+ const cur = quoted[i];
748
+ const next = quoted[i + 1];
749
+ if (cur.startsWith("-") && next && !next.startsWith("-") && !looksLikeUrl(next)) {
750
+ parts.push(`${cur} ${next}`);
751
+ i++;
752
+ } else {
753
+ parts.push(cur);
754
+ }
755
+ }
756
+ return parts.join("\n ");
757
+ }
758
+ function looksLikeUrl(s) {
759
+ return /^https?:\/\//i.test(s) || s.startsWith('"http');
760
+ }
761
+
708
762
  // src/questions.ts
709
763
  import * as p2 from "@clack/prompts";
710
764
  import { homedir } from "os";
@@ -1174,7 +1228,17 @@ async function askQuestions(url, meta) {
1174
1228
  }
1175
1229
 
1176
1230
  // src/cli.ts
1177
- var CLI_VERSION = "1.1.0";
1231
+ function readCliVersion() {
1232
+ try {
1233
+ const pkg = JSON.parse(
1234
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
1235
+ );
1236
+ return pkg.version ?? "0.0.0";
1237
+ } catch {
1238
+ return "0.0.0";
1239
+ }
1240
+ }
1241
+ var CLI_VERSION = readCliVersion();
1178
1242
  function fail(message) {
1179
1243
  p3.log.error(message);
1180
1244
  process.exit(1);
@@ -1401,12 +1465,13 @@ async function runWizard(urlsArg, opts = {}) {
1401
1465
  const u = urls[i];
1402
1466
  if (r.status === "fulfilled") {
1403
1467
  const m = r.value;
1468
+ const isPlaylist = m._type === "playlist";
1404
1469
  videoInfos.push({
1405
1470
  url: u,
1406
1471
  meta: m,
1407
1472
  title: m.title ?? "Unknown title",
1408
1473
  uploader: m.uploader ?? "Unknown uploader",
1409
- duration: formatDuration(m.duration)
1474
+ duration: isPlaylist ? `${m.playlist_count ?? "?"} videos` : formatDuration(m.duration)
1410
1475
  });
1411
1476
  } else {
1412
1477
  failedUrls.push(u);
@@ -1414,7 +1479,17 @@ async function runWizard(urlsArg, opts = {}) {
1414
1479
  }
1415
1480
  if (videoInfos.length === 0) {
1416
1481
  spinner2.stop("Could not fetch metadata");
1417
- p3.log.error("Failed to fetch metadata for any of the provided URLs.");
1482
+ const reasons = metaResults.filter(
1483
+ (r) => r.status === "rejected"
1484
+ ).map(
1485
+ (r) => r.reason instanceof Error ? r.reason.message : String(r.reason)
1486
+ );
1487
+ p3.log.error(
1488
+ [
1489
+ "Failed to fetch metadata for any of the provided URLs.",
1490
+ ...new Set(reasons)
1491
+ ].join("\n\n")
1492
+ );
1418
1493
  process.exit(1);
1419
1494
  }
1420
1495
  spinner2.stop("Metadata loaded");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jnzlab/easy-ytdlp",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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",