@jnzlab/easy-ytdlp 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cli.js +181 -133
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.3 - 2026-08-05
4
+
5
+ ### Fixed
6
+
7
+ - Pasting a bare YouTube playlist link no longer fails/hangs at startup. The
8
+ metadata fetch now uses `--flat-playlist` for playlist URLs (fast, one small
9
+ line per video) and shows the playlist title + video count instead of dumping
10
+ full metadata for every video. "Just this video" on a bare playlist URL now
11
+ downloads only the first entry (`--playlist-items 1`) instead of the whole
12
+ playlist.
13
+
3
14
  ## 1.1.0 - 2026-07-27
4
15
 
5
16
  ### 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,56 @@ 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", "-f", "best"] : ["--no-playlist"];
395
+ return [...youtubeCompatFlags(), ...scopeFlags, url];
396
+ }
397
+ function normalizePlaylistMeta(raw, url) {
398
+ const entries = Array.isArray(raw) ? raw : raw && typeof raw === "object" ? [raw] : [];
399
+ const first = entries[0];
400
+ if (!first) {
401
+ return { _type: "playlist", webpage_url: url, playlist_count: 0, entries: [] };
402
+ }
403
+ const count = first.playlist_count ?? first.n_entries ?? entries.length;
404
+ return {
405
+ _type: "playlist",
406
+ id: first.playlist_id ?? first.id,
407
+ title: first.playlist_title ?? first.playlist ?? first.title,
408
+ uploader: first.playlist_uploader,
409
+ playlist: first.playlist_title ?? first.playlist,
410
+ playlist_id: first.playlist_id,
411
+ playlist_count: count,
412
+ n_entries: count,
413
+ webpage_url: url,
414
+ entries
415
+ };
416
+ }
495
417
  async function fetchMetadata(ytDlp, url) {
418
+ const playlistOnly = isPlaylistOnlyUrl(url);
496
419
  try {
497
- return await ytDlp.getVideoInfo([
498
- ...youtubeCompatFlags(),
499
- "--no-playlist",
500
- url
501
- ]);
420
+ const raw = await ytDlp.getVideoInfo(metadataArgs(url));
421
+ return playlistOnly ? normalizePlaylistMeta(raw, url) : raw;
502
422
  } catch (err) {
503
423
  const msg = err instanceof Error ? err.message : String(err);
504
424
  throw new Error(humanizeError(msg));
@@ -694,9 +614,7 @@ async function runDownload(ytDlp, flags, options = {}) {
694
614
  }
695
615
  if (code !== 0 && code !== null) {
696
616
  reject(
697
- new Error(
698
- humanizeError(lastError || `yt-dlp exited with code ${code}`)
699
- )
617
+ new Error(humanizeError(lastError || `yt-dlp exited with code ${code}`))
700
618
  );
701
619
  return;
702
620
  }
@@ -705,6 +623,135 @@ async function runDownload(ytDlp, flags, options = {}) {
705
623
  });
706
624
  }
707
625
 
