@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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
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();
|
package/scripts/install.mjs
CHANGED
|
@@ -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) {
|