@mkterswingman/5mghost-yonder 0.0.11 → 0.0.12
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/tools/subtitles.js +53 -21
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +1 -0
- package/dist/utils/cookies.js +32 -10
- package/dist/utils/ytdlp.d.ts +1 -0
- package/dist/utils/ytdlp.js +62 -7
- package/package.json +1 -1
package/dist/tools/subtitles.js
CHANGED
|
@@ -10,7 +10,8 @@ const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkte
|
|
|
10
10
|
const COOKIE_SETUP_COMMAND = "yt-mcp setup-cookies";
|
|
11
11
|
const COOKIE_MISSING_MESSAGE = `No valid YouTube cookies found.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
|
|
12
12
|
const COOKIE_EXPIRED_MESSAGE = `YouTube cookies have expired and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
|
|
13
|
-
const
|
|
13
|
+
const COOKIE_INVALID_MESSAGE = `YouTube rejected the current cookies and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
|
|
14
|
+
const COOKIE_JOB_MESSAGE = `YouTube cookies are missing, expired, or invalid.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
|
|
14
15
|
function toolOk(payload) {
|
|
15
16
|
return {
|
|
16
17
|
structuredContent: payload,
|
|
@@ -55,12 +56,39 @@ function isCookieFailureText(error) {
|
|
|
55
56
|
normalized.includes("login") ||
|
|
56
57
|
normalized.includes("not a bot"));
|
|
57
58
|
}
|
|
59
|
+
function appendDiagnosticLog(message, logPath) {
|
|
60
|
+
return logPath ? `${message}\nDiagnostic log: ${logPath}` : message;
|
|
61
|
+
}
|
|
62
|
+
function classifyYtDlpCookieFailure(stderr) {
|
|
63
|
+
const normalized = stderr.toLowerCase();
|
|
64
|
+
if (normalized.includes("cookies are no longer valid") ||
|
|
65
|
+
normalized.includes("cookie has expired") ||
|
|
66
|
+
normalized.includes("cookies have expired") ||
|
|
67
|
+
normalized.includes("session expired")) {
|
|
68
|
+
return {
|
|
69
|
+
code: "COOKIES_EXPIRED",
|
|
70
|
+
message: COOKIE_EXPIRED_MESSAGE,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (normalized.includes("sign in") ||
|
|
74
|
+
normalized.includes("login") ||
|
|
75
|
+
normalized.includes("cookies") ||
|
|
76
|
+
normalized.includes("not a bot")) {
|
|
77
|
+
return {
|
|
78
|
+
code: "COOKIES_INVALID",
|
|
79
|
+
message: COOKIE_INVALID_MESSAGE,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
58
84
|
export function toReadableSubtitleJobError(error) {
|
|
59
85
|
const message = error instanceof Error ? error.message : String(error);
|
|
60
86
|
return isCookieFailureText(message) ? COOKIE_JOB_MESSAGE : message;
|
|
61
87
|
}
|
|
62
88
|
async function ensureSubtitleCookiesReady() {
|
|
63
|
-
|
|
89
|
+
const hasSession = hasSIDCookies(PATHS.cookiesTxt);
|
|
90
|
+
const expired = areCookiesExpired(PATHS.cookiesTxt);
|
|
91
|
+
if (hasSession && !expired) {
|
|
64
92
|
return { ok: true };
|
|
65
93
|
}
|
|
66
94
|
const refreshed = await tryRefreshSubtitleCookies();
|
|
@@ -69,9 +97,9 @@ async function ensureSubtitleCookiesReady() {
|
|
|
69
97
|
}
|
|
70
98
|
return {
|
|
71
99
|
ok: false,
|
|
72
|
-
code: "COOKIES_MISSING",
|
|
73
|
-
toolMessage: COOKIE_MISSING_MESSAGE,
|
|
74
|
-
jobMessage: COOKIE_JOB_MESSAGE,
|
|
100
|
+
code: hasSession && expired ? "COOKIES_EXPIRED" : "COOKIES_MISSING",
|
|
101
|
+
toolMessage: hasSession && expired ? COOKIE_EXPIRED_MESSAGE : COOKIE_MISSING_MESSAGE,
|
|
102
|
+
jobMessage: appendDiagnosticLog(COOKIE_JOB_MESSAGE, undefined),
|
|
75
103
|
};
|
|
76
104
|
}
|
|
77
105
|
/**
|
|
@@ -244,16 +272,20 @@ async function downloadSubtitle(videoId, lang, format, options = {}) {
|
|
|
244
272
|
outTemplate,
|
|
245
273
|
options.sourceUrl ?? `https://www.youtube.com/watch?v=${videoId}`,
|
|
246
274
|
]);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
275
|
+
const cookieFailure = result.exitCode !== 0 ? classifyYtDlpCookieFailure(result.stderr) : null;
|
|
276
|
+
if (cookieFailure) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
cookieFailureCode: cookieFailure.code,
|
|
280
|
+
diagnosticLogPath: result.failureLogPath,
|
|
281
|
+
error: appendDiagnosticLog(cookieFailure.message, result.failureLogPath),
|
|
282
|
+
};
|
|
252
283
|
}
|
|
253
284
|
if (result.exitCode !== 0) {
|
|
254
285
|
return {
|
|
255
286
|
ok: false,
|
|
256
|
-
|
|
287
|
+
diagnosticLogPath: result.failureLogPath,
|
|
288
|
+
error: appendDiagnosticLog(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`, result.failureLogPath),
|
|
257
289
|
};
|
|
258
290
|
}
|
|
259
291
|
// Find the output file - yt-dlp appends lang and format extension.
|
|
@@ -335,7 +367,7 @@ export async function downloadSubtitlesForLanguages(input) {
|
|
|
335
367
|
outputStem: `${lang}`,
|
|
336
368
|
targetFile,
|
|
337
369
|
});
|
|
338
|
-
if (result.
|
|
370
|
+
if (result.cookieFailureCode) {
|
|
339
371
|
const refreshed = await tryRefreshSubtitleCookies();
|
|
340
372
|
if (refreshed) {
|
|
341
373
|
result = await downloadSubtitle(input.videoId, lang, format, {
|
|
@@ -419,7 +451,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
|
|
|
419
451
|
}
|
|
420
452
|
const lang = langs[i];
|
|
421
453
|
const dl = await downloadSubtitle(videoId, lang, fmt);
|
|
422
|
-
if (dl.
|
|
454
|
+
if (dl.cookieFailureCode) {
|
|
423
455
|
const refreshed = await tryRefreshSubtitleCookies();
|
|
424
456
|
if (refreshed) {
|
|
425
457
|
// Retry the download with fresh cookies
|
|
@@ -435,7 +467,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
|
|
|
435
467
|
}
|
|
436
468
|
// Retry also failed with expired cookies — give up
|
|
437
469
|
}
|
|
438
|
-
return toolErr("COOKIES_EXPIRED"
|
|
470
|
+
return toolErr(dl.cookieFailureCode, dl.error ?? (dl.cookieFailureCode === "COOKIES_EXPIRED" ? COOKIE_EXPIRED_MESSAGE : COOKIE_INVALID_MESSAGE));
|
|
439
471
|
}
|
|
440
472
|
if (!dl.ok) {
|
|
441
473
|
// When using default languages, silently skip unavailable ones
|
|
@@ -505,14 +537,14 @@ export function registerSubtitleTools(server, config, tokenManager) {
|
|
|
505
537
|
const videoId = resolvedIds[v];
|
|
506
538
|
const langFound = [];
|
|
507
539
|
let lastError;
|
|
508
|
-
let
|
|
540
|
+
let cookieFailureCode = null;
|
|
509
541
|
for (let l = 0; l < langs.length; l++) {
|
|
510
542
|
if (l > 0) {
|
|
511
543
|
await randomSleep(2000, 5000);
|
|
512
544
|
}
|
|
513
545
|
const dl = await downloadSubtitle(videoId, langs[l], fmt);
|
|
514
|
-
if (dl.
|
|
515
|
-
|
|
546
|
+
if (dl.cookieFailureCode) {
|
|
547
|
+
cookieFailureCode = dl.cookieFailureCode;
|
|
516
548
|
break;
|
|
517
549
|
}
|
|
518
550
|
if (dl.ok) {
|
|
@@ -522,7 +554,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
|
|
|
522
554
|
lastError = dl.error;
|
|
523
555
|
}
|
|
524
556
|
}
|
|
525
|
-
if (
|
|
557
|
+
if (cookieFailureCode) {
|
|
526
558
|
const refreshed = await tryRefreshSubtitleCookies();
|
|
527
559
|
if (refreshed) {
|
|
528
560
|
// Retry this video's subtitles
|
|
@@ -535,11 +567,11 @@ export function registerSubtitleTools(server, config, tokenManager) {
|
|
|
535
567
|
}
|
|
536
568
|
}
|
|
537
569
|
if (retrySuccess) {
|
|
538
|
-
|
|
570
|
+
cookieFailureCode = null;
|
|
539
571
|
}
|
|
540
572
|
}
|
|
541
|
-
if (
|
|
542
|
-
return toolErr("COOKIES_EXPIRED"
|
|
573
|
+
if (cookieFailureCode) {
|
|
574
|
+
return toolErr(cookieFailureCode, cookieFailureCode === "COOKIES_EXPIRED" ? COOKIE_EXPIRED_MESSAGE : COOKIE_INVALID_MESSAGE);
|
|
543
575
|
}
|
|
544
576
|
}
|
|
545
577
|
if (langFound.length > 0) {
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare function buildPaths(homeDir?: string): {
|
|
|
6
6
|
configDir: string;
|
|
7
7
|
authJson: string;
|
|
8
8
|
cookiesTxt: string;
|
|
9
|
+
logsDir: string;
|
|
9
10
|
browserProfile: string;
|
|
10
11
|
launcherJs: string;
|
|
11
12
|
npmCacheDir: string;
|
|
@@ -25,6 +26,7 @@ export declare const PATHS: {
|
|
|
25
26
|
configDir: string;
|
|
26
27
|
authJson: string;
|
|
27
28
|
cookiesTxt: string;
|
|
29
|
+
logsDir: string;
|
|
28
30
|
browserProfile: string;
|
|
29
31
|
launcherJs: string;
|
|
30
32
|
npmCacheDir: string;
|
package/dist/utils/config.js
CHANGED
|
@@ -20,6 +20,7 @@ export function buildPaths(homeDir = homedir()) {
|
|
|
20
20
|
configDir,
|
|
21
21
|
authJson: join(configDir, "auth.json"),
|
|
22
22
|
cookiesTxt: join(configDir, "cookies.txt"),
|
|
23
|
+
logsDir: join(configDir, "logs"),
|
|
23
24
|
browserProfile: join(configDir, "browser-profile"),
|
|
24
25
|
launcherJs: join(configDir, "launcher.mjs"),
|
|
25
26
|
npmCacheDir: join(configDir, "npm-cache"),
|
package/dist/utils/cookies.js
CHANGED
|
@@ -15,12 +15,39 @@ export function cookiesToNetscape(cookies) {
|
|
|
15
15
|
}
|
|
16
16
|
return lines.join("\n") + "\n";
|
|
17
17
|
}
|
|
18
|
+
function parseCookieRows(content) {
|
|
19
|
+
const rows = [];
|
|
20
|
+
for (const line of content.split("\n")) {
|
|
21
|
+
if (line.startsWith("#") || !line.trim())
|
|
22
|
+
continue;
|
|
23
|
+
const fields = line.split("\t");
|
|
24
|
+
if (fields.length < 7)
|
|
25
|
+
continue;
|
|
26
|
+
rows.push({
|
|
27
|
+
domain: fields[0],
|
|
28
|
+
expires: Number(fields[4]),
|
|
29
|
+
name: fields[5],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
function hasCookieTriplet(rows, domainFragment) {
|
|
35
|
+
const names = new Set(rows
|
|
36
|
+
.filter((row) => row.domain.includes(domainFragment))
|
|
37
|
+
.map((row) => row.name));
|
|
38
|
+
return names.has("SID") && names.has("HSID") && names.has("SSID");
|
|
39
|
+
}
|
|
18
40
|
export function hasSIDCookies(cookiesPath) {
|
|
19
41
|
if (!existsSync(cookiesPath))
|
|
20
42
|
return false;
|
|
21
43
|
try {
|
|
22
44
|
const content = readFileSync(cookiesPath, "utf8");
|
|
23
|
-
|
|
45
|
+
const rows = parseCookieRows(content);
|
|
46
|
+
if (hasCookieTriplet(rows, "youtube.com")) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const hasYouTubeLoginInfo = rows.some((row) => row.name === "LOGIN_INFO" && row.domain.includes("youtube.com"));
|
|
50
|
+
return hasYouTubeLoginInfo && hasCookieTriplet(rows, "google.");
|
|
24
51
|
}
|
|
25
52
|
catch {
|
|
26
53
|
return false;
|
|
@@ -48,19 +75,14 @@ export function areCookiesExpired(cookiesPath) {
|
|
|
48
75
|
try {
|
|
49
76
|
const content = readFileSync(cookiesPath, "utf8");
|
|
50
77
|
const nowSec = Date.now() / 1000;
|
|
78
|
+
const rows = parseCookieRows(content);
|
|
51
79
|
let foundAnySession = false;
|
|
52
80
|
let foundAnyValid = false;
|
|
53
|
-
for (const
|
|
54
|
-
if (
|
|
55
|
-
continue;
|
|
56
|
-
const fields = line.split("\t");
|
|
57
|
-
if (fields.length < 7)
|
|
58
|
-
continue;
|
|
59
|
-
const name = fields[5];
|
|
60
|
-
if (!SESSION_COOKIE_PATTERNS.includes(name))
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
if (!SESSION_COOKIE_PATTERNS.includes(row.name))
|
|
61
83
|
continue;
|
|
62
84
|
foundAnySession = true;
|
|
63
|
-
const expiry =
|
|
85
|
+
const expiry = row.expires;
|
|
64
86
|
// expiry 0 = session cookie (no expiration) — treat as valid
|
|
65
87
|
if (expiry === 0 || expiry >= nowSec) {
|
|
66
88
|
foundAnyValid = true;
|
package/dist/utils/ytdlp.d.ts
CHANGED
package/dist/utils/ytdlp.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { PATHS } from "./config.js";
|
|
5
6
|
import { getYtDlpPath } from "./ytdlpPath.js";
|
|
7
|
+
function writeYtDlpFailureLog(payload) {
|
|
8
|
+
try {
|
|
9
|
+
mkdirSync(PATHS.logsDir, { recursive: true });
|
|
10
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
11
|
+
const filePath = join(PATHS.logsDir, `yt-dlp-failure-${stamp}.json`);
|
|
12
|
+
writeFileSync(filePath, JSON.stringify({
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
binary: getYtDlpPath(),
|
|
15
|
+
...payload,
|
|
16
|
+
}, null, 2), "utf8");
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
6
23
|
export function createYtDlpStderrLineSplitter(onLine) {
|
|
7
24
|
let buffer = "";
|
|
8
25
|
let pendingCarriageReturn = false;
|
|
@@ -92,11 +109,22 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
|
92
109
|
settled = true;
|
|
93
110
|
proc.kill("SIGKILL");
|
|
94
111
|
flushStderrLineBuffer();
|
|
112
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
113
|
+
const stderr = `yt-dlp timed out after ${timeoutMs}ms`;
|
|
95
114
|
resolve({
|
|
96
115
|
exitCode: -1,
|
|
97
|
-
stdout
|
|
98
|
-
stderr
|
|
116
|
+
stdout,
|
|
117
|
+
stderr,
|
|
99
118
|
durationMs: Date.now() - start,
|
|
119
|
+
failureLogPath: writeYtDlpFailureLog({
|
|
120
|
+
args: finalArgs,
|
|
121
|
+
exitCode: -1,
|
|
122
|
+
stdout,
|
|
123
|
+
stderr,
|
|
124
|
+
durationMs: Date.now() - start,
|
|
125
|
+
timeoutMs,
|
|
126
|
+
trigger: "timeout",
|
|
127
|
+
}),
|
|
100
128
|
});
|
|
101
129
|
}
|
|
102
130
|
}, timeoutMs);
|
|
@@ -105,11 +133,25 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
|
105
133
|
if (!settled) {
|
|
106
134
|
settled = true;
|
|
107
135
|
flushStderrLineBuffer();
|
|
136
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
137
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
138
|
+
const exitCode = code ?? 1;
|
|
108
139
|
resolve({
|
|
109
|
-
exitCode
|
|
110
|
-
stdout
|
|
111
|
-
stderr
|
|
140
|
+
exitCode,
|
|
141
|
+
stdout,
|
|
142
|
+
stderr,
|
|
112
143
|
durationMs: Date.now() - start,
|
|
144
|
+
failureLogPath: exitCode === 0
|
|
145
|
+
? undefined
|
|
146
|
+
: writeYtDlpFailureLog({
|
|
147
|
+
args: finalArgs,
|
|
148
|
+
exitCode,
|
|
149
|
+
stdout,
|
|
150
|
+
stderr,
|
|
151
|
+
durationMs: Date.now() - start,
|
|
152
|
+
timeoutMs,
|
|
153
|
+
trigger: "exit",
|
|
154
|
+
}),
|
|
113
155
|
});
|
|
114
156
|
}
|
|
115
157
|
});
|
|
@@ -117,7 +159,20 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
|
117
159
|
clearTimeout(timer);
|
|
118
160
|
if (!settled) {
|
|
119
161
|
settled = true;
|
|
120
|
-
|
|
162
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
163
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
164
|
+
reject(Object.assign(err, {
|
|
165
|
+
failureLogPath: writeYtDlpFailureLog({
|
|
166
|
+
args: finalArgs,
|
|
167
|
+
exitCode: 1,
|
|
168
|
+
stdout,
|
|
169
|
+
stderr,
|
|
170
|
+
durationMs: Date.now() - start,
|
|
171
|
+
timeoutMs,
|
|
172
|
+
trigger: "spawn_error",
|
|
173
|
+
spawnError: err instanceof Error ? err.message : String(err),
|
|
174
|
+
}),
|
|
175
|
+
}));
|
|
121
176
|
}
|
|
122
177
|
});
|
|
123
178
|
});
|