@danhachuel/thunderbolt 0.5.86 → 0.5.88

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.
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
+ import errno
4
5
  import json
5
6
  import os
6
7
  import re
@@ -46,6 +47,14 @@ def _authorization_error_message(email: str, exc: Exception) -> str:
46
47
  return f"A autorização da conta {email} falhou: {detail}"
47
48
 
48
49
 
50
+ def _is_address_in_use_error(exc: BaseException) -> bool:
51
+ """Return whether an OAuth callback bind failed because its port is busy."""
52
+ if getattr(exc, "winerror", None) == 10048 or getattr(exc, "errno", None) == errno.EADDRINUSE:
53
+ return True
54
+ detail = str(exc).lower()
55
+ return "10048" in detail or "address already in use" in detail or "only one usage" in detail
56
+
57
+
49
58
  def _text(value: Any) -> str:
50
59
  return str(value or "").strip()
51
60
 
@@ -115,15 +124,26 @@ def authorize_account(account: dict[str, Any], storage_root: Path, *, open_brows
115
124
  except ImportError as exc:
116
125
  return IntegrationResult(False, "As dependências Google OAuth ainda não estão instaladas. Execute a instalação do Thunderbolt novamente.", {"status": "missing_dependencies", "error": str(exc)})
117
126
  try:
118
- flow = InstalledAppFlow.from_client_config(_client_config(account), BATCH_SCOPES)
119
- credentials = flow.run_local_server(
120
- host=loopback_host(),
121
- port=loopback_port(),
122
- open_browser=open_browser,
123
- access_type="offline",
124
- prompt="consent",
125
- include_granted_scopes="true",
126
- )
127
+ credentials = None
128
+ configured_port = loopback_port()
129
+ for attempt, port in enumerate((configured_port, 0)):
130
+ flow = InstalledAppFlow.from_client_config(_client_config(account), BATCH_SCOPES)
131
+ try:
132
+ credentials = flow.run_local_server(
133
+ host=loopback_host(),
134
+ port=port,
135
+ open_browser=open_browser,
136
+ access_type="offline",
137
+ prompt="consent",
138
+ include_granted_scopes="true",
139
+ )
140
+ break
141
+ except OSError as exc:
142
+ if attempt == 0 and _is_address_in_use_error(exc):
143
+ continue
144
+ raise
145
+ if credentials is None:
146
+ raise RuntimeError("Não foi possível iniciar o callback OAuth local.")
127
147
  path = token_path(storage_root, account)
128
148
  _save_credentials(path, credentials)
129
149
  return IntegrationResult(True, f"Conta Google autorizada para listagem de canais: {email}.", {"status": "authorized", "email": email, "token_path": str(path)})
@@ -14,6 +14,8 @@ import os
14
14
  import re
15
15
  import shutil
16
16
  import signal
17
+ import subprocess
18
+ import sys
17
19
  import time
18
20
  from contextlib import contextmanager
19
21
  from dataclasses import dataclass
@@ -52,6 +54,70 @@ DESCRIPTION_SELECTORS = [
52
54
  ]
53
55
  BLOCKED_TEXT = ("verify", "verificação", "verification", "captcha", "confirme que você é humano", "confirm it's you", "confirme que é você")
54
56
 
57
+
58
+ class _BrowserUnavailableError(RuntimeError):
59
+ """Raised when Playwright cannot find or prepare a usable browser."""
60
+
61
+
62
+ def _looks_like_missing_playwright_browser(error: BaseException) -> bool:
63
+ message = str(error).lower()
64
+ return "executable doesn't exist" in message or "executable does not exist" in message or "playwright was just installed" in message
65
+
66
+
67
+ def _install_playwright_chromium() -> bool:
68
+ """Install the bundled Chromium once when Playwright reports it missing."""
69
+ try:
70
+ result = subprocess.run(
71
+ [os.fspath(sys.executable), "-m", "playwright", "install", "chromium"],
72
+ check=False,
73
+ capture_output=True,
74
+ text=True,
75
+ timeout=300,
76
+ )
77
+ except (OSError, subprocess.SubprocessError) as exc:
78
+ logger.warning("Não foi possível preparar o Chromium do Playwright: %s", _safe_error(exc))
79
+ return False
80
+ if result.returncode != 0:
81
+ detail = (result.stderr or result.stdout or "").strip().splitlines()
82
+ logger.warning("A instalação automática do Chromium falhou: %s", detail[-1] if detail else "erro desconhecido")
83
+ return False
84
+ return True
85
+
86
+
87
+ def _launch_playwright_browser(playwright: Any) -> Any:
88
+ """Launch the configured browser, recovering from a missing Playwright binary."""
89
+ configured_path = str(os.environ.get("THUNDERBOLT_CHROME_PATH") or "").strip()
90
+ launch_kwargs: dict[str, Any] = {"headless": True}
91
+ if configured_path:
92
+ if not Path(configured_path).is_file():
93
+ raise _BrowserUnavailableError(f"Executável Chrome/Chromium não encontrado: {configured_path}")
94
+ launch_kwargs["executable_path"] = configured_path
95
+
96
+ try:
97
+ return playwright.chromium.launch(**launch_kwargs)
98
+ except Exception as initial_error:
99
+ if configured_path or not _looks_like_missing_playwright_browser(initial_error):
100
+ raise
101
+
102
+ # Prefer an already installed Google Chrome before downloading another
103
+ # browser. This is particularly useful on Windows machines where the
104
+ # Playwright cache was removed but Chrome itself is still available.
105
+ try:
106
+ return playwright.chromium.launch(headless=True, channel="chrome")
107
+ except Exception as chrome_error:
108
+ if not _looks_like_missing_playwright_browser(initial_error):
109
+ raise initial_error
110
+ if not _install_playwright_chromium():
111
+ raise _BrowserUnavailableError(
112
+ "O Chromium do Playwright não está instalado e não foi possível prepará-lo automaticamente."
113
+ ) from chrome_error
114
+ try:
115
+ return playwright.chromium.launch(headless=True)
116
+ except Exception as retry_error:
117
+ raise _BrowserUnavailableError(
118
+ "O Chromium do Playwright continua indisponível depois da instalação automática."
119
+ ) from retry_error
120
+
55
121
  try:
56
122
  from yt_cm import YouTubeCookieManager # type: ignore
57
123
  YT_CM_AVAILABLE = True
@@ -203,7 +269,11 @@ def _capture_with_playwright(document: dict[str, Any], *, logs_dir: Path, video_
203
269
  logs_dir.mkdir(parents=True, exist_ok=True)
204
270
  cookies = [{"name": key, "value": str(document["cookies"][key]), "domain": ".youtube.com", "path": "/"} for key in COOKIE_KEYS if document.get("cookies", {}).get(key)]
205
271
  with sync_playwright() as playwright:
206
- browser = playwright.chromium.launch(headless=True)
272
+ try:
273
+ browser = _launch_playwright_browser(playwright)
274
+ except _BrowserUnavailableError as exc:
275
+ logger.error("Browser Playwright indisponível: %s", _safe_error(exc))
276
+ return "browser_unavailable", None
207
277
  context = browser.new_context()
208
278
  context.add_cookies(cookies)
209
279
  page = context.new_page()
@@ -298,6 +368,14 @@ def renew_account_session(storage_root: Path, account: dict[str, Any], settings:
298
368
  return RenewalResult(False, "cookie_renewal_failed", f"Falha na renovação de cookies: {_safe_error(exc)}", account_id)
299
369
  for attempt in range(1, MAX_CAPTURE_ATTEMPTS + 1):
300
370
  capture_status, token = _capture_with_playwright(document, logs_dir=logs_dir, video_id=str(account.get("video_id") or ""))
371
+ if capture_status == "browser_unavailable":
372
+ return RenewalResult(
373
+ False,
374
+ "browser_unavailable",
375
+ "Renovação falhou: o Chromium do Playwright não está disponível. Execute novamente a instalação do Thunderbolt e tente renovar outra vez.",
376
+ account_id,
377
+ attempt,
378
+ )
301
379
  if capture_status == "blocked_by_google":
302
380
  _write_renewal_state(directory, "blocked_by_google")
303
381
  return RenewalResult(False, "blocked_by_google", "🔒 Sessão bloqueada pelo Google. Renovação manual necessária.", account_id, attempt)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.86",
3
+ "version": "0.5.88",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",
package/scripts/cli.mjs CHANGED
@@ -135,7 +135,7 @@ function check() {
135
135
  console.error(`Python não encontrado. Execute: npx.cmd --yes @danhachuel/thunderbolt install`);
136
136
  process.exit(1);
137
137
  }
138
- const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub", "composio"];
138
+ const requiredModules = ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "sklearn", "mlxtend", "plotly", "seaborn", "kagglehub", "composio", "playwright"];
139
139
  const missing = requiredModules.filter((moduleName) => !moduleAvailable(moduleName));
140
140
  const ffmpeg = moduleAvailable("imageio_ffmpeg");
141
141
  const mptPath = configuredMoneyPrinterPath();
@@ -452,6 +452,17 @@ function installRequirementIfNeeded(requirementsPath, stateKey, modules, label)
452
452
  writeDependencyState({ [stateKey]: currentHash });
453
453
  }
454
454
 
455
+ function installPlaywrightBrowsers() {
456
+ if (!existsSync(pythonBin)) return;
457
+ console.log("Playwright: a verificar o browser Chromium... ");
458
+ const result = spawnSync(pythonBin, ["-m", "playwright", "install", "chromium"], { stdio: "inherit", env: pythonEnvironment });
459
+ if (result.status !== 0) {
460
+ console.error("Não foi possível instalar o browser Chromium do Playwright.");
461
+ console.error("Execute novamente o instalador depois de confirmar o acesso à Internet.");
462
+ process.exit(result.status || 1);
463
+ }
464
+ }
465
+
455
466
  function writeSettings(moneyprinterPath) {
456
467
  const stateDir = join(thunderboltHome, "storage", "state");
457
468
  mkdirSync(stateDir, { recursive: true });
@@ -470,7 +481,8 @@ function writeSettings(moneyprinterPath) {
470
481
 
471
482
  function installThunderboltDependencies(python) {
472
483
  if (!existsSync(pythonBin)) run(python.command, [...python.args, "-m", "venv", venvPath]);
473
- installRequirementIfNeeded(join(root, "requirements.txt"), "thunderbolt_requirements_sha256", ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "google.auth", "google_auth_oauthlib", "googleapiclient", "yt_dlp", "deno", "youtube_transcript_api", "huggingface_hub"], "Thunderbolt");
484
+ installRequirementIfNeeded(join(root, "requirements.txt"), "thunderbolt_requirements_sha256", ["streamlit", "requests", "pandas", "toml", "imageio_ffmpeg", "edge_tts", "google.auth", "google_auth_oauthlib", "googleapiclient", "yt_dlp", "deno", "youtube_transcript_api", "huggingface_hub", "playwright"], "Thunderbolt");
485
+ installPlaywrightBrowsers();
474
486
  }
475
487
 
476
488
  function installMoneyPrinterDependencies(moneyprinterPath) {