@jnzlab/easy-ytdlp 1.0.3 → 1.1.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 +28 -0
- package/README.md +28 -0
- package/dist/cli.js +505 -198
- package/package.json +10 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0 - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added a shorter default wizard that keeps common downloads focused on mode, quality, and output folder.
|
|
8
|
+
- Added an advanced-options step for subtitles, playlist handling, filename presets, container preferences, embedded thumbnail/metadata, and SponsorBlock.
|
|
9
|
+
- Added a grouped final summary with readable labels for download, source, output, and extras.
|
|
10
|
+
- Added a final action menu with start, show command, change settings, and cancel actions.
|
|
11
|
+
- Added persisted output-folder preferences so the last destination is reused on future runs.
|
|
12
|
+
- Added support for multiple URLs in one command and `--batch-file` input with blank-line and comment handling.
|
|
13
|
+
- Added compact numbered metadata previews for multi-URL jobs.
|
|
14
|
+
- Added per-URL download progress labels for multi-URL runs.
|
|
15
|
+
- Added non-interactive `--yes` mode with flags for mode, quality, output folder, audio format, subtitles, playlist range, filename preset, embedded metadata/thumbnail, and SponsorBlock.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Downloads for multiple URLs now run one URL at a time, which makes progress and failures easier to understand.
|
|
20
|
+
- CLI version output now matches the package version.
|
|
21
|
+
- Plain video-only downloads no longer warn about missing ffmpeg unless remuxing or post-processing is requested.
|
|
22
|
+
- Saved-file output now uses consistent Clack log formatting.
|
|
23
|
+
- README usage examples now cover multi-URL, batch-file, advanced wizard, and non-interactive workflows.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Batch-file read failures now show a friendly CLI error instead of a raw Node exception.
|
|
28
|
+
- Multi-URL command preview now explains that the displayed command is for the first URL and will be repeated for each URL.
|
package/README.md
CHANGED
|
@@ -22,10 +22,38 @@ easy-ytdlp <url>
|
|
|
22
22
|
```bash
|
|
23
23
|
easy-ytdlp # prompts for URL
|
|
24
24
|
easy-ytdlp https://youtu.be/dQw4w9WgXcQ # start with a URL
|
|
25
|
+
easy-ytdlp <url1> <url2> # download multiple URLs with shared settings
|
|
26
|
+
easy-ytdlp --batch-file urls.txt # read URLs from a file, one per line
|
|
25
27
|
easy-ytdlp <url> --show-command # preview the yt-dlp flags first
|
|
28
|
+
easy-ytdlp <url> --yes --mode audio --audio-format mp3
|
|
26
29
|
easy-ytdlp update-binary # force-refresh the cached yt-dlp binary
|
|
27
30
|
```
|
|
28
31
|
|
|
32
|
+
Batch files may include blank lines and comments that start with `#`.
|
|
33
|
+
When multiple URLs are provided, easy-ytdlp fetches metadata for each URL, then asks one set of questions using the first URL with valid metadata as the prompt context.
|
|
34
|
+
The default wizard keeps common downloads short. Choose advanced options to customize subtitles, playlists, filename presets, containers, and embeds.
|
|
35
|
+
|
|
36
|
+
### Non-interactive options
|
|
37
|
+
|
|
38
|
+
Use `--yes` to skip prompts and run with defaults plus any flags you provide:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
easy-ytdlp <url> --yes --mode video --quality 1080 --output ~/Videos
|
|
42
|
+
easy-ytdlp <url> --yes --mode audio --audio-format mp3 --audio-quality good
|
|
43
|
+
easy-ytdlp --batch-file urls.txt --yes --mode video --quality best
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Useful flags:
|
|
47
|
+
|
|
48
|
+
- `--mode video|audio|video-only|subs-only|thumbnail-only`
|
|
49
|
+
- `--quality best|1080|720|480|<height>`
|
|
50
|
+
- `--container best|mp4|mkv|webm`
|
|
51
|
+
- `--audio-format best|mp3|m4a|opus|flac|wav`
|
|
52
|
+
- `--subs none|embed|write|both --sub-langs en,es`
|
|
53
|
+
- `--playlist single|all|range --playlist-range 2:5`
|
|
54
|
+
- `--filename title|title-channel|title-date`
|
|
55
|
+
- `--embed-thumbnail --embed-metadata --sponsorblock`
|
|
56
|
+
|
|
29
57
|
### Example session
|
|
30
58
|
|
|
31
59
|

