@mkterswingman/5mghost-yonder 0.0.12 → 0.0.13

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.
@@ -38,6 +38,7 @@ interface DownloadOneItemDeps {
38
38
  videoQuality: string;
39
39
  onProgress: (update: DownloadOneItemProgressUpdate) => void;
40
40
  }) => Promise<string>;
41
+ refreshCookies: () => Promise<boolean>;
41
42
  downloadSubtitles: (input: {
42
43
  videoId: string;
43
44
  sourceUrl: string;
@@ -2,9 +2,11 @@ import { mkdir, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { downloadSubtitlesForLanguages, toReadableSubtitleJobError } from "../tools/subtitles.js";
4
4
  import { loadConfig } from "../utils/config.js";
5
+ import { tryHeadlessRefresh } from "../utils/cookieRefresh.js";
5
6
  import { hasFfmpeg, isRuntimeMissingMessage } from "../utils/ffmpeg.js";
6
7
  import { formatBytes } from "../utils/formatters.js";
7
8
  import { hasYtDlp, runYtDlp, runYtDlpJson } from "../utils/ytdlp.js";
9
+ import { appendDiagnosticLog, classifyYtDlpFailure } from "../utils/ytdlpFailures.js";
8
10
  import { parseYtDlpProgressLine } from "../utils/ytdlpProgress.js";
9
11
  export async function downloadOneItem(input) {
10
12
  const deps = resolveDeps(input.deps);
@@ -50,12 +52,28 @@ export async function downloadOneItem(input) {
50
52
  if (needsVideo(input.mode)) {
51
53
  currentStep = "download_video";
52
54
  manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 0);
53
- result.video_file = await deps.downloadVideo({
54
- sourceUrl: input.item.source_url,
55
- outputFile: videoFile,
56
- videoQuality,
57
- onProgress,
58
- });
55
+ try {
56
+ result.video_file = await deps.downloadVideo({
57
+ sourceUrl: input.item.source_url,
58
+ outputFile: videoFile,
59
+ videoQuality,
60
+ onProgress,
61
+ });
62
+ }
63
+ catch (error) {
64
+ const failureKind = classifyVideoDownloadError(error);
65
+ if (failureKind != null && failureKind !== "RATE_LIMITED" && await deps.refreshCookies()) {
66
+ result.video_file = await deps.downloadVideo({
67
+ sourceUrl: input.item.source_url,
68
+ outputFile: videoFile,
69
+ videoQuality,
70
+ onProgress,
71
+ });
72
+ }
73
+ else {
74
+ throw error;
75
+ }
76
+ }
59
77
  result.final_file_size = formatBytes((await deps.stat(result.video_file)).size);
60
78
  }
61
79
  if (needsSubtitles(input.mode)) {
@@ -103,6 +121,7 @@ function resolveDeps(overrides) {
103
121
  hasFfmpeg,
104
122
  fetchMetadata: fetchMetadataWithYtDlp,
105
123
  downloadVideo: downloadVideoWithYtDlp,
124
+ refreshCookies: tryHeadlessRefresh,
106
125
  downloadSubtitles: downloadSubtitlesWithYtDlp,
107
126
  mkdir: async (path, options) => {
108
127
  await mkdir(path, options);
@@ -166,7 +185,17 @@ async function downloadVideoWithYtDlp(input) {
166
185
  }
167
186
  });
168
187
  if (result.exitCode !== 0) {
169
- throw new Error(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`);
188
+ const failureKind = classifyYtDlpFailure(result.stderr);
189
+ if (failureKind === "RATE_LIMITED") {
190
+ throw new Error(appendDiagnosticLog("The current YouTube session has been rate-limited. Wait and retry later.", result.failureLogPath));
191
+ }
192
+ if (failureKind === "SIGN_IN_REQUIRED") {
193
+ throw new Error(appendDiagnosticLog("YouTube requested an additional sign-in confirmation for this video.", result.failureLogPath));
194
+ }
195
+ if (failureKind === "COOKIES_INVALID" || failureKind === "COOKIES_EXPIRED") {
196
+ throw new Error(appendDiagnosticLog("YouTube rejected the current cookies for video download.", result.failureLogPath));
197
+ }
198
+ throw new Error(appendDiagnosticLog(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`, result.failureLogPath));
170
199
  }
171
200
  return resolveYtDlpOutputPath(input.outputFile, result.stdout);
172
201
  }
@@ -259,6 +288,10 @@ function sortSubtitleFiles(files) {
259
288
  .sort(([left], [right]) => left.localeCompare(right))
260
289
  .map(([format, paths]) => [format, [...paths].sort()]));
261
290
  }
291
+ function classifyVideoDownloadError(error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ return classifyYtDlpFailure(message);
294
+ }
262
295
  function toErrorMessage(error) {
263
296
  return toReadableSubtitleJobError(error);
264
297
  }
@@ -5,12 +5,15 @@ import { PATHS } from "../utils/config.js";
5
5
  import { runYtDlp } from "../utils/ytdlp.js";
6
6
  import { hasSIDCookies, areCookiesExpired } from "../utils/cookies.js";
7
7
  import { tryHeadlessRefresh } from "../utils/cookieRefresh.js";
8
+ import { appendDiagnosticLog, classifyYtDlpFailure } from "../utils/ytdlpFailures.js";
8
9
  import { resolveVideoInput, normalizeVideoInputs } from "../utils/videoInput.js";
9
10
  const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkterswingman/5mghost-yonder setup\n• PAT: 设置环境变量 YT_MCP_TOKEN 或在 https://mkterswingman.com/pat/login 生成 token";
10
11
  const COOKIE_SETUP_COMMAND = "yt-mcp setup-cookies";
11
12
  const COOKIE_MISSING_MESSAGE = `No valid YouTube cookies found.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
12
13
  const COOKIE_EXPIRED_MESSAGE = `YouTube cookies have expired and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
13
14
  const COOKIE_INVALID_MESSAGE = `YouTube rejected the current cookies and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
15
+ const SIGN_IN_REQUIRED_MESSAGE = `YouTube requested an additional sign-in confirmation and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
16
+ const RATE_LIMITED_MESSAGE = "The current YouTube session has been rate-limited. Wait and retry later.";
14
17
  const COOKIE_JOB_MESSAGE = `YouTube cookies are missing, expired, or invalid.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
15
18
  function toolOk(payload) {
16
19
  return {
@@ -56,30 +59,20 @@ function isCookieFailureText(error) {
56
59
  normalized.includes("login") ||
57
60
  normalized.includes("not a bot"));
58
61
  }
59
- function appendDiagnosticLog(message, logPath) {
60
- return logPath ? `${message}\nDiagnostic log: ${logPath}` : message;
61
- }
62
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
- };
63
+ const kind = classifyYtDlpFailure(stderr);
64
+ switch (kind) {
65
+ case "COOKIES_EXPIRED":
66
+ return { code: kind, message: COOKIE_EXPIRED_MESSAGE };
67
+ case "COOKIES_INVALID":
68
+ return { code: kind, message: COOKIE_INVALID_MESSAGE };
69
+ case "SIGN_IN_REQUIRED":
70
+ return { code: kind, message: SIGN_IN_REQUIRED_MESSAGE };
71
+ case "RATE_LIMITED":
72
+ return { code: kind, message: RATE_LIMITED_MESSAGE };
73
+ default:
74
+ return null;
81
75
  }
82
- return null;
83
76
  }
84
77
  export function toReadableSubtitleJobError(error) {
85
78
  const message = error instanceof Error ? error.message : String(error);
@@ -367,7 +360,7 @@ export async function downloadSubtitlesForLanguages(input) {
367
360
  outputStem: `${lang}`,
368
361
  targetFile,
369
362
  });
370
- if (result.cookieFailureCode) {
363
+ if (result.cookieFailureCode && result.cookieFailureCode !== "RATE_LIMITED") {
371
364
  const refreshed = await tryRefreshSubtitleCookies();
372
365
  if (refreshed) {
373
366
  result = await downloadSubtitle(input.videoId, lang, format, {
@@ -467,7 +460,14 @@ export function registerSubtitleTools(server, config, tokenManager) {
467
460
  }
468
461
  // Retry also failed with expired cookies — give up
469
462
  }
470
- return toolErr(dl.cookieFailureCode, dl.error ?? (dl.cookieFailureCode === "COOKIES_EXPIRED" ? COOKIE_EXPIRED_MESSAGE : COOKIE_INVALID_MESSAGE));
463
+ return toolErr(dl.cookieFailureCode, dl.error ??
464
+ (dl.cookieFailureCode === "COOKIES_EXPIRED"
465
+ ? COOKIE_EXPIRED_MESSAGE
466
+ : dl.cookieFailureCode === "SIGN_IN_REQUIRED"
467
+ ? SIGN_IN_REQUIRED_MESSAGE
468
+ : dl.cookieFailureCode === "RATE_LIMITED"
469
+ ? RATE_LIMITED_MESSAGE
470
+ : COOKIE_INVALID_MESSAGE));
471
471
  }
472
472
  if (!dl.ok) {
473
473
  // When using default languages, silently skip unavailable ones
@@ -554,6 +554,9 @@ export function registerSubtitleTools(server, config, tokenManager) {
554
554
  lastError = dl.error;
555
555
  }
556
556
  }
557
+ if (cookieFailureCode === "RATE_LIMITED") {
558
+ return toolErr("RATE_LIMITED", RATE_LIMITED_MESSAGE);
559
+ }
557
560
  if (cookieFailureCode) {
558
561
  const refreshed = await tryRefreshSubtitleCookies();
559
562
  if (refreshed) {
@@ -571,7 +574,11 @@ export function registerSubtitleTools(server, config, tokenManager) {
571
574
  }
572
575
  }
573
576
  if (cookieFailureCode) {
574
- return toolErr(cookieFailureCode, cookieFailureCode === "COOKIES_EXPIRED" ? COOKIE_EXPIRED_MESSAGE : COOKIE_INVALID_MESSAGE);
577
+ return toolErr(cookieFailureCode, cookieFailureCode === "COOKIES_EXPIRED"
578
+ ? COOKIE_EXPIRED_MESSAGE
579
+ : cookieFailureCode === "SIGN_IN_REQUIRED"
580
+ ? SIGN_IN_REQUIRED_MESSAGE
581
+ : COOKIE_INVALID_MESSAGE);
575
582
  }
576
583
  }
577
584
  if (langFound.length > 0) {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {