ytdwn 1.0.0 → 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 +461 -333
- 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
|
@@ -6,60 +6,158 @@ import { program } from "commander";
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
var APP_NAME = "ytdwn";
|
|
9
|
-
var APP_TAGLINE = "YouTube to MP3 • Fast & Simple";
|
|
10
|
-
var APP_VERSION = "1.
|
|
11
|
-
var
|
|
9
|
+
var APP_TAGLINE = "YouTube to MP3/MP4 • Fast & Simple";
|
|
10
|
+
var APP_VERSION = "1.1.0";
|
|
11
|
+
var DEFAULT_AUDIO_FORMAT = "mp3";
|
|
12
12
|
var DEFAULT_AUDIO_QUALITY = "0";
|
|
13
13
|
var CONCURRENT_FRAGMENTS = "8";
|
|
14
|
+
var DEFAULT_FORMAT = DEFAULT_AUDIO_FORMAT;
|
|
14
15
|
var BIN_DIR = join(process.cwd(), "bin");
|
|
15
16
|
var getOutputTemplate = (downloadDir) => join(downloadDir, "%(title)s.%(ext)s");
|
|
16
17
|
|
|
17
|
-
// src/
|
|
18
|
-
import {
|
|
19
|
-
import { access, chmod, mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
20
|
-
import { platform } from "os";
|
|
21
|
-
import { join as join3, dirname } from "path";
|
|
22
|
-
import YTDlpWrap from "yt-dlp-wrap";
|
|
18
|
+
// src/layers/AppLive.ts
|
|
19
|
+
import { Layer as Layer4 } from "effect";
|
|
23
20
|
|
|
24
|
-
// src/
|
|
25
|
-
import {
|
|
21
|
+
// src/services/SettingsService.ts
|
|
22
|
+
import { Effect as Effect2, Context, Layer } from "effect";
|
|
26
23
|
import { homedir } from "os";
|
|
27
24
|
import { join as join2, resolve } from "path";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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") {
|
|
35
47
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const { downloadDir } = await load();
|
|
39
|
-
return downloadDir ?? process.cwd();
|
|
48
|
+
|
|
49
|
+
class DownloadError extends Data.TaggedError("DownloadError") {
|
|
40
50
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await save({ ...settings, downloadDir: resolve(dir) });
|
|
51
|
+
|
|
52
|
+
class BinaryNotFoundError extends Data.TaggedError("BinaryNotFoundError") {
|
|
44
53
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await save(rest);
|
|
54
|
+
|
|
55
|
+
class BinaryDownloadError extends Data.TaggedError("BinaryDownloadError") {
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return binaryPath ?? null;
|
|
57
|
+
|
|
58
|
+
class BinaryExecutionError extends Data.TaggedError("BinaryExecutionError") {
|
|
52
59
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await save({ ...settings, binaryPath: path });
|
|
60
|
+
|
|
61
|
+
class TimestampParseError extends Data.TaggedError("TimestampParseError") {
|
|
56
62
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await save(rest);
|
|
63
|
+
|
|
64
|
+
class VideoNotFoundError extends Data.TaggedError("VideoNotFoundError") {
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
class InvalidUrlError extends Data.TaggedError("InvalidUrlError") {
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class AgeRestrictedError extends Data.TaggedError("AgeRestrictedError") {
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class ConnectionError extends Data.TaggedError("ConnectionError") {
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
|
63
161
|
var BINARY_NAMES = {
|
|
64
162
|
win32: ["yt-dlp.exe"],
|
|
65
163
|
darwin: {
|
|
@@ -72,7 +170,7 @@ var BINARY_NAMES = {
|
|
|
72
170
|
}
|
|
73
171
|
};
|
|
74
172
|
var FALLBACK_BINARY = "yt-dlp";
|
|
75
|
-
|
|
173
|
+
var getCandidateNames = () => {
|
|
76
174
|
const platformNames = BINARY_NAMES[process.platform];
|
|
77
175
|
if (!platformNames)
|
|
78
176
|
return [FALLBACK_BINARY];
|
|
@@ -80,81 +178,106 @@ function getCandidateNames() {
|
|
|
80
178
|
return platformNames;
|
|
81
179
|
const archNames = platformNames[process.arch];
|
|
82
180
|
return archNames ?? [FALLBACK_BINARY];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
async function isExecutable(filePath) {
|
|
88
|
-
try {
|
|
89
|
-
await access(filePath, fsConstants.X_OK);
|
|
90
|
-
return true;
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
async function downloadFile(url, targetPath) {
|
|
96
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
97
|
-
const response = await fetch(url);
|
|
98
|
-
if (!response.ok) {
|
|
99
|
-
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
100
|
-
}
|
|
101
|
-
await writeFile2(targetPath, Buffer.from(await response.arrayBuffer()));
|
|
102
|
-
if (platform() !== "win32") {
|
|
103
|
-
await chmod(targetPath, 493);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function pickAsset(release) {
|
|
181
|
+
};
|
|
182
|
+
var getCandidatePaths = () => getCandidateNames().map((name) => join3(BIN_DIR, name));
|
|
183
|
+
var pickAsset = (release) => {
|
|
107
184
|
const assets = release.assets ?? [];
|
|
108
185
|
const candidates = getCandidateNames();
|
|
109
186
|
const match = assets.find((a) => candidates.includes(a.name)) ?? assets.find((a) => a.name === FALLBACK_BINARY);
|
|
110
187
|
if (!match) {
|
|
111
|
-
|
|
188
|
+
return Effect4.fail(new BinaryDownloadError({
|
|
189
|
+
platform: `${process.platform}-${process.arch}`,
|
|
190
|
+
cause: "No suitable binary found for this platform"
|
|
191
|
+
}));
|
|
112
192
|
}
|
|
113
|
-
return match;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}
|
|
124
207
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
216
|
+
}
|
|
217
|
+
const found = yield* searchForBinary;
|
|
218
|
+
if (found)
|
|
219
|
+
return found;
|
|
220
|
+
if (cached) {
|
|
221
|
+
yield* settings.clearCachedBinaryPath.pipe(Effect4.ignore);
|
|
222
|
+
}
|
|
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);
|
|
145
266
|
return binaryPath;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
findBinary,
|
|
270
|
+
requireBinary,
|
|
271
|
+
getYtDlpWrap,
|
|
272
|
+
downloadLatestBinary
|
|
273
|
+
};
|
|
274
|
+
}));
|
|
151
275
|
|
|
152
|
-
// src/
|
|
153
|
-
import {
|
|
154
|
-
import { spawn } from "child_process";
|
|
155
|
-
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
|
|
276
|
+
// src/services/DownloadService.ts
|
|
277
|
+
import { Effect as Effect6, Context as Context3, Layer as Layer3 } from "effect";
|
|
156
278
|
|
|
157
279
|
// src/timestamp.ts
|
|
280
|
+
import { Effect as Effect5 } from "effect";
|
|
158
281
|
var TIME_FORMAT_ERROR = "Invalid time format. Use MM:SS or HH:MM:SS (e.g. 0:02 or 01:23:45).";
|
|
159
282
|
var RANGE_FORMAT_ERROR = "Range must be in 'start-end' format (e.g. 0:02-23:10).";
|
|
160
283
|
var pad = (value) => value.padStart(2, "0");
|
|
@@ -206,7 +329,7 @@ var c = {
|
|
|
206
329
|
}
|
|
207
330
|
};
|
|
208
331
|
|
|
209
|
-
// src/
|
|
332
|
+
// src/services/DownloadService.ts
|
|
210
333
|
var SPINNER_FRAMES = [
|
|
211
334
|
"⠋",
|
|
212
335
|
"⠙",
|
|
@@ -222,20 +345,6 @@ var SPINNER_FRAMES = [
|
|
|
222
345
|
var SPINNER_INTERVAL_MS = 80;
|
|
223
346
|
var PROGRESS_BAR_WIDTH = 12;
|
|
224
347
|
var LINE_CLEAR_WIDTH = 60;
|
|
225
|
-
var REGEX = {
|
|
226
|
-
percent: /(\d+\.?\d*)%/,
|
|
227
|
-
total: /of\s+~?([\d.]+\s*\w+)/i,
|
|
228
|
-
speed: /at\s+([\d.]+\s*\w+\/s)/i,
|
|
229
|
-
size: /([\d.]+)\s*(\w+)/,
|
|
230
|
-
binaryUnits: /([KMG])iB/g,
|
|
231
|
-
destination: /\[(?:ExtractAudio|Merger)\].*?Destination:\s*(.+)$/m,
|
|
232
|
-
downloadDest: /\[download\]\s+Destination:\s*(.+)$/m,
|
|
233
|
-
fileSize: /~?([\d.]+\s*[KMG]i?B)/i
|
|
234
|
-
};
|
|
235
|
-
var PHASE_MESSAGES = new Map([
|
|
236
|
-
["Extracting URL", "Extracting..."],
|
|
237
|
-
["Downloading webpage", "Fetching info..."]
|
|
238
|
-
]);
|
|
239
348
|
var write = (text) => process.stdout.write(text);
|
|
240
349
|
var clearLine = () => write(`\r${" ".repeat(LINE_CLEAR_WIDTH)}\r`);
|
|
241
350
|
function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
@@ -243,10 +352,7 @@ function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
|
243
352
|
let message = initialMessage;
|
|
244
353
|
let stopped = false;
|
|
245
354
|
if (quiet) {
|
|
246
|
-
return {
|
|
247
|
-
update: () => {},
|
|
248
|
-
stop: () => {}
|
|
249
|
-
};
|
|
355
|
+
return { update: () => {}, stop: () => {} };
|
|
250
356
|
}
|
|
251
357
|
const interval = setInterval(() => {
|
|
252
358
|
if (!stopped) {
|
|
@@ -267,7 +373,7 @@ function createSpinner(initialMessage = "Getting ready...", quiet = false) {
|
|
|
267
373
|
}
|
|
268
374
|
};
|
|
269
375
|
}
|
|
270
|
-
function renderProgress(
|
|
376
|
+
function renderProgress(percent, speed, quiet = false) {
|
|
271
377
|
if (quiet)
|
|
272
378
|
return;
|
|
273
379
|
const filled = Math.round(percent / 100 * PROGRESS_BAR_WIDTH);
|
|
@@ -275,58 +381,45 @@ function renderProgress({ percent, speed }, quiet = false) {
|
|
|
275
381
|
const speedText = speed ? ` ${c.speed(speed)}` : "";
|
|
276
382
|
write(`\r${c.bold("Downloading:")} ${bar} ${c.bar.percent(`${percent.toFixed(0)}%`)}${speedText} `);
|
|
277
383
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const [, size, unit] = total.match(REGEX.size) ?? [];
|
|
289
|
-
if (size && unit) {
|
|
290
|
-
downloaded = normalizeUnit(`${(percent / 100 * parseFloat(size)).toFixed(1)}${unit}`);
|
|
291
|
-
}
|
|
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 });
|
|
292
394
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
downloaded,
|
|
296
|
-
total: total ? normalizeUnit(total) : undefined,
|
|
297
|
-
speed: speed ? normalizeUnit(speed) : undefined
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
function detectPhase(text) {
|
|
301
|
-
for (const [pattern, message] of PHASE_MESSAGES) {
|
|
302
|
-
if (text.includes(pattern))
|
|
303
|
-
return message;
|
|
395
|
+
if (/\b(network|connection)\s+(error|failed|refused)/i.test(output)) {
|
|
396
|
+
return new ConnectionError({ message: "Connection error, try again" });
|
|
304
397
|
}
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
var isConvertingPhase = (text) => text.includes("ExtractAudio") || text.includes("Converting");
|
|
308
|
-
function extractFileName(text) {
|
|
309
|
-
const match = text.match(REGEX.destination) || text.match(REGEX.downloadDest);
|
|
310
|
-
if (match?.[1]) {
|
|
311
|
-
const fullPath = match[1].trim();
|
|
312
|
-
return fullPath.split("/").pop() || null;
|
|
398
|
+
if (/video unavailable/i.test(output) || /private video/i.test(output)) {
|
|
399
|
+
return new VideoNotFoundError({ url });
|
|
313
400
|
}
|
|
314
401
|
return null;
|
|
315
402
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
}
|
|
319
415
|
}
|
|
416
|
+
var VIDEO_FORMATS = ["mp4", "mkv", "webm", "avi", "mov"];
|
|
417
|
+
var isVideoFormat = (format) => VIDEO_FORMATS.includes(format.toLowerCase());
|
|
320
418
|
function buildArgs(url, options, downloadDir) {
|
|
419
|
+
const format = options.format.toLowerCase();
|
|
420
|
+
const isVideo = isVideoFormat(format);
|
|
321
421
|
const baseArgs = [
|
|
322
422
|
url,
|
|
323
|
-
"-f",
|
|
324
|
-
"bestaudio/best",
|
|
325
|
-
"-x",
|
|
326
|
-
"--audio-format",
|
|
327
|
-
options.format,
|
|
328
|
-
"--audio-quality",
|
|
329
|
-
DEFAULT_AUDIO_QUALITY,
|
|
330
423
|
"-o",
|
|
331
424
|
getOutputTemplate(downloadDir),
|
|
332
425
|
"--no-playlist",
|
|
@@ -334,68 +427,39 @@ function buildArgs(url, options, downloadDir) {
|
|
|
334
427
|
"--progress",
|
|
335
428
|
"--concurrent-fragments",
|
|
336
429
|
CONCURRENT_FRAGMENTS,
|
|
337
|
-
"--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,
|
|
338
445
|
"--prefer-free-formats"
|
|
339
446
|
];
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
conditionalArgs.push("--download-sections", `*${parseClipRange(options.clip)}`);
|
|
343
|
-
}
|
|
344
|
-
if (ffmpegInstaller?.path) {
|
|
345
|
-
conditionalArgs.push("--ffmpeg-location", ffmpegInstaller.path);
|
|
346
|
-
}
|
|
347
|
-
return [...baseArgs, ...conditionalArgs];
|
|
447
|
+
const clipArgs = options.clip ? ["--download-sections", `*${parseClipRange(options.clip)}`] : [];
|
|
448
|
+
return [...baseArgs, ...formatArgs, ...clipArgs];
|
|
348
449
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const text = data.toString();
|
|
352
|
-
const fileName = extractFileName(text);
|
|
353
|
-
if (fileName)
|
|
354
|
-
state.fileName = fileName;
|
|
355
|
-
const fileSize = extractFileSize(text);
|
|
356
|
-
if (fileSize)
|
|
357
|
-
state.fileSize = fileSize;
|
|
358
|
-
if (state.phase !== "converting" && isConvertingPhase(text)) {
|
|
359
|
-
state.phase = "converting";
|
|
360
|
-
spinner.stop();
|
|
361
|
-
clearLine();
|
|
362
|
-
if (!state.quiet) {
|
|
363
|
-
let frame = 0;
|
|
364
|
-
state.convertingInterval = setInterval(() => {
|
|
365
|
-
const frameChar = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "⠋";
|
|
366
|
-
write(`\r${c.info(frameChar)} ${c.dim("Converting...")}`);
|
|
367
|
-
}, SPINNER_INTERVAL_MS);
|
|
368
|
-
}
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const progress = parseProgress(text);
|
|
372
|
-
if (progress && progress.percent > state.lastPercent) {
|
|
373
|
-
if (state.phase === "init") {
|
|
374
|
-
state.phase = "downloading";
|
|
375
|
-
spinner.stop();
|
|
376
|
-
}
|
|
377
|
-
state.lastPercent = progress.percent;
|
|
378
|
-
renderProgress(progress, state.quiet);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (state.phase === "init") {
|
|
382
|
-
const message = detectPhase(text);
|
|
383
|
-
if (message)
|
|
384
|
-
spinner.update(message);
|
|
385
|
-
}
|
|
386
|
-
};
|
|
450
|
+
|
|
451
|
+
class DownloadService extends Context3.Tag("DownloadService")() {
|
|
387
452
|
}
|
|
388
|
-
function
|
|
389
|
-
return
|
|
453
|
+
function executeDownload(ytDlpWrap, args, downloadDir, url, quiet) {
|
|
454
|
+
return Effect6.async((resume) => {
|
|
455
|
+
const spinner = createSpinner("Getting ready...", quiet);
|
|
390
456
|
const state = {
|
|
391
457
|
phase: "init",
|
|
392
|
-
lastPercent: 0,
|
|
393
458
|
fileName: null,
|
|
394
459
|
fileSize: null,
|
|
395
|
-
|
|
460
|
+
outputBuffer: "",
|
|
396
461
|
convertingInterval: null
|
|
397
462
|
};
|
|
398
|
-
const handleOutput = createOutputHandler(spinner, state);
|
|
399
463
|
const cleanup = () => {
|
|
400
464
|
spinner.stop();
|
|
401
465
|
if (state.convertingInterval) {
|
|
@@ -404,40 +468,118 @@ function attachProcessHandlers(child, spinner, downloadDir, quiet) {
|
|
|
404
468
|
if (!quiet)
|
|
405
469
|
clearLine();
|
|
406
470
|
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
fileSize: state.fileSize || undefined
|
|
416
|
-
});
|
|
417
|
-
} else {
|
|
418
|
-
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);
|
|
419
479
|
}
|
|
420
480
|
});
|
|
421
|
-
|
|
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) => {
|
|
422
521
|
cleanup();
|
|
423
|
-
|
|
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", () => {
|
|
529
|
+
cleanup();
|
|
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
|
+
}));
|
|
424
545
|
});
|
|
425
546
|
});
|
|
426
547
|
}
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
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
|
+
}));
|
|
440
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";
|
|
441
583
|
// src/banner.ts
|
|
442
584
|
import cfonts from "cfonts";
|
|
443
585
|
import gradient from "gradient-string";
|
|
@@ -463,100 +605,86 @@ function showBanner() {
|
|
|
463
605
|
`));
|
|
464
606
|
}
|
|
465
607
|
|
|
466
|
-
//
|
|
467
|
-
var
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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;
|
|
474
619
|
}
|
|
475
|
-
|
|
476
|
-
|
|
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;
|
|
477
630
|
}
|
|
478
|
-
if (
|
|
479
|
-
|
|
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);
|
|
480
637
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
async function handlePrepare() {
|
|
489
|
-
try {
|
|
490
|
-
if (await findBinary()) {
|
|
491
|
-
console.log(`${c.sym.success} ${c.success("Ready")}`);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
console.log(`${c.dim("Downloading yt-dlp...")}`);
|
|
495
|
-
await downloadLatestBinary();
|
|
496
|
-
console.log(`${c.sym.success} ${c.success("Ready")}`);
|
|
497
|
-
} catch (err) {
|
|
498
|
-
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
|
+
`);
|
|
499
645
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
console.log(`${c.sym.success} ${c.info(folderPath)}`);
|
|
511
|
-
} else {
|
|
512
|
-
console.log(c.info(await getDownloadDir()));
|
|
513
|
-
}
|
|
514
|
-
} catch (err) {
|
|
515
|
-
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}`);
|
|
516
656
|
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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)
|
|
521
664
|
showBanner();
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (opts.quiet) {
|
|
531
|
-
console.log(result.fileName);
|
|
532
|
-
} else {
|
|
533
|
-
console.log();
|
|
534
|
-
console.log(`${c.sym.success} ${c.success("Process done!")}
|
|
535
|
-
`);
|
|
536
|
-
const sizeInfo = result.fileSize ? ` ${c.size(`(${result.fileSize})`)}` : "";
|
|
537
|
-
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);
|
|
538
673
|
}
|
|
539
|
-
}
|
|
540
|
-
|
|
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;
|
|
541
684
|
}
|
|
542
|
-
|
|
543
|
-
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) => {
|
|
544
|
-
if (thisCommand.args.includes("help") || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
685
|
+
if (process.argv.includes("-v") || process.argv.includes("--version")) {
|
|
545
686
|
showBanner();
|
|
546
687
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
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);
|
|
551
|
-
var firstArg = process.argv[2];
|
|
552
|
-
if (firstArg && isUrl(firstArg)) {
|
|
553
|
-
process.argv.splice(2, 0, "download");
|
|
554
|
-
}
|
|
555
|
-
if (process.argv.length <= 2) {
|
|
556
|
-
showBanner();
|
|
557
|
-
program.help();
|
|
558
|
-
}
|
|
559
|
-
if (process.argv.includes("-v") || process.argv.includes("--version")) {
|
|
560
|
-
showBanner();
|
|
561
|
-
}
|
|
562
|
-
program.parse();
|
|
688
|
+
configureCli().parse();
|
|
689
|
+
};
|
|
690
|
+
main();
|