ytdwn 1.0.1 → 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/README.md +121 -81
- package/dist/index.js +459 -363
- package/dist/src/cli/commands.d.ts +18 -0
- package/dist/src/cli/errors.d.ts +8 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/config.d.ts +6 -3
- package/dist/src/layers/AppLive.d.ts +14 -0
- package/dist/src/layers/index.d.ts +1 -0
- package/dist/src/lib/errors.d.ts +92 -0
- package/dist/src/lib/filesystem.d.ts +27 -0
- package/dist/src/lib/http.d.ts +6 -0
- package/dist/src/lib/index.d.ts +3 -0
- package/dist/src/services/BinaryService.d.ts +14 -0
- package/dist/src/services/DownloadService.d.ts +22 -0
- package/dist/src/services/SettingsService.d.ts +14 -0
- package/dist/src/services/index.d.ts +3 -0
- package/dist/src/timestamp.d.ts +16 -0
- package/package.json +6 -3
- package/dist/src/binary.d.ts +0 -12
- package/dist/src/download.d.ts +0 -16
- package/dist/src/settings.d.ts +0 -24
package/dist/index.js
CHANGED
|
@@ -1,22 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
-
for (let key of __getOwnPropNames(mod))
|
|
12
|
-
if (!__hasOwnProp.call(to, key))
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
2
|
|
|
21
3
|
// index.ts
|
|
22
4
|
import { program } from "commander";
|
|
@@ -24,59 +6,158 @@ import { program } from "commander";
|
|
|
24
6
|
// src/config.ts
|
|
25
7
|
import { join } from "path";
|
|
26
8
|
var APP_NAME = "ytdwn";
|
|
27
|
-
var APP_TAGLINE = "YouTube to MP3 • Fast & Simple";
|
|
28
|
-
var APP_VERSION = "1.
|
|
29
|
-
var
|
|
9
|
+
var APP_TAGLINE = "YouTube to MP3/MP4 • Fast & Simple";
|
|
10
|
+
var APP_VERSION = "1.1.0";
|
|
11
|
+
var DEFAULT_AUDIO_FORMAT = "mp3";
|
|
30
12
|
var DEFAULT_AUDIO_QUALITY = "0";
|
|
31
13
|
var CONCURRENT_FRAGMENTS = "8";
|
|
14
|
+
var DEFAULT_FORMAT = DEFAULT_AUDIO_FORMAT;
|
|
32
15
|
var BIN_DIR = join(process.cwd(), "bin");
|
|
33
16
|
var getOutputTemplate = (downloadDir) => join(downloadDir, "%(title)s.%(ext)s");
|
|
34
17
|
|
|
35
|
-
// src/
|
|
36
|
-
import {
|
|
37
|
-
import { access, chmod, mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
38
|
-
import { platform } from "os";
|
|
39
|
-
import { join as join3, dirname } from "path";
|
|
18
|
+
// src/layers/AppLive.ts
|
|
19
|
+
import { Layer as Layer4 } from "effect";
|
|
40
20
|
|
|
41
|
-
// src/
|
|
42
|
-
import {
|
|
21
|
+
// src/services/SettingsService.ts
|
|
22
|
+
import { Effect as Effect2, Context, Layer } from "effect";
|
|
43
23
|
import { homedir } from "os";
|
|
44
24
|
import { join as join2, resolve } from "path";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
25
|
+
|
|
26
|
+
// src/lib/filesystem.ts
|
|
27
|
+
import { Effect } from "effect";
|
|
28
|
+
import {
|
|
29
|
+
access,
|
|
30
|
+
chmod,
|
|
31
|
+
mkdir,
|
|
32
|
+
readFile,
|
|
33
|
+
writeFile,
|
|
34
|
+
constants as fsConstants
|
|
35
|
+
} from "fs/promises";
|
|
36
|
+
|
|
37
|
+
// src/lib/errors.ts
|
|
38
|
+
import { Data } from "effect";
|
|
39
|
+
|
|
40
|
+
class FileWriteError extends Data.TaggedError("FileWriteError") {
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class DirectoryCreateError extends Data.TaggedError("DirectoryCreateError") {
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class NetworkError extends Data.TaggedError("NetworkError") {
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class DownloadError extends Data.TaggedError("DownloadError") {
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const { downloadDir } = await load();
|
|
56
|
-
return downloadDir ?? process.cwd();
|
|
51
|
+
|
|
52
|
+
class BinaryNotFoundError extends Data.TaggedError("BinaryNotFoundError") {
|
|
57
53
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await save({ ...settings, downloadDir: resolve(dir) });
|
|
54
|
+
|
|
55
|
+
class BinaryDownloadError extends Data.TaggedError("BinaryDownloadError") {
|
|
61
56
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await save(rest);
|
|
57
|
+
|
|
58
|
+
class BinaryExecutionError extends Data.TaggedError("BinaryExecutionError") {
|
|
65
59
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
|
|
61
|
+
class TimestampParseError extends Data.TaggedError("TimestampParseError") {
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class VideoNotFoundError extends Data.TaggedError("VideoNotFoundError") {
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class InvalidUrlError extends Data.TaggedError("InvalidUrlError") {
|
|
69
68
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await save({ ...settings, binaryPath: path });
|
|
69
|
+
|
|
70
|
+
class AgeRestrictedError extends Data.TaggedError("AgeRestrictedError") {
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
await save(rest);
|
|
72
|
+
|
|
73
|
+
class ConnectionError extends Data.TaggedError("ConnectionError") {
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
// src/
|
|
76
|
+
// src/lib/filesystem.ts
|
|
77
|
+
var isExecutable = (path) => Effect.tryPromise({
|
|
78
|
+
try: async () => {
|
|
79
|
+
await access(path, fsConstants.X_OK);
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
82
|
+
catch: () => false
|
|
83
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
84
|
+
var readJsonFileOrDefault = (path, defaultValue) => Effect.tryPromise({
|
|
85
|
+
try: async () => JSON.parse(await readFile(path, "utf-8")),
|
|
86
|
+
catch: () => defaultValue
|
|
87
|
+
}).pipe(Effect.orElseSucceed(() => defaultValue));
|
|
88
|
+
var writeJsonFile = (path, data) => Effect.tryPromise({
|
|
89
|
+
try: () => writeFile(path, JSON.stringify(data, null, 2), "utf-8"),
|
|
90
|
+
catch: (cause) => new FileWriteError({ path, cause })
|
|
91
|
+
});
|
|
92
|
+
var writeFileBinary = (path, data) => Effect.tryPromise({
|
|
93
|
+
try: () => writeFile(path, data),
|
|
94
|
+
catch: (cause) => new FileWriteError({ path, cause })
|
|
95
|
+
});
|
|
96
|
+
var ensureDirectory = (path) => Effect.tryPromise({
|
|
97
|
+
try: () => mkdir(path, { recursive: true }),
|
|
98
|
+
catch: (cause) => new DirectoryCreateError({ path, cause })
|
|
99
|
+
}).pipe(Effect.as(path));
|
|
100
|
+
var makeExecutable = (path) => Effect.tryPromise({
|
|
101
|
+
try: () => chmod(path, 493),
|
|
102
|
+
catch: (cause) => new FileWriteError({ path, cause })
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// src/services/SettingsService.ts
|
|
106
|
+
var SETTINGS_PATH = join2(homedir(), ".ytdwn.json");
|
|
107
|
+
|
|
108
|
+
class SettingsService extends Context.Tag("SettingsService")() {
|
|
109
|
+
}
|
|
110
|
+
var loadSettings = () => readJsonFileOrDefault(SETTINGS_PATH, {});
|
|
111
|
+
var saveSettings = (settings) => writeJsonFile(SETTINGS_PATH, settings);
|
|
112
|
+
var SettingsServiceLive = Layer.succeed(SettingsService, {
|
|
113
|
+
getDownloadDir: Effect2.gen(function* () {
|
|
114
|
+
const { downloadDir } = yield* loadSettings();
|
|
115
|
+
return downloadDir ?? process.cwd();
|
|
116
|
+
}),
|
|
117
|
+
setDownloadDir: (dir) => Effect2.gen(function* () {
|
|
118
|
+
const settings = yield* loadSettings();
|
|
119
|
+
yield* saveSettings({ ...settings, downloadDir: resolve(dir) });
|
|
120
|
+
}),
|
|
121
|
+
resetDownloadDir: Effect2.gen(function* () {
|
|
122
|
+
const settings = yield* loadSettings();
|
|
123
|
+
const { downloadDir: _, ...rest } = settings;
|
|
124
|
+
yield* saveSettings(rest);
|
|
125
|
+
}),
|
|
126
|
+
getCachedBinaryPath: Effect2.gen(function* () {
|
|
127
|
+
const { binaryPath } = yield* loadSettings();
|
|
128
|
+
return binaryPath ?? null;
|
|
129
|
+
}),
|
|
130
|
+
setCachedBinaryPath: (path) => Effect2.gen(function* () {
|
|
131
|
+
const settings = yield* loadSettings();
|
|
132
|
+
yield* saveSettings({ ...settings, binaryPath: path });
|
|
133
|
+
}),
|
|
134
|
+
clearCachedBinaryPath: Effect2.gen(function* () {
|
|
135
|
+
const settings = yield* loadSettings();
|
|
136
|
+
const { binaryPath: _, ...rest } = settings;
|
|
137
|
+
yield* saveSettings(rest);
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// src/services/BinaryService.ts
|
|
142
|
+
import { Effect as Effect4, Context as Context2, Layer as Layer2 } from "effect";
|
|
143
|
+
import YTDlpWrap from "yt-dlp-wrap";
|
|
144
|
+
import { platform } from "os";
|
|
145
|
+
import { join as join3 } from "path";
|
|
146
|
+
|
|
147
|
+
// src/lib/http.ts
|
|
148
|
+
import { Effect as Effect3, Schedule } from "effect";
|
|
149
|
+
var fetchBinaryWithRetry = (url, maxRetries = 2) => Effect3.tryPromise({
|
|
150
|
+
try: async () => {
|
|
151
|
+
const response = await fetch(url);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
154
|
+
}
|
|
155
|
+
return Buffer.from(await response.arrayBuffer());
|
|
156
|
+
},
|
|
157
|
+
catch: (cause) => new DownloadError({ url, cause })
|
|
158
|
+
}).pipe(Effect3.retry(Schedule.recurs(maxRetries).pipe(Schedule.addDelay(() => "1 second"))));
|
|
159
|
+
|
|
160
|
+
// src/services/BinaryService.ts
|
|
80
161
|
var BINARY_NAMES = {
|
|
81
162
|
win32: ["yt-dlp.exe"],
|
|
82
163
|
darwin: {
|
|
@@ -89,7 +170,7 @@ var BINARY_NAMES = {
|
|
|
89
170
|
}
|
|
90
171
|
};
|
|
91
172
|
var FALLBACK_BINARY = "yt-dlp";
|
|
92
|
-
|
|
173
|
+
var getCandidateNames = () => {
|
|
93
174
|
const platformNames = BINARY_NAMES[process.platform];
|
|
94
175
|
if (!platformNames)
|
|
95
176
|
return [FALLBACK_BINARY];
|
|
@@ -97,96 +178,106 @@ function getCandidateNames() {
|
|
|
97
178
|
return platformNames;
|
|
98
179
|
const archNames = platformNames[process.arch];
|
|
99
180
|
return archNames ?? [FALLBACK_BINARY];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
async function isExecutable(filePath) {
|
|
105
|
-
try {
|
|
106
|
-
await access(filePath, fsConstants.X_OK);
|
|
107
|
-
return true;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
async function downloadFile(url, targetPath) {
|
|
113
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
114
|
-
const response = await fetch(url);
|
|
115
|
-
if (!response.ok) {
|
|
116
|
-
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
117
|
-
}
|
|
118
|
-
await writeFile2(targetPath, Buffer.from(await response.arrayBuffer()));
|
|
119
|
-
if (platform() !== "win32") {
|
|
120
|
-
await chmod(targetPath, 493);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function pickAsset(release) {
|
|
181
|
+
};
|
|
182
|
+
var getCandidatePaths = () => getCandidateNames().map((name) => join3(BIN_DIR, name));
|
|
183
|
+
var pickAsset = (release) => {
|
|
124
184
|
const assets = release.assets ?? [];
|
|
125
185
|
const candidates = getCandidateNames();
|
|
126
186
|
const match = assets.find((a) => candidates.includes(a.name)) ?? assets.find((a) => a.name === FALLBACK_BINARY);
|
|
127
187
|
if (!match) {
|
|
128
|
-
|
|
188
|
+
return Effect4.fail(new BinaryDownloadError({
|
|
189
|
+
platform: `${process.platform}-${process.arch}`,
|
|
190
|
+
cause: "No suitable binary found for this platform"
|
|
191
|
+
}));
|
|
129
192
|
}
|
|
130
|
-
return match;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
193
|
+
return Effect4.succeed(match);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
class BinaryService extends Context2.Tag("BinaryService")() {
|
|
197
|
+
}
|
|
198
|
+
var BinaryServiceLive = Layer2.effect(BinaryService, Effect4.gen(function* () {
|
|
199
|
+
const settings = yield* SettingsService;
|
|
200
|
+
const searchForBinary = Effect4.gen(function* () {
|
|
201
|
+
for (const candidate of getCandidatePaths()) {
|
|
202
|
+
const executable = yield* isExecutable(candidate);
|
|
203
|
+
if (executable) {
|
|
204
|
+
yield* settings.setCachedBinaryPath(candidate).pipe(Effect4.ignore);
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
141
207
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return existing;
|
|
151
|
-
throw new Error("yt-dlp binary not found. Run 'ytdwn prepare' first.");
|
|
152
|
-
}
|
|
153
|
-
async function downloadLatestBinary() {
|
|
154
|
-
let release = null;
|
|
155
|
-
try {
|
|
156
|
-
const ytDlpWrapModule = await import("yt-dlp-wrap");
|
|
157
|
-
const YTDlpWrap = ytDlpWrapModule.default;
|
|
158
|
-
if (typeof YTDlpWrap?.getGithubReleases === "function") {
|
|
159
|
-
const releases = await YTDlpWrap.getGithubReleases(1, 1);
|
|
160
|
-
release = Array.isArray(releases) ? releases[0] : releases;
|
|
208
|
+
return null;
|
|
209
|
+
});
|
|
210
|
+
const findBinary = Effect4.gen(function* () {
|
|
211
|
+
const cached = yield* settings.getCachedBinaryPath;
|
|
212
|
+
if (cached) {
|
|
213
|
+
const executable = yield* isExecutable(cached);
|
|
214
|
+
if (executable)
|
|
215
|
+
return cached;
|
|
161
216
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
217
|
+
const found = yield* searchForBinary;
|
|
218
|
+
if (found)
|
|
219
|
+
return found;
|
|
220
|
+
if (cached) {
|
|
221
|
+
yield* settings.clearCachedBinaryPath.pipe(Effect4.ignore);
|
|
167
222
|
}
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
223
|
+
return null;
|
|
224
|
+
});
|
|
225
|
+
const requireBinary = Effect4.gen(function* () {
|
|
226
|
+
const existing = yield* findBinary;
|
|
227
|
+
if (existing)
|
|
228
|
+
return existing;
|
|
229
|
+
return yield* Effect4.fail(new BinaryNotFoundError({
|
|
230
|
+
message: "yt-dlp binary not found. Run 'ytdwn prepare' first."
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
233
|
+
const getYtDlpWrap = Effect4.gen(function* () {
|
|
234
|
+
const binaryPath = yield* requireBinary;
|
|
235
|
+
return new YTDlpWrap(binaryPath);
|
|
236
|
+
});
|
|
237
|
+
const downloadLatestBinary = Effect4.gen(function* () {
|
|
238
|
+
const releases = yield* Effect4.tryPromise({
|
|
239
|
+
try: () => YTDlpWrap.getGithubReleases(1, 1),
|
|
240
|
+
catch: (cause) => new BinaryDownloadError({
|
|
241
|
+
platform: `${process.platform}-${process.arch}`,
|
|
242
|
+
cause
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
const release = Array.isArray(releases) ? releases[0] : releases;
|
|
246
|
+
if (!release) {
|
|
247
|
+
return yield* Effect4.fail(new BinaryDownloadError({
|
|
248
|
+
platform: `${process.platform}-${process.arch}`,
|
|
249
|
+
cause: "No releases found"
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
const asset = yield* pickAsset(release);
|
|
253
|
+
const binaryPath = join3(BIN_DIR, asset.name);
|
|
254
|
+
const exists = yield* isExecutable(binaryPath);
|
|
255
|
+
if (exists) {
|
|
256
|
+
yield* settings.setCachedBinaryPath(binaryPath).pipe(Effect4.ignore);
|
|
257
|
+
return binaryPath;
|
|
258
|
+
}
|
|
259
|
+
yield* ensureDirectory(BIN_DIR);
|
|
260
|
+
const data = yield* fetchBinaryWithRetry(asset.browser_download_url);
|
|
261
|
+
yield* writeFileBinary(binaryPath, data);
|
|
262
|
+
if (platform() !== "win32") {
|
|
263
|
+
yield* makeExecutable(binaryPath);
|
|
264
|
+
}
|
|
265
|
+
yield* settings.setCachedBinaryPath(binaryPath).pipe(Effect4.ignore);
|
|
177
266
|
return binaryPath;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
findBinary,
|
|
270
|
+
requireBinary,
|
|
271
|
+
getYtDlpWrap,
|
|
272
|
+
downloadLatestBinary
|
|
273
|
+
};
|
|
274
|
+
}));
|
|
183
275
|
|
|
184
|
-
// src/
|
|
185
|
-
import {
|
|
186
|
-
import { spawn } from "child_process";
|
|
187
|
-
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
|
|
276
|
+
// src/services/DownloadService.ts
|
|
277
|
+
import { Effect as Effect6, Context as Context3, Layer as Layer3 } from "effect";
|
|
188
278
|
|
|
189
279
|
// src/timestamp.ts
|
|
280
|
+
import { Effect as Effect5 } from "effect";
|
|
190
281
|
var TIME_FORMAT_ERROR = "Invalid time format. Use MM:SS or HH:MM:SS (e.g. 0:02 or 01:23:45).";
|
|
191
282
|
var RANGE_FORMAT_ERROR = "Range must be in 'start-end' format (e.g. 0:02-23:10).";
|
|
192
283
|
var pad = (value) => value.padStart(2, "0");
|
|
@@ -238,7 +329,7 @@ var c = {
|
|
|
238
329
|
}
|
|
239
330
|
};
|
|
240
331
|
|
|
241
|
-
// src/
|
|
332
|
+
// src/services/DownloadService.ts
|
|
242
333
|
var SPINNER_FRAMES = [
|
|
243
334
|
"⠋",
|
|
244
335
|
"⠙",
|
|
@@ -254,20 +345,6 @@ var SPINNER_FRAMES = [
|
|
|
254
345
|
var SPINNER_INTERVAL_MS = 80;
|
|
255
346
|
var PROGRESS_BAR_WIDTH = 12;
|
|
256
347
|
var LINE_CLEAR_WIDTH = 60;
|
|
257
|
-
var REGEX = {
|
|
258
|
-
percent: /(\d+\.?\d*)%/,
|
|
259
|
-
total: /of\s+~?([\d.]+\s*\w+)/i,
|
|
260
|
-
speed: /at\s+([\d.]+\s*\w+\/s)/i,
|
|
261
|
-
size: /([\d.]+)\s*(\w+)/,
|
|
262
|
-
binaryUnits: /([KMG])iB/g,
|
|
263
|
-
destination: /\[(?:ExtractAudio|Merger)\].*?Destination:\s*(.+)$/m,
|
|
264
|
-
downloadDest: /\[download\]\s+Destination:\s*(.+)$/m,
|
|
265
|
-
fileSize: /~?([\d.]+\s*[KMG]i?B)/i
|
|
266
|
-
};
|
|
267
|
-
var PHASE_MESSAGES = new Map([
|
|
268
|
-
["Extracting URL", "Extracting..."],
|
|
269
|
-
["Downloading webpage", "Fetching info..."]
|
|
270
|
-
]);
|
|
271
348
|
var write = (text) => process.stdout.write(text);
|
|
272
349
|
var clearLine = () => write(`\r${" ".repeat(LINE_CLEAR_WIDTH)}\r`);
|
|
273
350
|
function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
@@ -275,10 +352,7 @@ function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
|
275
352
|
let message = initialMessage;
|
|
276
353
|
let stopped = false;
|
|
277
354
|
if (quiet) {
|
|
278
|
-
return {
|
|
279
|
-
update: () => {},
|
|
280
|
-
stop: () => {}
|
|
281
|
-
};
|
|
355
|
+
return { update: () => {}, stop: () => {} };
|
|
282
356
|
}
|
|
283
357
|
const interval = setInterval(() => {
|
|
284
358
|
if (!stopped) {
|
|
@@ -299,7 +373,7 @@ function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
|
299
373
|
}
|
|
300
374
|
};
|
|
301
375
|
}
|
|
302
|
-
function renderProgress(
|
|
376
|
+
function renderProgress(percent, speed, quiet = false) {
|
|
303
377
|
if (quiet)
|
|
304
378
|
return;
|
|
305
379
|
const filled = Math.round(percent / 100 * PROGRESS_BAR_WIDTH);
|
|
@@ -307,58 +381,45 @@ function renderProgress({ percent, speed }, quiet = false) {
|
|
|
307
381
|
const speedText = speed ? ` ${c.speed(speed)}` : "";
|
|
308
382
|
write(`\r${c.bold("Downloading:")} ${bar} ${c.bar.percent(`${percent.toFixed(0)}%`)}${speedText} `);
|
|
309
383
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const [, size, unit] = total.match(REGEX.size) ?? [];
|
|
321
|
-
if (size && unit) {
|
|
322
|
-
downloaded = normalizeUnit(`${(percent / 100 * parseFloat(size)).toFixed(1)}${unit}`);
|
|
323
|
-
}
|
|
384
|
+
function mapOutputToError(output, url) {
|
|
385
|
+
const ageRestrictedPatterns = [
|
|
386
|
+
/age[- ]?restrict/i,
|
|
387
|
+
/age[- ]?gate/i,
|
|
388
|
+
/sign in to confirm your age/i,
|
|
389
|
+
/this video is age-restricted/i,
|
|
390
|
+
/confirm your age/i
|
|
391
|
+
];
|
|
392
|
+
if (ageRestrictedPatterns.some((p) => p.test(output))) {
|
|
393
|
+
return new AgeRestrictedError({ url });
|
|
324
394
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
downloaded,
|
|
328
|
-
total: total ? normalizeUnit(total) : undefined,
|
|
329
|
-
speed: speed ? normalizeUnit(speed) : undefined
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function detectPhase(text) {
|
|
333
|
-
for (const [pattern, message] of PHASE_MESSAGES) {
|
|
334
|
-
if (text.includes(pattern))
|
|
335
|
-
return message;
|
|
395
|
+
if (/\b(network|connection)\s+(error|failed|refused)/i.test(output)) {
|
|
396
|
+
return new ConnectionError({ message: "Connection error, try again" });
|
|
336
397
|
}
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
var isConvertingPhase = (text) => text.includes("ExtractAudio") || text.includes("Converting");
|
|
340
|
-
function extractFileName(text) {
|
|
341
|
-
const match = text.match(REGEX.destination) || text.match(REGEX.downloadDest);
|
|
342
|
-
if (match?.[1]) {
|
|
343
|
-
const fullPath = match[1].trim();
|
|
344
|
-
return fullPath.split("/").pop() || null;
|
|
398
|
+
if (/video unavailable/i.test(output) || /private video/i.test(output)) {
|
|
399
|
+
return new VideoNotFoundError({ url });
|
|
345
400
|
}
|
|
346
401
|
return null;
|
|
347
402
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
403
|
+
function mapExitCodeToError(code, url) {
|
|
404
|
+
switch (code) {
|
|
405
|
+
case 1:
|
|
406
|
+
return new VideoNotFoundError({ url });
|
|
407
|
+
case 2:
|
|
408
|
+
return new InvalidUrlError({ url });
|
|
409
|
+
default:
|
|
410
|
+
return new BinaryExecutionError({
|
|
411
|
+
exitCode: code,
|
|
412
|
+
message: `Download failed with exit code ${code}`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
351
415
|
}
|
|
416
|
+
var VIDEO_FORMATS = ["mp4", "mkv", "webm", "avi", "mov"];
|
|
417
|
+
var isVideoFormat = (format) => VIDEO_FORMATS.includes(format.toLowerCase());
|
|
352
418
|
function buildArgs(url, options, downloadDir) {
|
|
419
|
+
const format = options.format.toLowerCase();
|
|
420
|
+
const isVideo = isVideoFormat(format);
|
|
353
421
|
const baseArgs = [
|
|
354
422
|
url,
|
|
355
|
-
"-f",
|
|
356
|
-
"bestaudio/best",
|
|
357
|
-
"-x",
|
|
358
|
-
"--audio-format",
|
|
359
|
-
options.format,
|
|
360
|
-
"--audio-quality",
|
|
361
|
-
DEFAULT_AUDIO_QUALITY,
|
|
362
423
|
"-o",
|
|
363
424
|
getOutputTemplate(downloadDir),
|
|
364
425
|
"--no-playlist",
|
|
@@ -366,68 +427,39 @@ function buildArgs(url, options, downloadDir) {
|
|
|
366
427
|
"--progress",
|
|
367
428
|
"--concurrent-fragments",
|
|
368
429
|
CONCURRENT_FRAGMENTS,
|
|
369
|
-
"--no-check-certificates"
|
|
430
|
+
"--no-check-certificates"
|
|
431
|
+
];
|
|
432
|
+
const formatArgs = isVideo ? [
|
|
433
|
+
"-f",
|
|
434
|
+
`bestvideo[ext=${format}]+bestaudio/best[ext=${format}]/best`,
|
|
435
|
+
"--merge-output-format",
|
|
436
|
+
format
|
|
437
|
+
] : [
|
|
438
|
+
"-f",
|
|
439
|
+
"bestaudio/best",
|
|
440
|
+
"-x",
|
|
441
|
+
"--audio-format",
|
|
442
|
+
format,
|
|
443
|
+
"--audio-quality",
|
|
444
|
+
DEFAULT_AUDIO_QUALITY,
|
|
370
445
|
"--prefer-free-formats"
|
|
371
446
|
];
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
conditionalArgs.push("--download-sections", `*${parseClipRange(options.clip)}`);
|
|
375
|
-
}
|
|
376
|
-
if (ffmpegInstaller?.path) {
|
|
377
|
-
conditionalArgs.push("--ffmpeg-location", ffmpegInstaller.path);
|
|
378
|
-
}
|
|
379
|
-
return [...baseArgs, ...conditionalArgs];
|
|
447
|
+
const clipArgs = options.clip ? ["--download-sections", `*${parseClipRange(options.clip)}`] : [];
|
|
448
|
+
return [...baseArgs, ...formatArgs, ...clipArgs];
|
|
380
449
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const text = data.toString();
|
|
384
|
-
const fileName = extractFileName(text);
|
|
385
|
-
if (fileName)
|
|
386
|
-
state.fileName = fileName;
|
|
387
|
-
const fileSize = extractFileSize(text);
|
|
388
|
-
if (fileSize)
|
|
389
|
-
state.fileSize = fileSize;
|
|
390
|
-
if (state.phase !== "converting" && isConvertingPhase(text)) {
|
|
391
|
-
state.phase = "converting";
|
|
392
|
-
spinner.stop();
|
|
393
|
-
clearLine();
|
|
394
|
-
if (!state.quiet) {
|
|
395
|
-
let frame = 0;
|
|
396
|
-
state.convertingInterval = setInterval(() => {
|
|
397
|
-
const frameChar = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "⠋";
|
|
398
|
-
write(`\r${c.info(frameChar)} ${c.dim("Converting...")}`);
|
|
399
|
-
}, SPINNER_INTERVAL_MS);
|
|
400
|
-
}
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const progress = parseProgress(text);
|
|
404
|
-
if (progress && progress.percent > state.lastPercent) {
|
|
405
|
-
if (state.phase === "init") {
|
|
406
|
-
state.phase = "downloading";
|
|
407
|
-
spinner.stop();
|
|
408
|
-
}
|
|
409
|
-
state.lastPercent = progress.percent;
|
|
410
|
-
renderProgress(progress, state.quiet);
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (state.phase === "init") {
|
|
414
|
-
const message = detectPhase(text);
|
|
415
|
-
if (message)
|
|
416
|
-
spinner.update(message);
|
|
417
|
-
}
|
|
418
|
-
};
|
|
450
|
+
|
|
451
|
+
class DownloadService extends Context3.Tag("DownloadService")() {
|
|
419
452
|
}
|
|
420
|
-
function
|
|
421
|
-
return
|
|
453
|
+
function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
|
|
454
|
+
return Effect6.async((resume) => {
|
|
455
|
+
const spinner = createSpinner("Getting ready...", quiet);
|
|
422
456
|
const state = {
|
|
423
457
|
phase: "init",
|
|
424
|
-
lastPercent: 0,
|
|
425
458
|
fileName: null,
|
|
426
459
|
fileSize: null,
|
|
427
|
-
|
|
460
|
+
outputBuffer: "",
|
|
428
461
|
convertingInterval: null
|
|
429
462
|
};
|
|
430
|
-
const handleOutput = createOutputHandler(spinner, state);
|
|
431
463
|
const cleanup = () => {
|
|
432
464
|
spinner.stop();
|
|
433
465
|
if (state.convertingInterval) {
|
|
@@ -436,40 +468,118 @@ function attachProcessHandlers(child, spinner, downloadDir, quiet) {
|
|
|
436
468
|
if (!quiet)
|
|
437
469
|
clearLine();
|
|
438
470
|
};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
fileSize: state.fileSize || undefined
|
|
448
|
-
});
|
|
449
|
-
} else {
|
|
450
|
-
reject(new Error(`Download failed (code ${code})`));
|
|
471
|
+
const emitter = ytDlpWrap.exec(args);
|
|
472
|
+
emitter.on("progress", (progress) => {
|
|
473
|
+
if (state.phase === "init") {
|
|
474
|
+
state.phase = "downloading";
|
|
475
|
+
spinner.stop();
|
|
476
|
+
}
|
|
477
|
+
if (progress.percent !== undefined) {
|
|
478
|
+
renderProgress(progress.percent, progress.currentSpeed, quiet);
|
|
451
479
|
}
|
|
452
480
|
});
|
|
453
|
-
|
|
481
|
+
emitter.on("ytDlpEvent", (eventType, eventData) => {
|
|
482
|
+
state.outputBuffer += `[${eventType}] ${eventData}
|
|
483
|
+
`;
|
|
484
|
+
if (eventType === "ExtractAudio" || eventType === "Merger") {
|
|
485
|
+
const match = eventData.match(/Destination:\s*(.+)/);
|
|
486
|
+
if (match?.[1]) {
|
|
487
|
+
state.fileName = match[1].split("/").pop() || null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (eventType === "download") {
|
|
491
|
+
const destMatch = eventData.match(/Destination:\s*(.+)/);
|
|
492
|
+
if (destMatch?.[1]) {
|
|
493
|
+
state.fileName = destMatch[1].split("/").pop() || null;
|
|
494
|
+
}
|
|
495
|
+
const sizeMatch = eventData.match(/~?([\d.]+\s*[KMG]i?B)/i);
|
|
496
|
+
if (sizeMatch?.[1]) {
|
|
497
|
+
state.fileSize = sizeMatch[1];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if ((eventType === "ExtractAudio" || eventType === "ffmpeg") && state.phase !== "converting") {
|
|
501
|
+
state.phase = "converting";
|
|
502
|
+
spinner.stop();
|
|
503
|
+
clearLine();
|
|
504
|
+
if (!quiet) {
|
|
505
|
+
let frame = 0;
|
|
506
|
+
state.convertingInterval = setInterval(() => {
|
|
507
|
+
const frameChar = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "⠋";
|
|
508
|
+
write(`\r${c.info(frameChar)} ${c.dim("Converting...")}`);
|
|
509
|
+
}, SPINNER_INTERVAL_MS);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (state.phase === "init") {
|
|
513
|
+
if (eventType === "youtube" && eventData.includes("Extracting")) {
|
|
514
|
+
spinner.update("Extracting...");
|
|
515
|
+
} else if (eventType === "youtube" && eventData.includes("Downloading webpage")) {
|
|
516
|
+
spinner.update("Fetching info...");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
emitter.on("error", (error) => {
|
|
521
|
+
cleanup();
|
|
522
|
+
const outputError = mapOutputToError(String(error), url);
|
|
523
|
+
resume(Effect6.fail(outputError ?? new BinaryExecutionError({
|
|
524
|
+
exitCode: -1,
|
|
525
|
+
message: String(error)
|
|
526
|
+
})));
|
|
527
|
+
});
|
|
528
|
+
emitter.on("close", () => {
|
|
454
529
|
cleanup();
|
|
455
|
-
|
|
530
|
+
const outputError = mapOutputToError(state.outputBuffer, url);
|
|
531
|
+
if (outputError) {
|
|
532
|
+
resume(Effect6.fail(outputError));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const exitCode = emitter.ytDlpProcess?.exitCode;
|
|
536
|
+
if (exitCode !== null && exitCode !== undefined && exitCode !== 0) {
|
|
537
|
+
resume(Effect6.fail(mapExitCodeToError(exitCode, url)));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
resume(Effect6.succeed({
|
|
541
|
+
filePath: downloadDir,
|
|
542
|
+
fileName: state.fileName || "audio",
|
|
543
|
+
fileSize: state.fileSize || undefined
|
|
544
|
+
}));
|
|
456
545
|
});
|
|
457
546
|
});
|
|
458
547
|
}
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
548
|
+
var DownloadServiceLive = Layer3.effect(DownloadService, Effect6.gen(function* () {
|
|
549
|
+
const binary = yield* BinaryService;
|
|
550
|
+
const settings = yield* SettingsService;
|
|
551
|
+
return {
|
|
552
|
+
download: (url, options) => Effect6.gen(function* () {
|
|
553
|
+
const [downloadDir, ytDlpWrap] = yield* Effect6.all([
|
|
554
|
+
settings.getDownloadDir.pipe(Effect6.flatMap((dir) => ensureDirectory(dir))),
|
|
555
|
+
binary.getYtDlpWrap
|
|
556
|
+
]);
|
|
557
|
+
const quiet = options.quiet ?? false;
|
|
558
|
+
const args = buildArgs(url, options, downloadDir);
|
|
559
|
+
return yield* executeDownload(ytDlpWrap, args, downloadDir, url, quiet);
|
|
560
|
+
})
|
|
561
|
+
};
|
|
562
|
+
}));
|
|
472
563
|
|
|
564
|
+
// src/layers/AppLive.ts
|
|
565
|
+
var AppLive = DownloadServiceLive.pipe(Layer4.provide(BinaryServiceLive), Layer4.provide(SettingsServiceLive));
|
|
566
|
+
var SettingsLive = SettingsServiceLive;
|
|
567
|
+
var BinaryLive = BinaryServiceLive.pipe(Layer4.provide(SettingsServiceLive));
|
|
568
|
+
// src/cli/errors.ts
|
|
569
|
+
import { Effect as Effect7, Match, Console, pipe } from "effect";
|
|
570
|
+
var formatAppError = (error) => Match.value(error).pipe(Match.tag("BinaryNotFoundError", (e) => e.message), Match.tag("VideoNotFoundError", () => "Video not found or private"), Match.tag("InvalidUrlError", () => "Invalid URL format"), Match.tag("AgeRestrictedError", () => "Age-restricted video (login required)"), Match.tag("ConnectionError", (e) => e.message), Match.tag("BinaryExecutionError", (e) => e.message), Match.tag("BinaryDownloadError", () => "Failed to download yt-dlp binary"), Match.tag("NetworkError", (e) => e.message), Match.tag("FileWriteError", () => "Failed to write file"), Match.tag("DirectoryCreateError", () => "Failed to create directory"), Match.exhaustive);
|
|
571
|
+
var isAppError = (error) => error !== null && typeof error === "object" && ("_tag" in error) && typeof error._tag === "string";
|
|
572
|
+
var formatError = (error) => {
|
|
573
|
+
if (isAppError(error))
|
|
574
|
+
return formatAppError(error);
|
|
575
|
+
if (error instanceof Error)
|
|
576
|
+
return error.message;
|
|
577
|
+
return String(error);
|
|
578
|
+
};
|
|
579
|
+
var withErrorHandler = (effect) => pipe(effect, Effect7.catchAll((error) => pipe(Console.error(`${c.sym.error} ${c.error("Error:")} ${formatError(error)}`), Effect7.flatMap(() => Effect7.sync(() => process.exit(1))))));
|
|
580
|
+
var runCommand = (effect, layer) => Effect7.runPromise(pipe(effect, Effect7.provide(layer), withErrorHandler));
|
|
581
|
+
// src/cli/commands.ts
|
|
582
|
+
import { Effect as Effect8 } from "effect";
|
|
473
583
|
// src/banner.ts
|
|
474
584
|
import cfonts from "cfonts";
|
|
475
585
|
import gradient from "gradient-string";
|
|
@@ -495,100 +605,86 @@ function showBanner() {
|
|
|
495
605
|
`));
|
|
496
606
|
}
|
|
497
607
|
|
|
498
|
-
//
|
|
499
|
-
var
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
608
|
+
// src/cli/commands.ts
|
|
609
|
+
var log = (message) => Effect8.sync(() => console.log(message));
|
|
610
|
+
var logSuccess = (message) => log(`${c.sym.success} ${c.success(message)}`);
|
|
611
|
+
var logInfo = (message) => log(c.info(message));
|
|
612
|
+
var logDim = (message) => log(c.dim(message));
|
|
613
|
+
var prepareCommand = Effect8.gen(function* () {
|
|
614
|
+
const binary = yield* BinaryService;
|
|
615
|
+
const existing = yield* binary.findBinary;
|
|
616
|
+
if (existing) {
|
|
617
|
+
yield* logSuccess("Ready");
|
|
618
|
+
return;
|
|
506
619
|
}
|
|
507
|
-
|
|
508
|
-
|
|
620
|
+
yield* logDim("Downloading yt-dlp...");
|
|
621
|
+
yield* binary.downloadLatestBinary;
|
|
622
|
+
yield* logSuccess("Ready");
|
|
623
|
+
});
|
|
624
|
+
var setFolderCommand = (path, options) => Effect8.gen(function* () {
|
|
625
|
+
const settings = yield* SettingsService;
|
|
626
|
+
if (options?.reset) {
|
|
627
|
+
yield* settings.resetDownloadDir;
|
|
628
|
+
yield* logSuccess("Reset to current directory");
|
|
629
|
+
return;
|
|
509
630
|
}
|
|
510
|
-
if (
|
|
511
|
-
|
|
631
|
+
if (path) {
|
|
632
|
+
yield* settings.setDownloadDir(path);
|
|
633
|
+
yield* log(`${c.sym.success} ${c.info(path)}`);
|
|
634
|
+
} else {
|
|
635
|
+
const dir = yield* settings.getDownloadDir;
|
|
636
|
+
yield* logInfo(dir);
|
|
512
637
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async function handlePrepare() {
|
|
521
|
-
try {
|
|
522
|
-
if (await findBinary()) {
|
|
523
|
-
console.log(`${c.sym.success} ${c.success("Ready")}`);
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
console.log(`${c.dim("Downloading yt-dlp...")}`);
|
|
527
|
-
await downloadLatestBinary();
|
|
528
|
-
console.log(`${c.sym.success} ${c.success("Ready")}`);
|
|
529
|
-
} catch (err) {
|
|
530
|
-
exitWithError(err);
|
|
638
|
+
});
|
|
639
|
+
var downloadCommand = (url, options) => Effect8.gen(function* () {
|
|
640
|
+
const quiet = options.quiet ?? false;
|
|
641
|
+
if (!quiet) {
|
|
642
|
+
yield* Effect8.sync(() => showBanner());
|
|
643
|
+
yield* log(`${c.dim("URL:")} ${c.info(url)}
|
|
644
|
+
`);
|
|
531
645
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.log(`${c.sym.success} ${c.info(folderPath)}`);
|
|
543
|
-
} else {
|
|
544
|
-
console.log(c.info(await getDownloadDir()));
|
|
545
|
-
}
|
|
546
|
-
} catch (err) {
|
|
547
|
-
exitWithError(err);
|
|
646
|
+
const download = yield* DownloadService;
|
|
647
|
+
const result = yield* download.download(url, options);
|
|
648
|
+
if (quiet) {
|
|
649
|
+
yield* log(result.fileName);
|
|
650
|
+
} else {
|
|
651
|
+
yield* log("");
|
|
652
|
+
yield* logSuccess("Process done!");
|
|
653
|
+
yield* log("");
|
|
654
|
+
const sizeInfo = result.fileSize ? ` ${c.size(`(${result.fileSize})`)}` : "";
|
|
655
|
+
yield* log(`${c.file(result.fileName)}${sizeInfo}`);
|
|
548
656
|
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
657
|
+
});
|
|
658
|
+
// index.ts
|
|
659
|
+
var isUrl = (arg) => arg.startsWith("http://") || arg.startsWith("https://");
|
|
660
|
+
var configureCli = () => {
|
|
661
|
+
program.name(APP_NAME).usage("<url> [options]").description("Download audio or video from YouTube").version(APP_VERSION, "-v, --version").argument("[url]", "YouTube URL to download").option("-f, --format <format>", "Output format (mp3, mp4, mkv, etc.)", DEFAULT_FORMAT).option("-c, --clip <range>", "Clip range (e.g. 1:30-2:45)").option("-q, --quiet", "Minimal output (only file name)").addHelpText("beforeAll", "").hook("preAction", (cmd) => {
|
|
662
|
+
const isHelp = cmd.args.includes("help") || process.argv.includes("--help") || process.argv.includes("-h");
|
|
663
|
+
if (isHelp)
|
|
553
664
|
showBanner();
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (opts.quiet) {
|
|
563
|
-
console.log(result.fileName);
|
|
564
|
-
} else {
|
|
565
|
-
console.log();
|
|
566
|
-
console.log(`${c.sym.success} ${c.success("Process done!")}
|
|
567
|
-
`);
|
|
568
|
-
const sizeInfo = result.fileSize ? ` ${c.size(`(${result.fileSize})`)}` : "";
|
|
569
|
-
console.log(`${c.file(result.fileName)}${sizeInfo}`);
|
|
665
|
+
}).action((url, opts) => {
|
|
666
|
+
if (url && isUrl(url)) {
|
|
667
|
+
const options = {
|
|
668
|
+
format: opts?.format ?? DEFAULT_FORMAT,
|
|
669
|
+
clip: opts?.clip,
|
|
670
|
+
quiet: opts?.quiet
|
|
671
|
+
};
|
|
672
|
+
return runCommand(downloadCommand(url, options), AppLive);
|
|
570
673
|
}
|
|
571
|
-
}
|
|
572
|
-
|
|
674
|
+
});
|
|
675
|
+
program.command("prepare").description("Download yt-dlp binary").action(() => runCommand(prepareCommand, BinaryLive));
|
|
676
|
+
program.command("setDefaultFolder [path]").description("Set or view default download folder").option("-r, --reset", "Reset to current directory").action((path, opts) => runCommand(setFolderCommand(path, opts), SettingsLive));
|
|
677
|
+
return program;
|
|
678
|
+
};
|
|
679
|
+
var main = () => {
|
|
680
|
+
if (process.argv.length <= 2) {
|
|
681
|
+
showBanner();
|
|
682
|
+
configureCli().help();
|
|
683
|
+
return;
|
|
573
684
|
}
|
|
574
|
-
|
|
575
|
-
program.name(APP_NAME).usage("<url> [options]").description("Download audio from YouTube").version(APP_VERSION, "-v, --version").option("-f, --format <format>", "Audio format", DEFAULT_FORMAT).option("-c, --clip <range>", "Clip range (e.g. 1:30-2:45)").option("-q, --quiet", "Minimal output (only file name)").addHelpText("beforeAll", "").hook("preAction", (thisCommand) => {
|
|
576
|
-
if (thisCommand.args.includes("help") || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
685
|
+
if (process.argv.includes("-v") || process.argv.includes("--version")) {
|
|
577
686
|
showBanner();
|
|
578
687
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
program.command("download <url>", { hidden: true }).description("Download audio from YouTube URL").option("-f, --format <format>", "Audio format", DEFAULT_FORMAT).option("-c, --clip <range>", "Clip range (e.g. 1:30-2:45)").option("-q, --quiet", "Minimal output").action(handleDownload);
|
|
583
|
-
var firstArg = process.argv[2];
|
|
584
|
-
if (firstArg && isUrl(firstArg)) {
|
|
585
|
-
process.argv.splice(2, 0, "download");
|
|
586
|
-
}
|
|
587
|
-
if (process.argv.length <= 2) {
|
|
588
|
-
showBanner();
|
|
589
|
-
program.help();
|
|
590
|
-
}
|
|
591
|
-
if (process.argv.includes("-v") || process.argv.includes("--version")) {
|
|
592
|
-
showBanner();
|
|
593
|
-
}
|
|
594
|
-
program.parse();
|
|
688
|
+
configureCli().parse();
|
|
689
|
+
};
|
|
690
|
+
main();
|