|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import * as p3 from "@clack/prompts";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
4
5
|
|
|
5
6
|
// src/binary.ts
|
|
6
7
|
import { createWriteStream } from "fs";
|
|
@@ -274,7 +275,10 @@ function buildFlags(answers) {
|
|
|
274
275
|
flags.push("--sponsorblock-remove", "default");
|
|
275
276
|
}
|
|
276
277
|
flags.push("--print", "after_move:filepath");
|
|
277
|
-
flags.push(answers.
|
|
278
|
+
flags.push(...answers.urls);
|
|
279
|
+
if (answers.urls.length > 1) {
|
|
280
|
+
flags.unshift("--ignore-errors");
|
|
281
|
+
}
|
|
278
282
|
return flags;
|
|
279
283
|
}
|
|
280
284
|
function formatCommand(flags) {
|
|
@@ -363,7 +367,8 @@ function ffmpegInstallHint() {
|
|
|
363
367
|
return lines.join("\n");
|
|
364
368
|
}
|
|
365
369
|
function needsFfmpeg(mode, extras) {
|
|
366
|
-
if (mode === "video"
|
|
370
|
+
if (mode === "video") return true;
|
|
371
|
+
if (mode === "video-only" && extras.remuxVideo) return true;
|
|
367
372
|
if (mode === "audio" || extras.extractAudio) return true;
|
|
368
373
|
if (extras.embedSubs || extras.embedThumbnail) return true;
|
|
369
374
|
return false;
|
|
@@ -538,7 +543,7 @@ function parsePostprocessLine(line) {
|
|
|
538
543
|
function progressFlags() {
|
|
539
544
|
return ["--newline", "--progress"];
|
|
540
545
|
}
|
|
541
|
-
async function runDownload(ytDlp, flags) {
|
|
546
|
+
async function runDownload(ytDlp, flags, options = {}) {
|
|
542
547
|
const filepaths = [];
|
|
543
548
|
let lastError = "";
|
|
544
549
|
let part = 1;
|
|
@@ -561,7 +566,7 @@ async function runDownload(ytDlp, flags) {
|
|
|
561
566
|
);
|
|
562
567
|
let barStarted = false;
|
|
563
568
|
let lastPercent = 0;
|
|
564
|
-
let phase = "Downloading";
|
|
569
|
+
let phase = options.label ? `Downloading ${options.label}` : "Downloading";
|
|
565
570
|
const ensureBar = () => {
|
|
566
571
|
if (!barStarted) {
|
|
567
572
|
bar.start(100, 0, {
|
|
@@ -577,7 +582,7 @@ async function runDownload(ytDlp, flags) {
|
|
|
577
582
|
if (!Number.isFinite(percent)) return;
|
|
578
583
|
if (percent + 5 < lastPercent) {
|
|
579
584
|
part += 1;
|
|
580
|
-
phase = `Downloading (${part})`;
|
|
585
|
+
phase = options.label ? `Downloading ${options.label} (${part})` : `Downloading (${part})`;
|
|
581
586
|
}
|
|
582
587
|
lastPercent = percent;
|
|
583
588
|
ensureBar();
|
|
@@ -613,7 +618,7 @@ async function runDownload(ytDlp, flags) {
|
|
|
613
618
|
if (lastPercent >= 99) {
|
|
614
619
|
part += 1;
|
|
615
620
|
lastPercent = 0;
|
|
616
|
-
phase = `Downloading (${part})`;
|
|
621
|
+
phase = options.label ? `Downloading ${options.label} (${part})` : `Downloading (${part})`;
|
|
617
622
|
ensureBar();
|
|
618
623
|
bar.update(0, {
|
|
619
624
|
phase,
|
|
@@ -703,7 +708,28 @@ async function runDownload(ytDlp, flags) {
|
|
|
703
708
|
// src/questions.ts
|
|
704
709
|
import * as p2 from "@clack/prompts";
|
|
705
710
|
import { homedir } from "os";
|
|
706
|
-
import { join as
|
|
711
|
+
import { join as join5 } from "path";
|
|
712
|
+
|
|
713
|
+
// src/preferences.ts
|
|
714
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
715
|
+
import { dirname, join as join4 } from "path";
|
|
716
|
+
import envPaths2 from "env-paths";
|
|
717
|
+
var paths2 = envPaths2("easy-ytdlp");
|
|
718
|
+
var PREFS_PATH = join4(paths2.config, "preferences.json");
|
|
719
|
+
async function loadPreferences() {
|
|
720
|
+
try {
|
|
721
|
+
const raw = await readFile(PREFS_PATH, "utf-8");
|
|
722
|
+
const parsed = JSON.parse(raw);
|
|
723
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
724
|
+
} catch {
|
|
725
|
+
return {};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function savePreferences(prefs) {
|
|
729
|
+
await mkdir2(dirname(PREFS_PATH), { recursive: true });
|
|
730
|
+
await writeFile(PREFS_PATH, `${JSON.stringify(prefs, null, 2)}
|
|
731
|
+
`, "utf-8");
|
|
732
|
+
}
|
|
707
733
|
|
|
708
734
|
// src/ui.ts
|
|
709
735
|
import * as p from "@clack/prompts";
|
|
@@ -730,13 +756,15 @@ function wrapLine(line, width) {
|
|
|
730
756
|
function showNote(body, title) {
|
|
731
757
|
p.note(wrapText(body, contentWidth(14)), title);
|
|
732
758
|
}
|
|
733
|
-
function showSaved(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
759
|
+
function showSaved(paths3) {
|
|
760
|
+
if (paths3.length === 0) return;
|
|
761
|
+
if (paths3.length === 1) {
|
|
762
|
+
p.log.success(paths3[0]);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
p.log.success(`Saved ${paths3.length} files`);
|
|
766
|
+
for (const filePath of paths3) {
|
|
767
|
+
p.log.info(filePath);
|
|
740
768
|
}
|
|
741
769
|
}
|
|
742
770
|
function showCommand(command) {
|
|
@@ -764,6 +792,9 @@ function looksLikeUrl2(input) {
|
|
|
764
792
|
return false;
|
|
765
793
|
}
|
|
766
794
|
}
|
|
795
|
+
function splitUrlList(text2) {
|
|
796
|
+
return text2.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
797
|
+
}
|
|
767
798
|
function formatDuration(seconds) {
|
|
768
799
|
if (seconds == null || Number.isNaN(seconds)) return "unknown duration";
|
|
769
800
|
const h = Math.floor(seconds / 3600);
|
|
@@ -803,26 +834,85 @@ function isPlaylistUrl(url, meta) {
|
|
|
803
834
|
return false;
|
|
804
835
|
}
|
|
805
836
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
837
|
+
function modeLabel(mode) {
|
|
838
|
+
const labels = {
|
|
839
|
+
video: "Video with audio",
|
|
840
|
+
audio: "Audio only",
|
|
841
|
+
"video-only": "Video only",
|
|
842
|
+
"subs-only": "Subtitles only",
|
|
843
|
+
"thumbnail-only": "Thumbnail only"
|
|
844
|
+
};
|
|
845
|
+
return labels[mode];
|
|
846
|
+
}
|
|
847
|
+
function filenameLabel(preset) {
|
|
848
|
+
const labels = {
|
|
849
|
+
title: "Title only",
|
|
850
|
+
"title-channel": "Title + Channel",
|
|
851
|
+
"title-date": "Title + Upload Date"
|
|
852
|
+
};
|
|
853
|
+
return labels[preset];
|
|
854
|
+
}
|
|
855
|
+
function playlistLabel(playlist) {
|
|
856
|
+
if (playlist.kind === "single") return "Just this video";
|
|
857
|
+
if (playlist.kind === "all") return "Whole playlist";
|
|
858
|
+
return `Playlist items ${playlist.start}:${playlist.stop}`;
|
|
859
|
+
}
|
|
860
|
+
function videoQualityLabel(quality) {
|
|
861
|
+
if (!quality || quality === "best") return "Best available";
|
|
862
|
+
if (typeof quality === "object") return `${quality.height}p`;
|
|
863
|
+
return `Up to ${quality}p`;
|
|
864
|
+
}
|
|
865
|
+
function summarizeAnswers(answers, sourceCount = answers.urls.length) {
|
|
866
|
+
const lines = [
|
|
867
|
+
"Download",
|
|
868
|
+
` Mode ${modeLabel(answers.mode)}`,
|
|
869
|
+
answers.mode === "video" || answers.mode === "video-only" ? ` Quality ${videoQualityLabel(answers.videoQuality)}` : null,
|
|
870
|
+
answers.container ? ` Container ${answers.container}` : null,
|
|
871
|
+
answers.mode === "audio" ? ` Audio ${answers.audioFormat ?? "best"} (${answers.audioQuality ?? "best"})` : null,
|
|
872
|
+
"",
|
|
873
|
+
"Sources",
|
|
874
|
+
` URLs ${sourceCount}`,
|
|
875
|
+
` Playlist ${playlistLabel(answers.playlist)}`,
|
|
876
|
+
"",
|
|
877
|
+
"Output",
|
|
878
|
+
` Folder ${answers.outputDir}`,
|
|
879
|
+
` Filename ${filenameLabel(answers.filenamePreset)}`,
|
|
880
|
+
"",
|
|
881
|
+
"Extras",
|
|
882
|
+
answers.subtitles.mode !== "none" ? ` Subtitles ${answers.subtitles.mode} [${answers.subtitles.languages.join(", ")}]` : " Subtitles none",
|
|
883
|
+
answers.embedThumbnail ? " Thumbnail embed" : null,
|
|
884
|
+
answers.embedMetadata ? " Metadata embed" : null,
|
|
885
|
+
answers.sponsorBlock ? " SponsorBlock remove segments" : null
|
|
886
|
+
];
|
|
887
|
+
return lines.filter((line) => line != null).join("\n");
|
|
888
|
+
}
|
|
889
|
+
async function promptUrls(initial) {
|
|
890
|
+
if (initial && initial.length > 0) {
|
|
891
|
+
for (const u of initial) {
|
|
892
|
+
if (!looksLikeUrl2(u)) {
|
|
893
|
+
p2.log.error(`Invalid URL: ${u}`);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return initial;
|
|
898
|
+
}
|
|
899
|
+
const input = await p2.text({
|
|
900
|
+
message: "Paste video URL(s)",
|
|
901
|
+
placeholder: "One or more URLs, separated by spaces or commas",
|
|
902
|
+
initialValue: "",
|
|
812
903
|
validate: (v) => {
|
|
813
|
-
if (!v?.trim()) return "URL is required";
|
|
814
|
-
|
|
904
|
+
if (!v?.trim()) return "At least one URL is required";
|
|
905
|
+
const urls = splitUrlList(v.trim());
|
|
906
|
+
if (urls.length === 0) return "At least one valid URL is required";
|
|
907
|
+
for (const u of urls) {
|
|
908
|
+
if (!looksLikeUrl2(u)) return `Invalid URL: ${u}`;
|
|
909
|
+
}
|
|
815
910
|
}
|
|
816
911
|
});
|
|
817
|
-
exitOnCancel(
|
|
818
|
-
return String(
|
|
912
|
+
exitOnCancel(input);
|
|
913
|
+
return splitUrlList(String(input).trim());
|
|
819
914
|
}
|
|
820
915
|
async function askQuestions(url, meta) {
|
|
821
|
-
const title = meta.title ?? "Unknown title";
|
|
822
|
-
const uploader = meta.uploader ?? "Unknown uploader";
|
|
823
|
-
const duration = formatDuration(meta.duration);
|
|
824
|
-
showNote(`${title}
|
|
825
|
-
by ${uploader} \xB7 ${duration}`, "Found");
|
|
826
916
|
const mode = await p2.select({
|
|
827
917
|
message: "What do you want to download?",
|
|
828
918
|
options: [
|
|
@@ -834,11 +924,17 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
834
924
|
]
|
|
835
925
|
});
|
|
836
926
|
exitOnCancel(mode);
|
|
927
|
+
const selectedMode = mode;
|
|
837
928
|
let videoQuality;
|
|
838
|
-
let container;
|
|
929
|
+
let container = selectedMode === "video" || selectedMode === "video-only" ? "best" : void 0;
|
|
839
930
|
let audioFormat;
|
|
840
931
|
let audioQuality;
|
|
841
|
-
|
|
932
|
+
let subMode = "none";
|
|
933
|
+
let subLangs = [];
|
|
934
|
+
let playlist = { kind: "single" };
|
|
935
|
+
let filenamePreset = "title";
|
|
936
|
+
let extras = [];
|
|
937
|
+
if (selectedMode === "video" || selectedMode === "video-only") {
|
|
842
938
|
const resolutions = availableResolutions(meta);
|
|
843
939
|
const qualityOpts = [
|
|
844
940
|
{ value: "best", label: "Best available" },
|
|
@@ -872,19 +968,8 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
872
968
|
} else {
|
|
873
969
|
videoQuality = q;
|
|
874
970
|
}
|
|
875
|
-
const c = await p2.select({
|
|
876
|
-
message: "Container preference?",
|
|
877
|
-
options: [
|
|
878
|
-
{ value: "best", label: "Best available" },
|
|
879
|
-
{ value: "mp4", label: "mp4" },
|
|
880
|
-
{ value: "mkv", label: "mkv" },
|
|
881
|
-
{ value: "webm", label: "webm" }
|
|
882
|
-
]
|
|
883
|
-
});
|
|
884
|
-
exitOnCancel(c);
|
|
885
|
-
container = c;
|
|
886
971
|
}
|
|
887
|
-
if (
|
|
972
|
+
if (selectedMode === "audio") {
|
|
888
973
|
const fmt = await p2.select({
|
|
889
974
|
message: "Audio format?",
|
|
890
975
|
options: [
|
|
@@ -908,9 +993,7 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
908
993
|
exitOnCancel(aq);
|
|
909
994
|
audioQuality = aq;
|
|
910
995
|
}
|
|
911
|
-
|
|
912
|
-
let subLangs = [];
|
|
913
|
-
if (mode === "subs-only") {
|
|
996
|
+
if (selectedMode === "subs-only") {
|
|
914
997
|
subMode = "write";
|
|
915
998
|
const langs = availableSubtitleLangs(meta);
|
|
916
999
|
if (langs.length === 0) {
|
|
@@ -929,7 +1012,36 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
929
1012
|
const sel = picked;
|
|
930
1013
|
subLangs = sel.includes("all") ? ["all"] : sel;
|
|
931
1014
|
}
|
|
932
|
-
}
|
|
1015
|
+
}
|
|
1016
|
+
const prefs = await loadPreferences();
|
|
1017
|
+
const defaultDir = prefs.outputDir ?? join5(homedir(), "Downloads");
|
|
1018
|
+
const outDir = await p2.text({
|
|
1019
|
+
message: "Destination folder",
|
|
1020
|
+
initialValue: defaultDir,
|
|
1021
|
+
validate: (v) => !v?.trim() ? "Folder is required" : void 0
|
|
1022
|
+
});
|
|
1023
|
+
exitOnCancel(outDir);
|
|
1024
|
+
const outputDir = String(outDir).trim();
|
|
1025
|
+
await savePreferences({ ...prefs, outputDir });
|
|
1026
|
+
const customize = selectedMode === "subs-only" || selectedMode === "thumbnail-only" ? false : await p2.confirm({
|
|
1027
|
+
message: "Customize advanced options?",
|
|
1028
|
+
initialValue: false
|
|
1029
|
+
});
|
|
1030
|
+
exitOnCancel(customize);
|
|
1031
|
+
if (customize && (selectedMode === "video" || selectedMode === "video-only")) {
|
|
1032
|
+
const c = await p2.select({
|
|
1033
|
+
message: "Container preference?",
|
|
1034
|
+
options: [
|
|
1035
|
+
{ value: "best", label: "Best available" },
|
|
1036
|
+
{ value: "mp4", label: "mp4" },
|
|
1037
|
+
{ value: "mkv", label: "mkv" },
|
|
1038
|
+
{ value: "webm", label: "webm" }
|
|
1039
|
+
]
|
|
1040
|
+
});
|
|
1041
|
+
exitOnCancel(c);
|
|
1042
|
+
container = c;
|
|
1043
|
+
}
|
|
1044
|
+
if (customize && selectedMode !== "thumbnail-only") {
|
|
933
1045
|
const wantSubs = await p2.confirm({
|
|
934
1046
|
message: "Download subtitles?",
|
|
935
1047
|
initialValue: false
|
|
@@ -964,71 +1076,62 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
964
1076
|
exitOnCancel(how);
|
|
965
1077
|
subMode = how;
|
|
966
1078
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1079
|
+
if (isPlaylistUrl(url, meta)) {
|
|
1080
|
+
const pl = await p2.select({
|
|
1081
|
+
message: "This URL is part of a playlist. What should we download?",
|
|
1082
|
+
options: [
|
|
1083
|
+
{ value: "single", label: "Just this video" },
|
|
1084
|
+
{ value: "all", label: "Whole playlist" },
|
|
1085
|
+
{ value: "range", label: "A specific range" }
|
|
1086
|
+
]
|
|
1087
|
+
});
|
|
1088
|
+
exitOnCancel(pl);
|
|
1089
|
+
if (pl === "single") {
|
|
1090
|
+
playlist = { kind: "single" };
|
|
1091
|
+
} else if (pl === "all") {
|
|
1092
|
+
playlist = { kind: "all" };
|
|
1093
|
+
} else {
|
|
1094
|
+
const start = await p2.text({
|
|
1095
|
+
message: "Playlist start index (1-based)",
|
|
1096
|
+
initialValue: "1",
|
|
1097
|
+
validate: (v) => {
|
|
1098
|
+
const n = Number(v);
|
|
1099
|
+
if (!Number.isInteger(n) || n < 1) return "Enter a positive integer";
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
exitOnCancel(start);
|
|
1103
|
+
const stop = await p2.text({
|
|
1104
|
+
message: "Playlist stop index (inclusive)",
|
|
1105
|
+
initialValue: String(meta.playlist_count ?? 10),
|
|
1106
|
+
validate: (v) => {
|
|
1107
|
+
const n = Number(v);
|
|
1108
|
+
if (!Number.isInteger(n) || n < 1) return "Enter a positive integer";
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
exitOnCancel(stop);
|
|
1112
|
+
playlist = {
|
|
1113
|
+
kind: "range",
|
|
1114
|
+
start: Number(start),
|
|
1115
|
+
stop: Number(stop)
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const pickedFilename = await p2.select({
|
|
1120
|
+
message: "Filename style?",
|
|
972
1121
|
options: [
|
|
973
|
-
{ value: "
|
|
974
|
-
{
|
|
975
|
-
|
|
1122
|
+
{ value: "title", label: "Title only" },
|
|
1123
|
+
{
|
|
1124
|
+
value: "title-channel",
|
|
1125
|
+
label: "Title + Channel"
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
value: "title-date",
|
|
1129
|
+
label: "Title + Upload Date"
|
|
1130
|
+
}
|
|
976
1131
|
]
|
|
977
1132
|
});
|
|
978
|
-
exitOnCancel(
|
|
979
|
-
|
|
980
|
-
playlist = { kind: "single" };
|
|
981
|
-
} else if (pl === "all") {
|
|
982
|
-
playlist = { kind: "all" };
|
|
983
|
-
} else {
|
|
984
|
-
const start = await p2.text({
|
|
985
|
-
message: "Playlist start index (1-based)",
|
|
986
|
-
initialValue: "1",
|
|
987
|
-
validate: (v) => {
|
|
988
|
-
const n = Number(v);
|
|
989
|
-
if (!Number.isInteger(n) || n < 1) return "Enter a positive integer";
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
exitOnCancel(start);
|
|
993
|
-
const stop = await p2.text({
|
|
994
|
-
message: "Playlist stop index (inclusive)",
|
|
995
|
-
initialValue: String(meta.playlist_count ?? 10),
|
|
996
|
-
validate: (v) => {
|
|
997
|
-
const n = Number(v);
|
|
998
|
-
if (!Number.isInteger(n) || n < 1) return "Enter a positive integer";
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
exitOnCancel(stop);
|
|
1002
|
-
playlist = {
|
|
1003
|
-
kind: "range",
|
|
1004
|
-
start: Number(start),
|
|
1005
|
-
stop: Number(stop)
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
const defaultDir = join4(homedir(), "Downloads");
|
|
1010
|
-
const outDir = await p2.text({
|
|
1011
|
-
message: "Destination folder",
|
|
1012
|
-
initialValue: defaultDir,
|
|
1013
|
-
validate: (v) => !v?.trim() ? "Folder is required" : void 0
|
|
1014
|
-
});
|
|
1015
|
-
exitOnCancel(outDir);
|
|
1016
|
-
const filenamePreset = await p2.select({
|
|
1017
|
-
message: "Filename style?",
|
|
1018
|
-
options: [
|
|
1019
|
-
{ value: "title", label: "Title only" },
|
|
1020
|
-
{
|
|
1021
|
-
value: "title-channel",
|
|
1022
|
-
label: "Title + Channel"
|
|
1023
|
-
},
|
|
1024
|
-
{
|
|
1025
|
-
value: "title-date",
|
|
1026
|
-
label: "Title + Upload Date"
|
|
1027
|
-
}
|
|
1028
|
-
]
|
|
1029
|
-
});
|
|
1030
|
-
exitOnCancel(filenamePreset);
|
|
1031
|
-
const extras = mode === "subs-only" || mode === "thumbnail-only" ? [] : await (async () => {
|
|
1133
|
+
exitOnCancel(pickedFilename);
|
|
1134
|
+
filenamePreset = pickedFilename;
|
|
1032
1135
|
const e = await p2.multiselect({
|
|
1033
1136
|
message: "Extras (optional)",
|
|
1034
1137
|
options: [
|
|
@@ -1045,29 +1148,10 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
1045
1148
|
required: false
|
|
1046
1149
|
});
|
|
1047
1150
|
exitOnCancel(e);
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
const summaryLines = [
|
|
1053
|
-
`Mode: ${selectedMode}`,
|
|
1054
|
-
videoQuality ? `Quality: ${typeof videoQuality === "object" ? `${videoQuality.height}p` : videoQuality}` : null,
|
|
1055
|
-
container ? `Container: ${container}` : null,
|
|
1056
|
-
audioFormat ? `Audio: ${audioFormat} (${audioQuality ?? "best"})` : null,
|
|
1057
|
-
subMode !== "none" ? `Subtitles: ${subMode} [${subLangs.join(", ")}]` : "Subtitles: none",
|
|
1058
|
-
`Playlist: ${playlist.kind}${playlist.kind === "range" ? ` ${playlist.start}:${playlist.stop}` : ""}`,
|
|
1059
|
-
`Output: ${String(outDir).trim()}`,
|
|
1060
|
-
`Filename: ${selectedFilename}`,
|
|
1061
|
-
extras.length ? `Extras: ${extras.join(", ")}` : "Extras: none"
|
|
1062
|
-
].filter(Boolean).join("\n");
|
|
1063
|
-
showNote(summaryLines, "Summary");
|
|
1064
|
-
const showCmd = await p2.confirm({
|
|
1065
|
-
message: "Show the yt-dlp command before downloading?",
|
|
1066
|
-
initialValue: false
|
|
1067
|
-
});
|
|
1068
|
-
exitOnCancel(showCmd);
|
|
1069
|
-
return {
|
|
1070
|
-
url,
|
|
1151
|
+
extras = e;
|
|
1152
|
+
}
|
|
1153
|
+
const answers = {
|
|
1154
|
+
urls: [url],
|
|
1071
1155
|
mode: selectedMode,
|
|
1072
1156
|
videoQuality,
|
|
1073
1157
|
container,
|
|
@@ -1078,17 +1162,193 @@ by ${uploader} \xB7 ${duration}`, "Found");
|
|
|
1078
1162
|
languages: subLangs
|
|
1079
1163
|
},
|
|
1080
1164
|
playlist,
|
|
1081
|
-
outputDir
|
|
1082
|
-
filenamePreset
|
|
1165
|
+
outputDir,
|
|
1166
|
+
filenamePreset,
|
|
1083
1167
|
embedThumbnail: extras.includes("thumbnail"),
|
|
1084
1168
|
embedMetadata: extras.includes("metadata"),
|
|
1085
1169
|
sponsorBlock: extras.includes("sponsorblock"),
|
|
1086
|
-
showCommand:
|
|
1170
|
+
showCommand: false
|
|
1087
1171
|
};
|
|
1172
|
+
showNote(summarizeAnswers(answers), "Summary");
|
|
1173
|
+
return answers;
|
|
1088
1174
|
}
|
|
1089
1175
|
|
|
1090
1176
|
// src/cli.ts
|
|
1091
|
-
|
|
1177
|
+
var CLI_VERSION = "1.1.0";
|
|
1178
|
+
function fail(message) {
|
|
1179
|
+
p3.log.error(message);
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
function pick(value, allowed, name, fallback) {
|
|
1183
|
+
if (value == null) return fallback;
|
|
1184
|
+
if (allowed.includes(value)) return value;
|
|
1185
|
+
fail(`Invalid ${name}: ${value}. Expected one of: ${allowed.join(", ")}`);
|
|
1186
|
+
}
|
|
1187
|
+
function parseQuality(value) {
|
|
1188
|
+
if (!value || value === "best") return "best";
|
|
1189
|
+
if (value === "1080" || value === "720" || value === "480") return value;
|
|
1190
|
+
const height = Number(value.replace(/p$/i, ""));
|
|
1191
|
+
if (Number.isInteger(height) && height > 0) return { height };
|
|
1192
|
+
fail(`Invalid quality: ${value}. Use best, 1080, 720, 480, or a height like 1440.`);
|
|
1193
|
+
}
|
|
1194
|
+
function parsePlaylist(options) {
|
|
1195
|
+
const kind = pick(
|
|
1196
|
+
options.playlist,
|
|
1197
|
+
["single", "all", "range"],
|
|
1198
|
+
"playlist",
|
|
1199
|
+
"single"
|
|
1200
|
+
);
|
|
1201
|
+
if (kind !== "range") return { kind };
|
|
1202
|
+
if (!options.playlistRange) {
|
|
1203
|
+
fail("--playlist range requires --playlist-range <start:stop>");
|
|
1204
|
+
}
|
|
1205
|
+
const [startRaw, stopRaw] = options.playlistRange.split(":");
|
|
1206
|
+
const start = Number(startRaw);
|
|
1207
|
+
const stop = Number(stopRaw);
|
|
1208
|
+
if (!Number.isInteger(start) || !Number.isInteger(stop) || start < 1 || stop < start) {
|
|
1209
|
+
fail("Invalid --playlist-range. Use a 1-based inclusive range like 2:5.");
|
|
1210
|
+
}
|
|
1211
|
+
return { kind: "range", start, stop };
|
|
1212
|
+
}
|
|
1213
|
+
function parseLanguages(value) {
|
|
1214
|
+
return value ? value.split(",").map((lang) => lang.trim()).filter(Boolean) : [];
|
|
1215
|
+
}
|
|
1216
|
+
function compactTitle(title, max = 72) {
|
|
1217
|
+
return title.length > max ? `${title.slice(0, max - 1)}\u2026` : title;
|
|
1218
|
+
}
|
|
1219
|
+
function answersFromOptions(urls, options) {
|
|
1220
|
+
const mode = pick(
|
|
1221
|
+
options.mode,
|
|
1222
|
+
["video", "audio", "video-only", "subs-only", "thumbnail-only"],
|
|
1223
|
+
"mode",
|
|
1224
|
+
"video"
|
|
1225
|
+
);
|
|
1226
|
+
const subtitlesMode = pick(
|
|
1227
|
+
options.subs,
|
|
1228
|
+
["none", "embed", "write", "both"],
|
|
1229
|
+
"subs",
|
|
1230
|
+
mode === "subs-only" ? "write" : "none"
|
|
1231
|
+
);
|
|
1232
|
+
return {
|
|
1233
|
+
urls,
|
|
1234
|
+
mode,
|
|
1235
|
+
videoQuality: mode === "video" || mode === "video-only" ? parseQuality(options.quality) : void 0,
|
|
1236
|
+
container: mode === "video" || mode === "video-only" ? pick(
|
|
1237
|
+
options.container,
|
|
1238
|
+
["best", "mp4", "mkv", "webm"],
|
|
1239
|
+
"container",
|
|
1240
|
+
"best"
|
|
1241
|
+
) : void 0,
|
|
1242
|
+
audioFormat: mode === "audio" ? pick(
|
|
1243
|
+
options.audioFormat,
|
|
1244
|
+
["best", "mp3", "m4a", "opus", "flac", "wav"],
|
|
1245
|
+
"audio-format",
|
|
1246
|
+
"best"
|
|
1247
|
+
) : void 0,
|
|
1248
|
+
audioQuality: mode === "audio" ? pick(
|
|
1249
|
+
options.audioQuality,
|
|
1250
|
+
["best", "good"],
|
|
1251
|
+
"audio-quality",
|
|
1252
|
+
"best"
|
|
1253
|
+
) : void 0,
|
|
1254
|
+
subtitles: {
|
|
1255
|
+
mode: subtitlesMode,
|
|
1256
|
+
languages: parseLanguages(options.subLangs)
|
|
1257
|
+
},
|
|
1258
|
+
playlist: parsePlaylist(options),
|
|
1259
|
+
outputDir: options.output ?? process.cwd(),
|
|
1260
|
+
filenamePreset: pick(
|
|
1261
|
+
options.filename,
|
|
1262
|
+
["title", "title-channel", "title-date"],
|
|
1263
|
+
"filename",
|
|
1264
|
+
"title"
|
|
1265
|
+
),
|
|
1266
|
+
embedThumbnail: Boolean(options.embedThumbnail),
|
|
1267
|
+
embedMetadata: Boolean(options.embedMetadata),
|
|
1268
|
+
sponsorBlock: Boolean(options.sponsorblock),
|
|
1269
|
+
showCommand: Boolean(options.showCommand)
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
function displayFlagsForAnswers(answers) {
|
|
1273
|
+
const urls = answers.urls.length > 1 ? [answers.urls[0]] : answers.urls;
|
|
1274
|
+
return [
|
|
1275
|
+
...youtubeCompatFlags(),
|
|
1276
|
+
...progressFlags(),
|
|
1277
|
+
...buildFlags({ ...answers, urls })
|
|
1278
|
+
];
|
|
1279
|
+
}
|
|
1280
|
+
function showCommandForAnswers(answers) {
|
|
1281
|
+
if (answers.urls.length > 1) {
|
|
1282
|
+
p3.log.info("Showing the command for the first URL. It will be repeated for each URL.");
|
|
1283
|
+
}
|
|
1284
|
+
showCommand(formatCommand(displayFlagsForAnswers(answers)));
|
|
1285
|
+
}
|
|
1286
|
+
async function confirmStart(answers) {
|
|
1287
|
+
while (true) {
|
|
1288
|
+
const action = await p3.select({
|
|
1289
|
+
message: "Ready?",
|
|
1290
|
+
options: [
|
|
1291
|
+
{ value: "start", label: "Start download" },
|
|
1292
|
+
{ value: "show-command", label: "Show yt-dlp command" },
|
|
1293
|
+
{ value: "change", label: "Change settings" },
|
|
1294
|
+
{ value: "cancel", label: "Cancel" }
|
|
1295
|
+
]
|
|
1296
|
+
});
|
|
1297
|
+
if (p3.isCancel(action) || action === "cancel") {
|
|
1298
|
+
p3.cancel("Cancelled.");
|
|
1299
|
+
process.exit(0);
|
|
1300
|
+
}
|
|
1301
|
+
if (action === "show-command") {
|
|
1302
|
+
showCommandForAnswers(answers);
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
return action;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
async function ensureFfmpegIfNeeded(answers) {
|
|
1309
|
+
if (!needsFfmpeg(answers.mode, {
|
|
1310
|
+
embedSubs: answers.subtitles.mode === "embed" || answers.subtitles.mode === "both",
|
|
1311
|
+
embedThumbnail: answers.embedThumbnail,
|
|
1312
|
+
extractAudio: answers.mode === "audio",
|
|
1313
|
+
remuxVideo: answers.container != null && answers.container !== "best"
|
|
1314
|
+
})) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const status = await checkFfmpeg();
|
|
1318
|
+
if (status.ok) return;
|
|
1319
|
+
p3.log.warn(
|
|
1320
|
+
[
|
|
1321
|
+
"ffmpeg/ffprobe not found on PATH.",
|
|
1322
|
+
ffmpegInstallHint()
|
|
1323
|
+
].join("\n\n")
|
|
1324
|
+
);
|
|
1325
|
+
const cont = await p3.confirm({
|
|
1326
|
+
message: "Continue anyway? (download may fail at merge/extract)",
|
|
1327
|
+
initialValue: false
|
|
1328
|
+
});
|
|
1329
|
+
if (p3.isCancel(cont) || !cont) {
|
|
1330
|
+
p3.cancel("Cancelled.");
|
|
1331
|
+
process.exit(0);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
async function runDownloads(ytDlp, answers) {
|
|
1335
|
+
const allPaths = [];
|
|
1336
|
+
for (let i = 0; i < answers.urls.length; i++) {
|
|
1337
|
+
const url = answers.urls[i];
|
|
1338
|
+
const label = answers.urls.length > 1 ? `${i + 1} of ${answers.urls.length}` : void 0;
|
|
1339
|
+
const flags = buildFlags({ ...answers, urls: [url] });
|
|
1340
|
+
if (label) p3.log.info(`Download ${label}`);
|
|
1341
|
+
try {
|
|
1342
|
+
const result = await runDownload(ytDlp, flags, { label });
|
|
1343
|
+
allPaths.push(...result.filepaths);
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
if (answers.urls.length === 1) throw err;
|
|
1346
|
+
p3.log.warn(err instanceof Error ? err.message : String(err));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return allPaths;
|
|
1350
|
+
}
|
|
1351
|
+
async function runWizard(urlsArg, opts = {}) {
|
|
1092
1352
|
p3.intro("easy-ytdlp");
|
|
1093
1353
|
const spinner2 = p3.spinner();
|
|
1094
1354
|
spinner2.start("Preparing yt-dlp binary\u2026");
|
|
@@ -1105,75 +1365,98 @@ async function runWizard(urlArg, opts = {}) {
|
|
|
1105
1365
|
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1106
1366
|
process.exit(1);
|
|
1107
1367
|
}
|
|
1108
|
-
const
|
|
1368
|
+
const urls = await promptUrls(urlsArg);
|
|
1369
|
+
if (opts.yes) {
|
|
1370
|
+
const answers2 = answersFromOptions(urls, opts);
|
|
1371
|
+
showNote(summarizeAnswers(answers2), "Summary");
|
|
1372
|
+
if (answers2.showCommand) {
|
|
1373
|
+
showCommandForAnswers(answers2);
|
|
1374
|
+
}
|
|
1375
|
+
await ensureFfmpegIfNeeded(answers2);
|
|
1376
|
+
p3.log.info("Starting download\u2026");
|
|
1377
|
+
try {
|
|
1378
|
+
const filepaths = await runDownloads(ytDlp, answers2);
|
|
1379
|
+
if (filepaths.length > 0) {
|
|
1380
|
+
showSaved(filepaths);
|
|
1381
|
+
} else {
|
|
1382
|
+
p3.log.success("Done. (No filepath printed \u2014 check your output folder.)");
|
|
1383
|
+
p3.log.info(`Output folder: ${answers2.outputDir}`);
|
|
1384
|
+
}
|
|
1385
|
+
p3.outro("Finished");
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1388
|
+
p3.outro("Failed");
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1109
1393
|
spinner2.start("Fetching video info\u2026");
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1394
|
+
const metaResults = await Promise.allSettled(
|
|
1395
|
+
urls.map((u) => fetchMetadata(ytDlp, u))
|
|
1396
|
+
);
|
|
1397
|
+
const videoInfos = [];
|
|
1398
|
+
const failedUrls = [];
|
|
1399
|
+
for (let i = 0; i < metaResults.length; i++) {
|
|
1400
|
+
const r = metaResults[i];
|
|
1401
|
+
const u = urls[i];
|
|
1402
|
+
if (r.status === "fulfilled") {
|
|
1403
|
+
const m = r.value;
|
|
1404
|
+
videoInfos.push({
|
|
1405
|
+
url: u,
|
|
1406
|
+
meta: m,
|
|
1407
|
+
title: m.title ?? "Unknown title",
|
|
1408
|
+
uploader: m.uploader ?? "Unknown uploader",
|
|
1409
|
+
duration: formatDuration(m.duration)
|
|
1410
|
+
});
|
|
1411
|
+
} else {
|
|
1412
|
+
failedUrls.push(u);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (videoInfos.length === 0) {
|
|
1115
1416
|
spinner2.stop("Could not fetch metadata");
|
|
1116
|
-
p3.log.error(
|
|
1417
|
+
p3.log.error("Failed to fetch metadata for any of the provided URLs.");
|
|
1117
1418
|
process.exit(1);
|
|
1118
1419
|
}
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const displayFlags = [
|
|
1125
|
-
...youtubeCompatFlags(),
|
|
1126
|
-
...progressFlags(),
|
|
1127
|
-
...flags
|
|
1128
|
-
];
|
|
1129
|
-
if (answers.showCommand) {
|
|
1130
|
-
showCommand(formatCommand(displayFlags));
|
|
1131
|
-
const proceed = await p3.confirm({
|
|
1132
|
-
message: "Run this command?",
|
|
1133
|
-
initialValue: true
|
|
1134
|
-
});
|
|
1135
|
-
if (p3.isCancel(proceed) || !proceed) {
|
|
1136
|
-
p3.cancel("Cancelled.");
|
|
1137
|
-
process.exit(0);
|
|
1138
|
-
}
|
|
1420
|
+
spinner2.stop("Metadata loaded");
|
|
1421
|
+
if (videoInfos.length === 1) {
|
|
1422
|
+
const v = videoInfos[0];
|
|
1423
|
+
showNote(`${v.title}
|
|
1424
|
+
by ${v.uploader} \xB7 ${v.duration}`, "Found");
|
|
1139
1425
|
} else {
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
p3.cancel("Cancelled.");
|
|
1146
|
-
process.exit(0);
|
|
1147
|
-
}
|
|
1426
|
+
const lines = videoInfos.map(
|
|
1427
|
+
(v, index) => `${index + 1}. ${compactTitle(v.title)}
|
|
1428
|
+
by ${v.uploader} \xB7 ${v.duration}`
|
|
1429
|
+
);
|
|
1430
|
+
showNote(lines.join("\n\n"), `Found (${videoInfos.length} videos)`);
|
|
1148
1431
|
}
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
initialValue: false
|
|
1165
|
-
});
|
|
1166
|
-
if (p3.isCancel(cont) || !cont) {
|
|
1167
|
-
p3.cancel("Cancelled.");
|
|
1168
|
-
process.exit(0);
|
|
1169
|
-
}
|
|
1432
|
+
if (failedUrls.length > 0) {
|
|
1433
|
+
p3.log.warn(
|
|
1434
|
+
`Could not fetch metadata for ${failedUrls.length} URL(s). They will still be downloaded with shared settings.`
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
const primary = videoInfos[0];
|
|
1438
|
+
if (urls.length > 1) {
|
|
1439
|
+
p3.log.info(`Using shared settings based on: ${primary.title}`);
|
|
1440
|
+
}
|
|
1441
|
+
let answers;
|
|
1442
|
+
while (true) {
|
|
1443
|
+
answers = await askQuestions(primary.url, primary.meta);
|
|
1444
|
+
answers.urls = urls;
|
|
1445
|
+
if (opts.showCommand) {
|
|
1446
|
+
answers.showCommand = true;
|
|
1170
1447
|
}
|
|
1448
|
+
if (answers.showCommand) {
|
|
1449
|
+
showCommandForAnswers(answers);
|
|
1450
|
+
}
|
|
1451
|
+
const action = await confirmStart(answers);
|
|
1452
|
+
if (action === "start") break;
|
|
1171
1453
|
}
|
|
1454
|
+
await ensureFfmpegIfNeeded(answers);
|
|
1172
1455
|
p3.log.info("Starting download\u2026");
|
|
1173
1456
|
try {
|
|
1174
|
-
const
|
|
1175
|
-
if (
|
|
1176
|
-
showSaved(
|
|
1457
|
+
const filepaths = await runDownloads(ytDlp, answers);
|
|
1458
|
+
if (filepaths.length > 0) {
|
|
1459
|
+
showSaved(filepaths);
|
|
1177
1460
|
} else {
|
|
1178
1461
|
p3.log.success("Done. (No filepath printed \u2014 check your output folder.)");
|
|
1179
1462
|
p3.log.info(`Output folder: ${answers.outputDir}`);
|
|
@@ -1202,11 +1485,35 @@ async function runUpdateBinary() {
|
|
|
1202
1485
|
var program = new Command();
|
|
1203
1486
|
program.name("easy-ytdlp").description(
|
|
1204
1487
|
"Interactive, user-friendly wrapper around yt-dlp \u2014 no flag memorization required"
|
|
1205
|
-
).version(
|
|
1488
|
+
).version(CLI_VERSION).argument("[urls...]", "One or more video URLs (prompted if omitted)").option(
|
|
1206
1489
|
"--show-command",
|
|
1207
1490
|
"Always show the generated yt-dlp command before running"
|
|
1208
|
-
).
|
|
1209
|
-
|
|
1491
|
+
).option(
|
|
1492
|
+
"-a, --batch-file <path>",
|
|
1493
|
+
"File containing URLs to download, one per line"
|
|
1494
|
+
).option("-y, --yes", "Run without interactive prompts using defaults/options").option(
|
|
1495
|
+
"--mode <mode>",
|
|
1496
|
+
"Download mode: video, audio, video-only, subs-only, thumbnail-only"
|
|
1497
|
+
).option("--quality <quality>", "Video quality: best, 1080, 720, 480, or height").option("--container <container>", "Container: best, mp4, mkv, webm").option("--audio-format <format>", "Audio format: best, mp3, m4a, opus, flac, wav").option("--audio-quality <quality>", "Audio quality: best, good").option("-o, --output <dir>", "Destination folder").option("--filename <preset>", "Filename preset: title, title-channel, title-date").option("--subs <mode>", "Subtitle mode: none, embed, write, both").option("--sub-langs <langs>", "Comma-separated subtitle languages, for example en,es").option("--playlist <mode>", "Playlist mode: single, all, range").option("--playlist-range <range>", "Playlist range for --playlist range, for example 2:5").option("--embed-thumbnail", "Embed thumbnail as cover art").option("--embed-metadata", "Embed metadata").option("--sponsorblock", "Remove SponsorBlock default segments").action(async (urls, options) => {
|
|
1498
|
+
let allUrls = urls ?? [];
|
|
1499
|
+
if (options.batchFile) {
|
|
1500
|
+
let content;
|
|
1501
|
+
try {
|
|
1502
|
+
content = readFileSync(options.batchFile, "utf-8");
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1505
|
+
p3.log.error(`Could not read batch file "${options.batchFile}": ${detail}`);
|
|
1506
|
+
process.exit(1);
|
|
1507
|
+
}
|
|
1508
|
+
const fileUrls = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
1509
|
+
allUrls = [...allUrls, ...fileUrls];
|
|
1510
|
+
}
|
|
1511
|
+
if (options.yes && allUrls.length === 0) {
|
|
1512
|
+
fail("--yes requires at least one URL or --batch-file.");
|
|
1513
|
+
}
|
|
1514
|
+
await runWizard(allUrls.length > 0 ? allUrls : void 0, {
|
|
1515
|
+
...options
|
|
1516
|
+
});
|
|
1210
1517
|
});
|
|
1211
1518
|
program.command("update-binary").description("Force-refresh the cached yt-dlp binary").action(async () => {
|
|
1212
1519
|
await runUpdateBinary();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jnzlab/easy-ytdlp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"bin",
|
|
16
16
|
"dist",
|
|
17
17
|
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
@@ -36,6 +37,14 @@
|
|
|
36
37
|
"video"
|
|
37
38
|
],
|
|
38
39
|
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/jnzlab/easy-ytdlp.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/jnzlab/easy-ytdlp#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/jnzlab/easy-ytdlp/issues"
|
|
47
|
+
},
|
|
39
48
|
"dependencies": {
|
|
40
49
|
"@clack/prompts": "^0.10.0",
|
|
41
50
|
"cli-progress": "^3.12.0",
|