ytdwn 1.0.1 → 1.1.1
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 +460 -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 +15 -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,159 @@ 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.1";
|
|
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 YTDlpWrapModule 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
|
|
161
|
+
var YTDlpWrap = YTDlpWrapModule.default ?? YTDlpWrapModule;
|
|
80
162
|
var BINARY_NAMES = {
|
|
81
163
|
win32: ["yt-dlp.exe"],
|
|
82
164
|
darwin: {
|
|
@@ -89,7 +171,7 @@ var BINARY_NAMES = {
|
|
|
89
171
|
}
|
|
90
172
|
};
|
|
91
173
|
var FALLBACK_BINARY = "yt-dlp";
|
|
92
|
-
|
|
174
|
+
var getCandidateNames = () => {
|
|
93
175
|
const platformNames = BINARY_NAMES[process.platform];
|
|
94
176
|
if (!platformNames)
|
|
95
177
|
return [FALLBACK_BINARY];
|
|
@@ -97,96 +179,106 @@ function getCandidateNames() {
|
|
|
97
179
|
return platformNames;
|
|
98
180
|
const archNames = platformNames[process.arch];
|
|
99
181
|
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) {
|
|
182
|
+
};
|
|
183
|
+
var getCandidatePaths = () => getCandidateNames().map((name) => join3(BIN_DIR, name));
|
|
184
|
+
var pickAsset = (release) => {
|
|
124
185
|
const assets = release.assets ?? [];
|
|
125
186
|
const candidates = getCandidateNames();
|
|
126
187
|
const match = assets.find((a) => candidates.includes(a.name)) ?? assets.find((a) => a.name === FALLBACK_BINARY);
|
|
127
188
|
if (!match) {
|
|
128
|
-
|
|
189
|
+
return Effect4.fail(new BinaryDownloadError({
|
|
190
|
+
platform: `${process.platform}-${process.arch}`,
|
|
191
|
+
cause: "No suitable binary found for this platform"
|
|
192
|
+
}));
|
|
129
193
|
}
|
|
130
|
-
return match;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
194
|
+
return Effect4.succeed(match);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
class BinaryService extends Context2.Tag("BinaryService")() {
|
|
198
|
+
}
|
|
199
|
+
var BinaryServiceLive = Layer2.effect(BinaryService, Effect4.gen(function* () {
|
|
200
|
+
const settings = yield* SettingsService;
|
|
201
|
+
const searchForBinary = Effect4.gen(function* () {
|
|
202
|
+
for (const candidate of getCandidatePaths()) {
|
|
203
|
+
const executable = yield* isExecutable(candidate);
|
|
204
|
+
if (executable) {
|
|
205
|
+
yield* settings.setCachedBinaryPath(candidate).pipe(Effect4.ignore);
|
|
206
|
+
return candidate;
|
|
207
|
+
}
|
|
141
208
|
}
|
|
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;
|
|
209
|
+
return null;
|
|
210
|
+
});
|
|
211
|
+
const findBinary = Effect4.gen(function* () {
|
|
212
|
+
const cached = yield* settings.getCachedBinaryPath;
|
|
213
|
+
if (cached) {
|
|
214
|
+
const executable = yield* isExecutable(cached);
|
|
215
|
+
if (executable)
|
|
216
|
+
return cached;
|
|
161
217
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
218
|
+
const found = yield* searchForBinary;
|
|
219
|
+
if (found)
|
|
220
|
+
return found;
|
|
221
|
+
if (cached) {
|
|
222
|
+
yield* settings.clearCachedBinaryPath.pipe(Effect4.ignore);
|
|
167
223
|
}
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
224
|
+
return null;
|
|
225
|
+
});
|
|
226
|
+
const requireBinary = Effect4.gen(function* () {
|
|
227
|
+
const existing = yield* findBinary;
|
|
228
|
+
if (existing)
|
|
229
|
+
return existing;
|
|
230
|
+
return yield* Effect4.fail(new BinaryNotFoundError({
|
|
231
|
+
message: "yt-dlp binary not found. Run 'ytdwn prepare' first."
|
|
232
|
+
}));
|
|
233
|
+
});
|
|
234
|
+
const getYtDlpWrap = Effect4.gen(function* () {
|
|
235
|
+
const binaryPath = yield* requireBinary;
|
|
236
|
+
return new YTDlpWrap(binaryPath);
|
|
237
|
+
});
|
|
238
|
+
const downloadLatestBinary = Effect4.gen(function* () {
|
|
239
|
+
const releases = yield* Effect4.tryPromise({
|
|
240
|
+
try: () => YTDlpWrap.getGithubReleases(1, 1),
|
|
241
|
+
catch: (cause) => new BinaryDownloadError({
|
|
242
|
+
platform: `${process.platform}-${process.arch}`,
|
|
243
|
+
cause
|
|
244
|
+
})
|
|
245
|
+
});
|
|
246
|
+
const release = Array.isArray(releases) ? releases[0] : releases;
|
|
247
|
+
if (!release) {
|
|
248
|
+
return yield* Effect4.fail(new BinaryDownloadError({
|
|
249
|
+
platform: `${process.platform}-${process.arch}`,
|
|
250
|
+
cause: "No releases found"
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
const asset = yield* pickAsset(release);
|
|
254
|
+
const binaryPath = join3(BIN_DIR, asset.name);
|
|
255
|
+
const exists = yield* isExecutable(binaryPath);
|
|
256
|
+
if (exists) {
|
|
257
|
+
yield* settings.setCachedBinaryPath(binaryPath).pipe(Effect4.ignore);
|
|
258
|
+
return binaryPath;
|
|
259
|
+
}
|
|
260
|
+
yield* ensureDirectory(BIN_DIR);
|
|
261
|
+
const data = yield* fetchBinaryWithRetry(asset.browser_download_url);
|
|
262
|
+
yield* writeFileBinary(binaryPath, data);
|
|
263
|
+
if (platform() !== "win32") {
|
|
264
|
+
yield* makeExecutable(binaryPath);
|
|
265
|
+
}
|
|
266
|
+
yield* settings.setCachedBinaryPath(binaryPath).pipe(Effect4.ignore);
|
|
177
267
|
return binaryPath;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
findBinary,
|
|
271
|
+
requireBinary,
|
|
272
|
+
getYtDlpWrap,
|
|
273
|
+
downloadLatestBinary
|
|
274
|
+
};
|
|
275
|
+
}));
|
|
183
276
|
|
|
184
|
-
// src/
|
|
185
|
-
import {
|
|
186
|
-
import { spawn } from "child_process";
|
|
187
|
-
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
|
|
277
|
+
// src/services/DownloadService.ts
|
|
278
|
+
import { Effect as Effect6, Context as Context3, Layer as Layer3 } from "effect";
|
|
188
279
|
|
|
189
280
|
// src/timestamp.ts
|
|
281
|
+
import { Effect as Effect5 } from "effect";
|
|
190
282
|
var TIME_FORMAT_ERROR = "Invalid time format. Use MM:SS or HH:MM:SS (e.g. 0:02 or 01:23:45).";
|
|
191
283
|
var RANGE_FORMAT_ERROR = "Range must be in 'start-end' format (e.g. 0:02-23:10).";
|
|
192
284
|
var pad = (value) => value.padStart(2, "0");
|
|
@@ -238,7 +330,7 @@ var c = {
|
|
|
238
330
|
}
|
|
239
331
|
};
|
|
240
332
|
|
|
241
|
-
// src/
|
|
333
|
+
// src/services/DownloadService.ts
|
|
242
334
|
var SPINNER_FRAMES = [
|
|
243
335
|
"⠋",
|
|
244
336
|
"⠙",
|
|
@@ -254,20 +346,6 @@ var SPINNER_FRAMES = [
|
|
|
254
346
|
var SPINNER_INTERVAL_MS = 80;
|
|
255
347
|
var PROGRESS_BAR_WIDTH = 12;
|
|
256
348
|
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
349
|
var write = (text) => process.stdout.write(text);
|
|
272
350
|
var clearLine = () => write(`\r${" ".repeat(LINE_CLEAR_WIDTH)}\r`);
|
|
273
351
|
function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
@@ -275,10 +353,7 @@ function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
|
275
353
|
let message = initialMessage;
|
|
276
354
|
let stopped = false;
|
|
277
355
|
if (quiet) {
|
|
278
|
-
return {
|
|
279
|
-
update: () => {},
|
|
280
|
-
stop: () => {}
|
|
281
|
-
};
|
|
356
|
+
return { update: () => {}, stop: () => {} };
|
|
282
357
|
}
|
|
283
358
|
const interval = setInterval(() => {
|
|
284
359
|
if (!stopped) {
|
|
@@ -299,7 +374,7 @@ function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
|
299
374
|
}
|
|
300
375
|
};
|
|
301
376
|
}
|
|
302
|
-
function renderProgress(
|
|
377
|
+
function renderProgress(percent, speed, quiet = false) {
|
|
303
378
|
if (quiet)
|
|
304
379
|
return;
|
|
305
380
|
const filled = Math.round(percent / 100 * PROGRESS_BAR_WIDTH);
|
|
@@ -307,58 +382,45 @@ function renderProgress({ percent, speed }, quiet = false) {
|
|
|
307
382
|
const speedText = speed ? ` ${c.speed(speed)}` : "";
|
|
308
383
|
write(`\r${c.bold("Downloading:")} ${bar} ${c.bar.percent(`${percent.toFixed(0)}%`)}${speedText} `);
|
|
309
384
|
}
|
|
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
|
-
}
|
|
385
|
+
function mapOutputToError(output, url) {
|
|
386
|
+
const ageRestrictedPatterns = [
|
|
387
|
+
/age[- ]?restrict/i,
|
|
388
|
+
/age[- ]?gate/i,
|
|
389
|
+
/sign in to confirm your age/i,
|
|
390
|
+
/this video is age-restricted/i,
|
|
391
|
+
/confirm your age/i
|
|
392
|
+
];
|
|
393
|
+
if (ageRestrictedPatterns.some((p) => p.test(output))) {
|
|
394
|
+
return new AgeRestrictedError({ url });
|
|
324
395
|
}
|
|
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;
|
|
396
|
+
if (/\b(network|connection)\s+(error|failed|refused)/i.test(output)) {
|
|
397
|
+
return new ConnectionError({ message: "Connection error, try again" });
|
|
336
398
|
}
|
|
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;
|
|
399
|
+
if (/video unavailable/i.test(output) || /private video/i.test(output)) {
|
|
400
|
+
return new VideoNotFoundError({ url });
|
|
345
401
|
}
|
|
346
402
|
return null;
|
|
347
403
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
404
|
+
function mapExitCodeToError(code, url) {
|
|
405
|
+
switch (code) {
|
|
406
|
+
case 1:
|
|
407
|
+
return new VideoNotFoundError({ url });
|
|
408
|
+
case 2:
|
|
409
|
+
return new InvalidUrlError({ url });
|
|
410
|
+
default:
|
|
411
|
+
return new BinaryExecutionError({
|
|
412
|
+
exitCode: code,
|
|
413
|
+
message: `Download failed with exit code ${code}`
|
|
414
|
+
});
|
|
415
|
+
}
|
|
351
416
|
}
|
|
417
|
+
var VIDEO_FORMATS = ["mp4", "mkv", "webm", "avi", "mov"];
|
|
418
|
+
var isVideoFormat = (format) => VIDEO_FORMATS.includes(format.toLowerCase());
|
|
352
419
|
function buildArgs(url, options, downloadDir) {
|
|
420
|
+
const format = options.format.toLowerCase();
|
|
421
|
+
const isVideo = isVideoFormat(format);
|
|
353
422
|
const baseArgs = [
|
|
354
423
|
url,
|
|
355
|
-
"-f",
|
|
356
|
-
"bestaudio/best",
|
|
357
|
-
"-x",
|
|
358
|
-
"--audio-format",
|
|
359
|
-
options.format,
|
|
360
|
-
"--audio-quality",
|
|
361
|
-
DEFAULT_AUDIO_QUALITY,
|
|
362
424
|
"-o",
|
|
363
425
|
getOutputTemplate(downloadDir),
|
|
364
426
|
"--no-playlist",
|
|
@@ -366,68 +428,39 @@ function buildArgs(url, options, downloadDir) {
|
|
|
366
428
|
"--progress",
|
|
367
429
|
"--concurrent-fragments",
|
|
368
430
|
CONCURRENT_FRAGMENTS,
|
|
369
|
-
"--no-check-certificates"
|
|
431
|
+
"--no-check-certificates"
|
|
432
|
+
];
|
|
433
|
+
const formatArgs = isVideo ? [
|
|
434
|
+
"-f",
|
|
435
|
+
`bestvideo[ext=${format}]+bestaudio/best[ext=${format}]/best`,
|
|
436
|
+
"--merge-output-format",
|
|
437
|
+
format
|
|
438
|
+
] : [
|
|
439
|
+
"-f",
|
|
440
|
+
"bestaudio/best",
|
|
441
|
+
"-x",
|
|
442
|
+
"--audio-format",
|
|
443
|
+
format,
|
|
444
|
+
"--audio-quality",
|
|
445
|
+
DEFAULT_AUDIO_QUALITY,
|
|
370
446
|
"--prefer-free-formats"
|
|
371
447
|
];
|
|
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];
|
|
448
|
+
const clipArgs = options.clip ? ["--download-sections", `*${parseClipRange(options.clip)}`] : [];
|
|
449
|
+
return [...baseArgs, ...formatArgs, ...clipArgs];
|
|
380
450
|
}
|
|
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
|
-
};
|
|
451
|
+
|
|
452
|
+
class DownloadService extends Context3.Tag("DownloadService")() {
|
|
419
453
|
}
|
|
420
|
-
function
|
|
421
|
-
return
|
|
454
|
+
function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
|
|
455
|
+
return Effect6.async((resume) => {
|
|
456
|
+
const spinner = createSpinner("Getting ready...", quiet);
|
|
422
457
|
const state = {
|
|
423
458
|
phase: "init",
|
|
424
|
-
lastPercent: 0,
|
|
425
459
|
fileName: null,
|
|
426
460
|
fileSize: null,
|
|
427
|
-
|
|
461
|
+
outputBuffer: "",
|
|
428
462
|
convertingInterval: null
|
|
429
463
|
};
|
|
430
|
-
const handleOutput = createOutputHandler(spinner, state);
|
|
431
464
|
const cleanup = () => {
|
|
432
465
|
spinner.stop();
|
|
433
466
|
if (state.convertingInterval) {
|
|
@@ -436,40 +469,118 @@ function attachProcessHandlers(child, spinner, downloadDir, quiet) {
|
|
|
436
469
|
if (!quiet)
|
|
437
470
|
clearLine();
|
|
438
471
|
};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
fileSize: state.fileSize || undefined
|
|
448
|
-
});
|
|
449
|
-
} else {
|
|
450
|
-
reject(new Error(`Download failed (code ${code})`));
|
|
472
|
+
const emitter = ytDlpWrap.exec(args);
|
|
473
|
+
emitter.on("progress", (progress) => {
|
|
474
|
+
if (state.phase === "init") {
|
|
475
|
+
state.phase = "downloading";
|
|
476
|
+
spinner.stop();
|
|
477
|
+
}
|
|
478
|
+
if (progress.percent !== undefined) {
|
|
479
|
+
renderProgress(progress.percent, progress.currentSpeed, quiet);
|
|
451
480
|
}
|
|
452
481
|
});
|
|
453
|
-
|
|
482
|
+
emitter.on("ytDlpEvent", (eventType, eventData) => {
|
|
483
|
+
state.outputBuffer += `[${eventType}] ${eventData}
|
|
484
|
+
`;
|
|
485
|
+
if (eventType === "ExtractAudio" || eventType === "Merger") {
|
|
486
|
+
const match = eventData.match(/Destination:\s*(.+)/);
|
|
487
|
+
if (match?.[1]) {
|
|
488
|
+
state.fileName = match[1].split("/").pop() || null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (eventType === "download") {
|
|
492
|
+
const destMatch = eventData.match(/Destination:\s*(.+)/);
|
|
493
|
+
if (destMatch?.[1]) {
|
|
494
|
+
state.fileName = destMatch[1].split("/").pop() || null;
|
|
495
|
+
}
|
|
496
|
+
const sizeMatch = eventData.match(/~?([\d.]+\s*[KMG]i?B)/i);
|
|
497
|
+
if (sizeMatch?.[1]) {
|
|
498
|
+
state.fileSize = sizeMatch[1];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if ((eventType === "ExtractAudio" || eventType === "ffmpeg") && state.phase !== "converting") {
|
|
502
|
+
state.phase = "converting";
|
|
503
|
+
spinner.stop();
|
|
504
|
+
clearLine();
|
|
505
|
+
if (!quiet) {
|
|
506
|
+
let frame = 0;
|
|
507
|
+
state.convertingInterval = setInterval(() => {
|
|
508
|
+
const frameChar = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "⠋";
|
|
509
|
+
write(`\r${c.info(frameChar)} ${c.dim("Converting...")}`);
|
|
510
|
+
}, SPINNER_INTERVAL_MS);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (state.phase === "init") {
|
|
514
|
+
if (eventType === "youtube" && eventData.includes("Extracting")) {
|
|
515
|
+
spinner.update("Extracting...");
|
|
516
|
+
} else if (eventType === "youtube" && eventData.includes("Downloading webpage")) {
|
|
517
|
+
spinner.update("Fetching info...");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
emitter.on("error", (error) => {
|
|
522
|
+
cleanup();
|
|
523
|
+
const outputError = mapOutputToError(String(error), url);
|
|
524
|
+
resume(Effect6.fail(outputError ?? new BinaryExecutionError({
|
|
525
|
+
exitCode: -1,
|
|
526
|
+
message: String(error)
|
|
527
|
+
})));
|
|
528
|
+
});
|
|
529
|
+
emitter.on("close", () => {
|
|
454
530
|
cleanup();
|
|
455
|
-
|
|
531
|
+
const outputError = mapOutputToError(state.outputBuffer, url);
|
|
532
|
+
if (outputError) {
|
|
533
|
+
resume(Effect6.fail(outputError));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const exitCode = emitter.ytDlpProcess?.exitCode;
|
|
537
|
+
if (exitCode !== null && exitCode !== undefined && exitCode !== 0) {
|
|
538
|
+
resume(Effect6.fail(mapExitCodeToError(exitCode, url)));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
resume(Effect6.succeed({
|
|
542
|
+
filePath: downloadDir,
|
|
543
|
+
fileName: state.fileName || "audio",
|
|
544
|
+
fileSize: state.fileSize || undefined
|
|
545
|
+
}));
|
|
456
546
|
});
|
|
457
547
|
});
|
|
458
548
|
}
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
549
|
+
var DownloadServiceLive = Layer3.effect(DownloadService, Effect6.gen(function* () {
|
|
550
|
+
const binary = yield* BinaryService;
|
|
551
|
+
const settings = yield* SettingsService;
|
|
552
|
+
return {
|
|
553
|
+
download: (url, options) => Effect6.gen(function* () {
|
|
554
|
+
const [downloadDir, ytDlpWrap] = yield* Effect6.all([
|
|
555
|
+
settings.getDownloadDir.pipe(Effect6.flatMap((dir) => ensureDirectory(dir))),
|
|
556
|
+
binary.getYtDlpWrap
|
|
557
|
+
]);
|
|
558
|
+
const quiet = options.quiet ?? false;
|
|
559
|
+
const args = buildArgs(url, options, downloadDir);
|
|
560
|
+
return yield* executeDownload(ytDlpWrap, args, downloadDir, url, quiet);
|
|
561
|
+
})
|
|
562
|
+
};
|
|
563
|
+
}));
|
|
472
564
|
|
|
565
|
+
// src/layers/AppLive.ts
|
|
566
|
+
var AppLive = DownloadServiceLive.pipe(Layer4.provide(BinaryServiceLive), Layer4.provide(SettingsServiceLive));
|
|
567
|
+
var SettingsLive = SettingsServiceLive;
|
|
568
|
+
var BinaryLive = BinaryServiceLive.pipe(Layer4.provide(SettingsServiceLive));
|
|
569
|
+
// src/cli/errors.ts
|
|
570
|
+
import { Effect as Effect7, Match, Console, pipe } from "effect";
|
|
571
|
+
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);
|
|
572
|
+
var isAppError = (error) => error !== null && typeof error === "object" && ("_tag" in error) && typeof error._tag === "string";
|
|
573
|
+
var formatError = (error) => {
|
|
574
|
+
if (isAppError(error))
|
|
575
|
+
return formatAppError(error);
|
|
576
|
+
if (error instanceof Error)
|
|
577
|
+
return error.message;
|
|
578
|
+
return String(error);
|
|
579
|
+
};
|
|
580
|
+
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))))));
|
|
581
|
+
var runCommand = (effect, layer) => Effect7.runPromise(pipe(effect, Effect7.provide(layer), withErrorHandler));
|
|
582
|
+
// src/cli/commands.ts
|
|
583
|
+
import { Effect as Effect8 } from "effect";
|
|
473
584
|
// src/banner.ts
|
|
474
585
|
import cfonts from "cfonts";
|
|
475
586
|
import gradient from "gradient-string";
|
|
@@ -495,100 +606,86 @@ function showBanner() {
|
|
|
495
606
|
`));
|
|
496
607
|
}
|
|
497
608
|
|
|
498
|
-
//
|
|
499
|
-
var
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
609
|
+
// src/cli/commands.ts
|
|
610
|
+
var log = (message) => Effect8.sync(() => console.log(message));
|
|
611
|
+
var logSuccess = (message) => log(`${c.sym.success} ${c.success(message)}`);
|
|
612
|
+
var logInfo = (message) => log(c.info(message));
|
|
613
|
+
var logDim = (message) => log(c.dim(message));
|
|
614
|
+
var prepareCommand = Effect8.gen(function* () {
|
|
615
|
+
const binary = yield* BinaryService;
|
|
616
|
+
const existing = yield* binary.findBinary;
|
|
617
|
+
if (existing) {
|
|
618
|
+
yield* logSuccess("Ready");
|
|
619
|
+
return;
|
|
506
620
|
}
|
|
507
|
-
|
|
508
|
-
|
|
621
|
+
yield* logDim("Downloading yt-dlp...");
|
|
622
|
+
yield* binary.downloadLatestBinary;
|
|
623
|
+
yield* logSuccess("Ready");
|
|
624
|
+
});
|
|
625
|
+
var setFolderCommand = (path, options) => Effect8.gen(function* () {
|
|
626
|
+
const settings = yield* SettingsService;
|
|
627
|
+
if (options?.reset) {
|
|
628
|
+
yield* settings.resetDownloadDir;
|
|
629
|
+
yield* logSuccess("Reset to current directory");
|
|
630
|
+
return;
|
|
509
631
|
}
|
|
510
|
-
if (
|
|
511
|
-
|
|
632
|
+
if (path) {
|
|
633
|
+
yield* settings.setDownloadDir(path);
|
|
634
|
+
yield* log(`${c.sym.success} ${c.info(path)}`);
|
|
635
|
+
} else {
|
|
636
|
+
const dir = yield* settings.getDownloadDir;
|
|
637
|
+
yield* logInfo(dir);
|
|
512
638
|
}
|
|
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);
|
|
639
|
+
});
|
|
640
|
+
var downloadCommand = (url, options) => Effect8.gen(function* () {
|
|
641
|
+
const quiet = options.quiet ?? false;
|
|
642
|
+
if (!quiet) {
|
|
643
|
+
yield* Effect8.sync(() => showBanner());
|
|
644
|
+
yield* log(`${c.dim("URL:")} ${c.info(url)}
|
|
645
|
+
`);
|
|
531
646
|
}
|
|
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);
|
|
647
|
+
const download = yield* DownloadService;
|
|
648
|
+
const result = yield* download.download(url, options);
|
|
649
|
+
if (quiet) {
|
|
650
|
+
yield* log(result.fileName);
|
|
651
|
+
} else {
|
|
652
|
+
yield* log("");
|
|
653
|
+
yield* logSuccess("Process done!");
|
|
654
|
+
yield* log("");
|
|
655
|
+
const sizeInfo = result.fileSize ? ` ${c.size(`(${result.fileSize})`)}` : "";
|
|
656
|
+
yield* log(`${c.file(result.fileName)}${sizeInfo}`);
|
|
548
657
|
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
658
|
+
});
|
|
659
|
+
// index.ts
|
|
660
|
+
var isUrl = (arg) => arg.startsWith("http://") || arg.startsWith("https://");
|
|
661
|
+
var configureCli = () => {
|
|
662
|
+
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) => {
|
|
663
|
+
const isHelp = cmd.args.includes("help") || process.argv.includes("--help") || process.argv.includes("-h");
|
|
664
|
+
if (isHelp)
|
|
553
665
|
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}`);
|
|
666
|
+
}).action((url, opts) => {
|
|
667
|
+
if (url && isUrl(url)) {
|
|
668
|
+
const options = {
|
|
669
|
+
format: opts?.format ?? DEFAULT_FORMAT,
|
|
670
|
+
clip: opts?.clip,
|
|
671
|
+
quiet: opts?.quiet
|
|
672
|
+
};
|
|
673
|
+
return runCommand(downloadCommand(url, options), AppLive);
|
|
570
674
|
}
|
|
571
|
-
}
|
|
572
|
-
|
|
675
|
+
});
|
|
676
|
+
program.command("prepare").description("Download yt-dlp binary").action(() => runCommand(prepareCommand, BinaryLive));
|
|
677
|
+
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));
|
|
678
|
+
return program;
|
|
679
|
+
};
|
|
680
|
+
var main = () => {
|
|
681
|
+
if (process.argv.length <= 2) {
|
|
682
|
+
showBanner();
|
|
683
|
+
configureCli().help();
|
|
684
|
+
return;
|
|
573
685
|
}
|
|
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")) {
|
|
686
|
+
if (process.argv.includes("-v") || process.argv.includes("--version")) {
|
|
577
687
|
showBanner();
|
|
578
688
|
}
|
|
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();
|
|
689
|
+
configureCli().parse();
|
|
690
|
+
};
|
|
691
|
+
main();
|