626
+ // src/builder.ts
627
+ var FILENAME_TEMPLATES = {
628
+ title: "%(title)s.%(ext)s",
629
+ "title-channel": "%(title)s [%(uploader)s].%(ext)s",
630
+ "title-date": "%(upload_date)s - %(title)s.%(ext)s"
631
+ };
632
+ function audioQualityValue(q) {
633
+ return q === "good" ? "5" : "0";
634
+ }
635
+ function videoQualityFlags(quality) {
636
+ if (!quality || quality === "best") {
637
+ return ["-f", "bv*+ba/b"];
638
+ }
639
+ if (typeof quality === "object" && "height" in quality) {
640
+ const h = quality.height;
641
+ return ["-f", `bv*[height=${h}]+ba/b`];
642
+ }
643
+ return ["-S", `res:${quality}`, "-f", "bv*+ba/b"];
644
+ }
645
+ function containerFlags(container) {
646
+ if (!container || container === "best") return [];
647
+ return ["--merge-output-format", container];
648
+ }
649
+ function buildFlags(answers) {
650
+ const flags = [];
651
+ switch (answers.mode) {
652
+ case "video":
653
+ flags.push(...videoQualityFlags(answers.videoQuality));
654
+ flags.push(...containerFlags(answers.container));
655
+ break;
656
+ case "video-only":
657
+ if (answers.videoQuality && answers.videoQuality !== "best" && typeof answers.videoQuality !== "object") {
658
+ flags.push("-S", `res:${answers.videoQuality}`, "-f", "bv");
659
+ } else if (typeof answers.videoQuality === "object" && answers.videoQuality?.height) {
660
+ flags.push("-f", `bv[height=${answers.videoQuality.height}]`);
661
+ } else {
662
+ flags.push("-f", "bv");
663
+ }
664
+ flags.push(...containerFlags(answers.container));
665
+ break;
666
+ case "audio":
667
+ flags.push(
668
+ "-x",
669
+ "--audio-format",
670
+ answers.audioFormat ?? "best",
671
+ "--audio-quality",
672
+ audioQualityValue(answers.audioQuality)
673
+ );
674
+ break;
675
+ case "subs-only":
676
+ flags.push("--skip-download", "--write-subs");
677
+ break;
678
+ case "thumbnail-only":
679
+ flags.push("--skip-download", "--write-thumbnail");
680
+ break;
681
+ }
682
+ if (answers.mode === "subs-only") {
683
+ const langs = answers.subtitles.languages.length > 0 ? answers.subtitles.languages.join(",") : "all";
684
+ flags.push("--sub-langs", langs);
685
+ } else if (answers.subtitles.mode !== "none" && answers.mode !== "thumbnail-only") {
686
+ const langs = answers.subtitles.languages.length > 0 ? answers.subtitles.languages.join(",") : "all";
687
+ flags.push("--sub-langs", langs);
688
+ if (answers.subtitles.mode === "write" || answers.subtitles.mode === "both") {
689
+ flags.push("--write-subs");
690
+ }
691
+ if (answers.subtitles.mode === "embed" || answers.subtitles.mode === "both") {
692
+ flags.push("--embed-subs");
693
+ }
694
+ }
695
+ switch (answers.playlist.kind) {
696
+ case "single": {
697
+ const firstUrl = answers.urls[0];
698
+ if (firstUrl && isPlaylistOnlyUrl(firstUrl)) {
699
+ flags.push("--playlist-items", "1");
700
+ } else {
701
+ flags.push("--no-playlist");
702
+ }
703
+ break;
704
+ }
705
+ case "all":
706
+ flags.push("--yes-playlist");
707
+ break;
708
+ case "range":
709
+ flags.push(
710
+ "--yes-playlist",
711
+ "-I",
712
+ `${answers.playlist.start}:${answers.playlist.stop}`
713
+ );
714
+ break;
715
+ }
716
+ flags.push("-P", answers.outputDir);
717
+ flags.push("-o", FILENAME_TEMPLATES[answers.filenamePreset]);
718
+ if (answers.embedThumbnail && answers.mode !== "thumbnail-only") {
719
+ flags.push("--embed-thumbnail");
720
+ }
721
+ if (answers.embedMetadata) {
722
+ flags.push("--embed-metadata");
723
+ }
724
+ if (answers.sponsorBlock) {
725
+ flags.push("--sponsorblock-remove", "default");
726
+ }
727
+ flags.push("--print", "after_move:filepath");
728
+ flags.push(...answers.urls);
729
+ if (answers.urls.length > 1) {
730
+ flags.unshift("--ignore-errors");
731
+ }
732
+ return flags;
733
+ }
734
+ function formatCommand(flags) {
735
+ const quoted = flags.map(
736
+ (f) => /[\s"'\\]/.test(f) ? JSON.stringify(f) : f
737
+ );
738
+ const parts = ["yt-dlp"];
739
+ for (let i = 0; i < quoted.length; i++) {
740
+ const cur = quoted[i];
741
+ const next = quoted[i + 1];
742
+ if (cur.startsWith("-") && next && !next.startsWith("-") && !looksLikeUrl(next)) {
743
+ parts.push(`${cur} ${next}`);
744
+ i++;
745
+ } else {
746
+ parts.push(cur);
747
+ }
748
+ }
749
+ return parts.join("\n ");
750
+ }
751
+ function looksLikeUrl(s) {
752
+ return /^https?:\/\//i.test(s) || s.startsWith('"http');
753
+ }
754
+
708
755
  // src/questions.ts
709
756
  import * as p2 from "@clack/prompts";
710
757
  import { homedir } from "os";
@@ -1174,7 +1221,7 @@ async function askQuestions(url, meta) {
1174
1221
  }
1175
1222
 
1176
1223
  // src/cli.ts
1177
- var CLI_VERSION = "1.1.0";
1224
+ var CLI_VERSION = "1.1.3";
1178
1225
  function fail(message) {
1179
1226
  p3.log.error(message);
1180
1227
  process.exit(1);
@@ -1401,12 +1448,13 @@ async function runWizard(urlsArg, opts = {}) {
1401
1448
  const u = urls[i];
1402
1449
  if (r.status === "fulfilled") {
1403
1450
  const m = r.value;
1451
+ const isPlaylist = m._type === "playlist";
1404
1452
  videoInfos.push({
1405
1453
  url: u,
1406
1454
  meta: m,
1407
1455
  title: m.title ?? "Unknown title",
1408
1456
  uploader: m.uploader ?? "Unknown uploader",
1409
- duration: formatDuration(m.duration)
1457
+ duration: isPlaylist ? `${m.playlist_count ?? "?"} videos` : formatDuration(m.duration)
1410
1458
  });
1411
1459
  } else {
1412
1460
  failedUrls.push(u);
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.3",
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",