@mkterswingman/5mghost-yonder 0.0.27 → 0.0.29
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/dist/auth/oauthFlow.d.ts +9 -0
- package/dist/auth/oauthFlow.js +151 -0
- package/dist/auth/sharedAuth.d.ts +10 -0
- package/dist/auth/sharedAuth.js +31 -0
- package/dist/auth/tokenManager.d.ts +18 -0
- package/dist/auth/tokenManager.js +92 -0
- package/dist/cli/check.d.ts +4 -0
- package/dist/cli/check.js +90 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.js +166 -0
- package/dist/cli/installSkills.d.ts +1 -0
- package/dist/cli/installSkills.js +39 -0
- package/dist/cli/runtime.d.ts +9 -0
- package/dist/cli/runtime.js +35 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +26 -0
- package/dist/cli/setup.d.ts +33 -0
- package/dist/cli/setup.js +450 -0
- package/dist/cli/setupCookies.d.ts +88 -0
- package/dist/cli/setupCookies.js +440 -0
- package/dist/cli/smoke.d.ts +27 -0
- package/dist/cli/smoke.js +108 -0
- package/dist/cli/uninstall.d.ts +16 -0
- package/dist/cli/uninstall.js +99 -0
- package/dist/download/downloader.d.ts +67 -0
- package/dist/download/downloader.js +309 -0
- package/dist/download/jobManager.d.ts +22 -0
- package/dist/download/jobManager.js +211 -0
- package/dist/download/types.d.ts +44 -0
- package/dist/download/types.js +1 -0
- package/dist/runtime/ffmpegRuntime.d.ts +13 -0
- package/dist/runtime/ffmpegRuntime.js +51 -0
- package/dist/runtime/installers.d.ts +12 -0
- package/dist/runtime/installers.js +45 -0
- package/dist/runtime/manifest.d.ts +18 -0
- package/dist/runtime/manifest.js +43 -0
- package/dist/runtime/playwrightRuntime.d.ts +17 -0
- package/dist/runtime/playwrightRuntime.js +49 -0
- package/dist/runtime/systemDeps.d.ts +3 -0
- package/dist/runtime/systemDeps.js +30 -0
- package/dist/runtime/ytdlpRuntime.d.ts +14 -0
- package/dist/runtime/ytdlpRuntime.js +58 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.js +81 -0
- package/dist/tools/downloads.d.ts +11 -0
- package/dist/tools/downloads.js +220 -0
- package/dist/tools/remote.d.ts +4 -0
- package/dist/tools/remote.js +239 -0
- package/dist/tools/subtitles.d.ts +29 -0
- package/dist/tools/subtitles.js +713 -0
- package/dist/utils/browserLaunch.d.ts +5 -0
- package/dist/utils/browserLaunch.js +22 -0
- package/dist/utils/browserProfileImport.d.ts +49 -0
- package/dist/utils/browserProfileImport.js +163 -0
- package/dist/utils/codexInternal.d.ts +9 -0
- package/dist/utils/codexInternal.js +60 -0
- package/dist/utils/config.d.ts +53 -0
- package/dist/utils/config.js +77 -0
- package/dist/utils/cookieRefresh.d.ts +18 -0
- package/dist/utils/cookieRefresh.js +70 -0
- package/dist/utils/cookies.d.ts +18 -0
- package/dist/utils/cookies.js +100 -0
- package/dist/utils/ffmpeg.d.ts +5 -0
- package/dist/utils/ffmpeg.js +16 -0
- package/dist/utils/ffmpegPath.d.ts +8 -0
- package/dist/utils/ffmpegPath.js +21 -0
- package/dist/utils/formatters.d.ts +4 -0
- package/dist/utils/formatters.js +42 -0
- package/dist/utils/launcher.d.ts +12 -0
- package/dist/utils/launcher.js +90 -0
- package/dist/utils/mcpRegistration.d.ts +7 -0
- package/dist/utils/mcpRegistration.js +23 -0
- package/dist/utils/mediaPaths.d.ts +7 -0
- package/dist/utils/mediaPaths.js +10 -0
- package/dist/utils/openClaw.d.ts +18 -0
- package/dist/utils/openClaw.js +82 -0
- package/dist/utils/skills.d.ts +16 -0
- package/dist/utils/skills.js +56 -0
- package/dist/utils/videoInput.d.ts +5 -0
- package/dist/utils/videoInput.js +58 -0
- package/dist/utils/videoMetadata.d.ts +11 -0
- package/dist/utils/videoMetadata.js +1 -0
- package/dist/utils/ytdlp.d.ts +28 -0
- package/dist/utils/ytdlp.js +205 -0
- package/dist/utils/ytdlpFailures.d.ts +3 -0
- package/dist/utils/ytdlpFailures.js +27 -0
- package/dist/utils/ytdlpPath.d.ts +33 -0
- package/dist/utils/ytdlpPath.js +77 -0
- package/dist/utils/ytdlpProgress.d.ts +13 -0
- package/dist/utils/ytdlpProgress.js +77 -0
- package/dist/utils/ytdlpScheduler.d.ts +25 -0
- package/dist/utils/ytdlpScheduler.js +78 -0
- package/package.json +2 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { PATHS } from "./config.js";
|
|
6
|
+
import { getYtDlpPath } from "./ytdlpPath.js";
|
|
7
|
+
import { markGlobalYtDlpRateLimited, scheduleYtDlp } from "./ytdlpScheduler.js";
|
|
8
|
+
import { classifyYtDlpFailure } from "./ytdlpFailures.js";
|
|
9
|
+
function writeYtDlpFailureLog(payload) {
|
|
10
|
+
try {
|
|
11
|
+
mkdirSync(PATHS.logsDir, { recursive: true });
|
|
12
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
13
|
+
const filePath = join(PATHS.logsDir, `yt-dlp-failure-${stamp}.json`);
|
|
14
|
+
writeFileSync(filePath, JSON.stringify({
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
binary: getYtDlpPath(),
|
|
17
|
+
...payload,
|
|
18
|
+
}, null, 2), "utf8");
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function createYtDlpStderrLineSplitter(onLine) {
|
|
26
|
+
let buffer = "";
|
|
27
|
+
let pendingCarriageReturn = false;
|
|
28
|
+
const emit = () => {
|
|
29
|
+
if (buffer.length === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
onLine(buffer);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Why: progress observers are best-effort and must not break downloads.
|
|
37
|
+
}
|
|
38
|
+
buffer = "";
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
push(chunk) {
|
|
42
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
43
|
+
for (const ch of text) {
|
|
44
|
+
if (pendingCarriageReturn) {
|
|
45
|
+
if (ch === "\n") {
|
|
46
|
+
pendingCarriageReturn = false;
|
|
47
|
+
emit();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
pendingCarriageReturn = false;
|
|
51
|
+
emit();
|
|
52
|
+
}
|
|
53
|
+
if (ch === "\r") {
|
|
54
|
+
pendingCarriageReturn = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === "\n") {
|
|
58
|
+
emit();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
buffer += ch;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
flush() {
|
|
65
|
+
if (pendingCarriageReturn) {
|
|
66
|
+
pendingCarriageReturn = false;
|
|
67
|
+
}
|
|
68
|
+
emit();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function hasYtDlp(runSpawnSync = spawnSync) {
|
|
73
|
+
const result = runSpawnSync(getYtDlpPath(), ["--version"], {
|
|
74
|
+
stdio: "ignore",
|
|
75
|
+
timeout: 30_000,
|
|
76
|
+
});
|
|
77
|
+
return result.status === 0 && result.error == null;
|
|
78
|
+
}
|
|
79
|
+
export function buildYtDlpArgs(args, options = {}) {
|
|
80
|
+
const cookiesPath = options.cookiesPath ?? PATHS.cookiesTxt;
|
|
81
|
+
const cookiesExist = options.cookiesExist ?? existsSync(cookiesPath);
|
|
82
|
+
const finalArgs = ["--force-ipv4", "--no-warnings", "--js-runtimes", "node", ...args];
|
|
83
|
+
if (cookiesExist) {
|
|
84
|
+
finalArgs.push("--cookies", cookiesPath);
|
|
85
|
+
}
|
|
86
|
+
return finalArgs;
|
|
87
|
+
}
|
|
88
|
+
export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
89
|
+
return scheduleYtDlp(() => new Promise((resolve, reject) => {
|
|
90
|
+
const start = Date.now();
|
|
91
|
+
const finalArgs = buildYtDlpArgs(args);
|
|
92
|
+
const proc = spawn(getYtDlpPath(), finalArgs, {
|
|
93
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
94
|
+
});
|
|
95
|
+
const stdoutChunks = [];
|
|
96
|
+
const stderrChunks = [];
|
|
97
|
+
const stderrSplitter = onStderrLine
|
|
98
|
+
? createYtDlpStderrLineSplitter(onStderrLine)
|
|
99
|
+
: null;
|
|
100
|
+
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
101
|
+
proc.stderr.on("data", (chunk) => {
|
|
102
|
+
stderrChunks.push(chunk);
|
|
103
|
+
stderrSplitter?.push(chunk);
|
|
104
|
+
});
|
|
105
|
+
function flushStderrLineBuffer() {
|
|
106
|
+
stderrSplitter?.flush();
|
|
107
|
+
}
|
|
108
|
+
let settled = false;
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
if (!settled) {
|
|
111
|
+
settled = true;
|
|
112
|
+
proc.kill("SIGKILL");
|
|
113
|
+
flushStderrLineBuffer();
|
|
114
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
115
|
+
const stderr = `yt-dlp timed out after ${timeoutMs}ms`;
|
|
116
|
+
resolve({
|
|
117
|
+
exitCode: -1,
|
|
118
|
+
stdout,
|
|
119
|
+
stderr,
|
|
120
|
+
durationMs: Date.now() - start,
|
|
121
|
+
failureLogPath: writeYtDlpFailureLog({
|
|
122
|
+
args: finalArgs,
|
|
123
|
+
exitCode: -1,
|
|
124
|
+
stdout,
|
|
125
|
+
stderr,
|
|
126
|
+
durationMs: Date.now() - start,
|
|
127
|
+
timeoutMs,
|
|
128
|
+
trigger: "timeout",
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}, timeoutMs);
|
|
133
|
+
proc.on("close", (code) => {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
if (!settled) {
|
|
136
|
+
settled = true;
|
|
137
|
+
flushStderrLineBuffer();
|
|
138
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
139
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
140
|
+
const exitCode = code ?? 1;
|
|
141
|
+
resolve({
|
|
142
|
+
exitCode,
|
|
143
|
+
stdout,
|
|
144
|
+
stderr,
|
|
145
|
+
durationMs: Date.now() - start,
|
|
146
|
+
failureLogPath: exitCode === 0
|
|
147
|
+
? undefined
|
|
148
|
+
: writeYtDlpFailureLog({
|
|
149
|
+
args: finalArgs,
|
|
150
|
+
exitCode,
|
|
151
|
+
stdout,
|
|
152
|
+
stderr,
|
|
153
|
+
durationMs: Date.now() - start,
|
|
154
|
+
timeoutMs,
|
|
155
|
+
trigger: "exit",
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
if (exitCode !== 0 && classifyYtDlpFailure(stderr) === "RATE_LIMITED") {
|
|
159
|
+
markGlobalYtDlpRateLimited();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
proc.on("error", (err) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
if (!settled) {
|
|
166
|
+
settled = true;
|
|
167
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
168
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
169
|
+
reject(Object.assign(err, {
|
|
170
|
+
failureLogPath: writeYtDlpFailureLog({
|
|
171
|
+
args: finalArgs,
|
|
172
|
+
exitCode: 1,
|
|
173
|
+
stdout,
|
|
174
|
+
stderr,
|
|
175
|
+
durationMs: Date.now() - start,
|
|
176
|
+
timeoutMs,
|
|
177
|
+
trigger: "spawn_error",
|
|
178
|
+
spawnError: err instanceof Error ? err.message : String(err),
|
|
179
|
+
}),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
export async function runYtDlpJson(args, timeoutMs = 45_000) {
|
|
186
|
+
const result = await runYtDlp(args, timeoutMs);
|
|
187
|
+
if (result.exitCode !== 0) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
value: JSON.parse(result.stdout),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: "yt-dlp returned invalid JSON metadata.",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type YtDlpFailureKind = "COOKIES_EXPIRED" | "COOKIES_INVALID" | "SIGN_IN_REQUIRED" | "RATE_LIMITED";
|
|
2
|
+
export declare function classifyYtDlpFailure(stderr: string): YtDlpFailureKind | null;
|
|
3
|
+
export declare function appendDiagnosticLog(message: string, logPath?: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function classifyYtDlpFailure(stderr) {
|
|
2
|
+
const normalized = stderr.toLowerCase();
|
|
3
|
+
if (normalized.includes("cookies are no longer valid") ||
|
|
4
|
+
normalized.includes("cookie has expired") ||
|
|
5
|
+
normalized.includes("cookies have expired") ||
|
|
6
|
+
normalized.includes("session expired")) {
|
|
7
|
+
return "COOKIES_EXPIRED";
|
|
8
|
+
}
|
|
9
|
+
if (normalized.includes("rate-limited by youtube") ||
|
|
10
|
+
normalized.includes("current session has been rate-limited")) {
|
|
11
|
+
return "RATE_LIMITED";
|
|
12
|
+
}
|
|
13
|
+
if (normalized.includes("sign in to confirm you're not a bot") ||
|
|
14
|
+
normalized.includes("sign in to confirm you’re not a bot")) {
|
|
15
|
+
return "SIGN_IN_REQUIRED";
|
|
16
|
+
}
|
|
17
|
+
if (normalized.includes("sign in") ||
|
|
18
|
+
normalized.includes("login") ||
|
|
19
|
+
normalized.includes("cookies") ||
|
|
20
|
+
normalized.includes("not a bot")) {
|
|
21
|
+
return "COOKIES_INVALID";
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export function appendDiagnosticLog(message, logPath) {
|
|
26
|
+
return logPath ? `${message}\nDiagnostic log: ${logPath}` : message;
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the yt-dlp binary path.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 1. YT_DLP_PATH env var (explicit override)
|
|
6
|
+
* 2. Runtime-managed binary at ~/.yt-mcp/runtime/bin/yt-dlp
|
|
7
|
+
* 3. "yt-dlp" — fall back to system PATH
|
|
8
|
+
*/
|
|
9
|
+
export interface ResolveYtDlpPathOptions {
|
|
10
|
+
envPath?: string | null;
|
|
11
|
+
runtimePath?: string;
|
|
12
|
+
runtimeExists?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function getRuntimeYtDlpPath(runtimeBinDir?: string): string;
|
|
15
|
+
export declare function resolveYtDlpPath(options?: ResolveYtDlpPathOptions): string;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the absolute path to the yt-dlp binary, or the bare command name
|
|
18
|
+
* "yt-dlp" if only available on system PATH.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getYtDlpPath(): string;
|
|
21
|
+
export interface YtDlpVersionInfo {
|
|
22
|
+
version: string;
|
|
23
|
+
source: "runtime" | "system" | "env";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the version string and source of the resolved yt-dlp binary.
|
|
27
|
+
* Returns null if yt-dlp is not available at all.
|
|
28
|
+
*
|
|
29
|
+
* Uses a 30s timeout because macOS Gatekeeper performs a network verification
|
|
30
|
+
* on first run of ad-hoc signed binaries (~15-20s). The result is cached by
|
|
31
|
+
* the OS, so subsequent calls return instantly.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getYtDlpVersion(binPath?: string): YtDlpVersionInfo | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the yt-dlp binary path.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 1. YT_DLP_PATH env var (explicit override)
|
|
6
|
+
* 2. Runtime-managed binary at ~/.yt-mcp/runtime/bin/yt-dlp
|
|
7
|
+
* 3. "yt-dlp" — fall back to system PATH
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { PATHS, buildRuntimeBinaryPath } from "./config.js";
|
|
12
|
+
export function getRuntimeYtDlpPath(runtimeBinDir = PATHS.runtimeBinDir) {
|
|
13
|
+
const name = process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
|
|
14
|
+
return buildRuntimeBinaryPath(runtimeBinDir, name);
|
|
15
|
+
}
|
|
16
|
+
export function resolveYtDlpPath(options = {}) {
|
|
17
|
+
const envPath = options.envPath ?? process.env.YT_DLP_PATH ?? null;
|
|
18
|
+
if (envPath) {
|
|
19
|
+
return envPath;
|
|
20
|
+
}
|
|
21
|
+
const runtimePath = options.runtimePath ?? getRuntimeYtDlpPath();
|
|
22
|
+
const runtimeExists = options.runtimeExists ?? existsSync(runtimePath);
|
|
23
|
+
if (runtimeExists) {
|
|
24
|
+
return runtimePath;
|
|
25
|
+
}
|
|
26
|
+
return "yt-dlp";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the absolute path to the yt-dlp binary, or the bare command name
|
|
30
|
+
* "yt-dlp" if only available on system PATH.
|
|
31
|
+
*/
|
|
32
|
+
export function getYtDlpPath() {
|
|
33
|
+
return resolveYtDlpPath();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the version string and source of the resolved yt-dlp binary.
|
|
37
|
+
* Returns null if yt-dlp is not available at all.
|
|
38
|
+
*
|
|
39
|
+
* Uses a 30s timeout because macOS Gatekeeper performs a network verification
|
|
40
|
+
* on first run of ad-hoc signed binaries (~15-20s). The result is cached by
|
|
41
|
+
* the OS, so subsequent calls return instantly.
|
|
42
|
+
*/
|
|
43
|
+
export function getYtDlpVersion(binPath) {
|
|
44
|
+
const resolved = binPath ?? getYtDlpPath();
|
|
45
|
+
let source;
|
|
46
|
+
if (process.env.YT_DLP_PATH && resolved === process.env.YT_DLP_PATH) {
|
|
47
|
+
source = "env";
|
|
48
|
+
}
|
|
49
|
+
else if (resolved === getRuntimeYtDlpPath()) {
|
|
50
|
+
source = "runtime";
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
source = "system";
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
// macOS Gatekeeper: try clearing quarantine xattr before first execution.
|
|
57
|
+
// This helps in some environments; in others the OS re-applies it.
|
|
58
|
+
if (process.platform === "darwin" && resolved !== "yt-dlp") {
|
|
59
|
+
try {
|
|
60
|
+
execFileSync("xattr", ["-cr", resolved], { stdio: "ignore" });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// non-fatal
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const ver = execFileSync(resolved, ["--version"], {
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
+
timeout: 30_000,
|
|
69
|
+
})
|
|
70
|
+
.toString()
|
|
71
|
+
.trim();
|
|
72
|
+
return { version: ver, source };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ParsedYtDlpProgress {
|
|
2
|
+
progress: string;
|
|
3
|
+
downloaded_size?: string;
|
|
4
|
+
total_size?: string;
|
|
5
|
+
speed?: string;
|
|
6
|
+
eta?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse the canonical yt-dlp download progress line used by this integration.
|
|
10
|
+
* Narrow contract: intentionally supports only representative `[download] ...`
|
|
11
|
+
* lines with percent, total size, speed, and ETA fields.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseYtDlpProgressLine(line: string): ParsedYtDlpProgress | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { formatBytes, formatEta, formatPercent, formatSpeed } from "./formatters.js";
|
|
2
|
+
const SIZE_UNITS = {
|
|
3
|
+
b: 1,
|
|
4
|
+
kb: 1000,
|
|
5
|
+
kib: 1024,
|
|
6
|
+
mb: 1000 ** 2,
|
|
7
|
+
mib: 1024 ** 2,
|
|
8
|
+
gb: 1000 ** 3,
|
|
9
|
+
gib: 1024 ** 3,
|
|
10
|
+
tb: 1000 ** 4,
|
|
11
|
+
tib: 1024 ** 4,
|
|
12
|
+
};
|
|
13
|
+
function parseSizeToBytes(text) {
|
|
14
|
+
const match = text.trim().match(/^(\d+(?:\.\d+)?)([kmgt]?i?b)$/i);
|
|
15
|
+
if (!match) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const value = Number(match[1]);
|
|
19
|
+
const unit = match[2].toLowerCase();
|
|
20
|
+
const multiplier = SIZE_UNITS[unit];
|
|
21
|
+
if (!Number.isFinite(value) || multiplier == null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return value * multiplier;
|
|
25
|
+
}
|
|
26
|
+
function parseEtaToSeconds(text) {
|
|
27
|
+
const trimmed = text.trim();
|
|
28
|
+
if (!trimmed || trimmed === "N/A") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const hms = trimmed.match(/^(\d+):(\d{2}):(\d{2})$/);
|
|
32
|
+
if (hms) {
|
|
33
|
+
return Number(hms[1]) * 3600 + Number(hms[2]) * 60 + Number(hms[3]);
|
|
34
|
+
}
|
|
35
|
+
const ms = trimmed.match(/^(\d{1,2}):(\d{2})$/);
|
|
36
|
+
if (ms) {
|
|
37
|
+
return Number(ms[1]) * 60 + Number(ms[2]);
|
|
38
|
+
}
|
|
39
|
+
const unitMatch = trimmed.match(/^(\d+(?:\.\d+)?)([smhd])$/i);
|
|
40
|
+
if (unitMatch) {
|
|
41
|
+
const value = Number(unitMatch[1]);
|
|
42
|
+
const unit = unitMatch[2].toLowerCase();
|
|
43
|
+
const multiplier = unit === "s" ? 1 : unit === "m" ? 60 : unit === "h" ? 3600 : 86400;
|
|
44
|
+
return value * multiplier;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse the canonical yt-dlp download progress line used by this integration.
|
|
50
|
+
* Narrow contract: intentionally supports only representative `[download] ...`
|
|
51
|
+
* lines with percent, total size, speed, and ETA fields.
|
|
52
|
+
*/
|
|
53
|
+
export function parseYtDlpProgressLine(line) {
|
|
54
|
+
const match = line.match(/^\[download\]\s+(?<progress>\d+(?:\.\d+)?)% of (?<total>\S+)(?: at (?<speed>\S+?\/s))?(?: ETA (?<eta>\S+))?(?: \((?<details>.+)\))?$/);
|
|
55
|
+
if (!match?.groups?.progress) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const progress = formatPercent(Number(match.groups.progress));
|
|
59
|
+
const totalBytes = match.groups.total ? parseSizeToBytes(match.groups.total) : null;
|
|
60
|
+
const speedBytes = match.groups.speed ? parseSizeToBytes(match.groups.speed.replace(/\/s$/i, "")) : null;
|
|
61
|
+
const etaSeconds = match.groups.eta ? parseEtaToSeconds(match.groups.eta) : null;
|
|
62
|
+
const parsed = { progress };
|
|
63
|
+
if (totalBytes != null) {
|
|
64
|
+
parsed.total_size = formatBytes(totalBytes);
|
|
65
|
+
}
|
|
66
|
+
if (speedBytes != null) {
|
|
67
|
+
parsed.speed = formatSpeed(speedBytes);
|
|
68
|
+
}
|
|
69
|
+
if (etaSeconds != null) {
|
|
70
|
+
parsed.eta = formatEta(etaSeconds);
|
|
71
|
+
}
|
|
72
|
+
if (totalBytes != null && Number.isFinite(Number(match.groups.progress))) {
|
|
73
|
+
const downloadedBytes = (totalBytes * Number(match.groups.progress)) / 100;
|
|
74
|
+
parsed.downloaded_size = formatBytes(downloadedBytes);
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface YtDlpSchedulerState {
|
|
2
|
+
running: boolean;
|
|
3
|
+
pendingCount: number;
|
|
4
|
+
nextAvailableAt: number | null;
|
|
5
|
+
wouldThrottle: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface YtDlpSchedulerOptions {
|
|
8
|
+
minIntervalMs?: number;
|
|
9
|
+
maxIntervalMs?: number;
|
|
10
|
+
rateLimitBackoffMs?: number;
|
|
11
|
+
nextIntervalMs?: () => number;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface YtDlpScheduler {
|
|
16
|
+
schedule<T>(task: () => Promise<T>): Promise<T>;
|
|
17
|
+
markRateLimited(): void;
|
|
18
|
+
getState(): YtDlpSchedulerState;
|
|
19
|
+
sampleNextIntervalMs(): number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createYtDlpScheduler(options?: YtDlpSchedulerOptions): YtDlpScheduler;
|
|
22
|
+
export declare function scheduleYtDlp<T>(task: () => Promise<T>): Promise<T>;
|
|
23
|
+
export declare function markGlobalYtDlpRateLimited(): void;
|
|
24
|
+
export declare function getGlobalYtDlpSchedulerState(): YtDlpSchedulerState;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const DEFAULT_MIN_INTERVAL_MS = 3_000;
|
|
2
|
+
const DEFAULT_MAX_INTERVAL_MS = 7_000;
|
|
3
|
+
const DEFAULT_RATE_LIMIT_BACKOFF_MS = 10 * 60_000;
|
|
4
|
+
export function createYtDlpScheduler(options = {}) {
|
|
5
|
+
const now = options.now ?? Date.now;
|
|
6
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
7
|
+
const minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
8
|
+
const maxIntervalMs = options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS;
|
|
9
|
+
const rateLimitBackoffMs = options.rateLimitBackoffMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS;
|
|
10
|
+
const sampleNextIntervalMs = options.nextIntervalMs ??
|
|
11
|
+
(() => {
|
|
12
|
+
if (maxIntervalMs <= minIntervalMs) {
|
|
13
|
+
return minIntervalMs;
|
|
14
|
+
}
|
|
15
|
+
return minIntervalMs + Math.floor(Math.random() * (maxIntervalMs - minIntervalMs + 1));
|
|
16
|
+
});
|
|
17
|
+
let running = false;
|
|
18
|
+
let pendingCount = 0;
|
|
19
|
+
let nextAvailableAt = null;
|
|
20
|
+
let tail = Promise.resolve();
|
|
21
|
+
async function waitForAvailability() {
|
|
22
|
+
const currentNext = nextAvailableAt;
|
|
23
|
+
if (currentNext == null) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const delayMs = currentNext - now();
|
|
27
|
+
if (delayMs > 0) {
|
|
28
|
+
await sleep(delayMs);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
async schedule(task) {
|
|
33
|
+
pendingCount += 1;
|
|
34
|
+
const previousTail = tail;
|
|
35
|
+
let release;
|
|
36
|
+
tail = new Promise((resolve) => {
|
|
37
|
+
release = resolve;
|
|
38
|
+
});
|
|
39
|
+
await previousTail;
|
|
40
|
+
pendingCount -= 1;
|
|
41
|
+
await waitForAvailability();
|
|
42
|
+
running = true;
|
|
43
|
+
try {
|
|
44
|
+
return await task();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
running = false;
|
|
48
|
+
nextAvailableAt = Math.max(nextAvailableAt ?? 0, now() + sampleNextIntervalMs());
|
|
49
|
+
release();
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
markRateLimited() {
|
|
53
|
+
nextAvailableAt = Math.max(nextAvailableAt ?? 0, now() + rateLimitBackoffMs);
|
|
54
|
+
},
|
|
55
|
+
getState() {
|
|
56
|
+
const next = nextAvailableAt;
|
|
57
|
+
return {
|
|
58
|
+
running,
|
|
59
|
+
pendingCount,
|
|
60
|
+
nextAvailableAt: next,
|
|
61
|
+
wouldThrottle: running || pendingCount > 0 || (next != null && next > now()),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
sampleNextIntervalMs() {
|
|
65
|
+
return sampleNextIntervalMs();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const globalYtDlpScheduler = createYtDlpScheduler();
|
|
70
|
+
export function scheduleYtDlp(task) {
|
|
71
|
+
return globalYtDlpScheduler.schedule(task);
|
|
72
|
+
}
|
|
73
|
+
export function markGlobalYtDlpRateLimited() {
|
|
74
|
+
globalYtDlpScheduler.markRateLimited();
|
|
75
|
+
}
|
|
76
|
+
export function getGlobalYtDlpSchedulerState() {
|
|
77
|
+
return globalYtDlpScheduler.getState();
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mkterswingman/5mghost-yonder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "Internal MCP client with local data tools and remote API proxy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc -p tsconfig.json",
|
|
14
14
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
15
16
|
"dev": "tsx src/cli/index.ts",
|
|
16
17
|
"start": "node dist/cli/index.js"
|
|
17
18
|
},
|