@jnzlab/easy-ytdlp 1.1.1 → 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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/dist/cli.js +181 -133
- 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/README.md
CHANGED
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
|
-
|
|
498
|
-
|
|
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.
|
|
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);
|