@danhachuel/thunderbolt 0.5.85 → 0.5.87
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/README.md +1 -1
- package/hermes_ui/media_downloader.py +28 -1
- package/integrations/youtube_session_manager.py +79 -1
- package/package.json +1 -1
- package/requirements.txt +2 -1
- package/scripts/cli.mjs +1 -1
- package/scripts/install.mjs +13 -1
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ A correcção da integração Nano Banana remove o campo de entrega `inline` rej
|
|
|
141
141
|
|
|
142
142
|
A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n, distribuído localmente em `seed/references/guide-instagram.md` e com ligação para a [fonte original no GitHub](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md). A página **Configurações > Notificações** mantém um histórico persistente de conclusões e falhas, reconcilia estados escritos por componentes locais e disponibiliza um checkbox independente para cada operação mapeada. A subaba **Geral** mantém esse centro local; a subaba **Telegram** permite configurar Bot Token, Chat ID e proxy opcional e envia as mesmas notificações pela Bot API oficial, sem polling ou mensagens recebidas. As notificações novas também aparecem automaticamente como pop-ups no canto inferior direito enquanto o utilizador está noutra página, sem precisar de abrir Notificações; cada aviso é mostrado uma vez por sessão e o histórico continua a controlar o estado de leitura. A página **Configurações > Logs**, colocada imediatamente abaixo de Notificações e acima de Configuração API, apresenta o histórico unificado de tarefas e notificações, com filtro livre por operação, filtros adicionais por operação e estado, e as colunas Operação, Estado, Data, Hora, Registo, Origem, Progresso e Detalhes. A tabela mantém uma barra vertical e, quando a soma das colunas excede a largura disponível, disponibiliza também uma barra de rolagem horizontal na parte inferior para consultar integralmente células longas, sobretudo em **Detalhes**.
|
|
143
143
|
|
|
144
|
-
A página **Edição > Download Mídia** utiliza a API Python do [yt-dlp](https://github.com/yt-dlp/yt-dlp) para descarregar vídeos e áudio de URLs públicas, com qualidade, contentor, formato de áudio, legendas, metadados, playlists, progresso e histórico local em `storage/downloads/` e `storage/state/media_downloads.json`. Conversão e combinação de streams podem exigir FFmpeg.
|
|
144
|
+
A página **Edição > Download Mídia** utiliza a API Python do [yt-dlp](https://github.com/yt-dlp/yt-dlp) para descarregar vídeos e áudio de URLs públicas, com qualidade, contentor, formato de áudio, legendas, metadados, playlists, progresso e histórico local em `storage/downloads/` e `storage/state/media_downloads.json`. O Thunderbolt instala o pacote Python **Deno** e resolve automaticamente o executável gerido por ele, passando-o ao yt-dlp como runtime `deno:<caminho>`. O pacote `yt-dlp[default]` inclui também os scripts EJS necessários para os desafios JavaScript do YouTube. Se existir um Deno instalado no `PATH`, ele é usado como fallback. Conversão e combinação de streams podem exigir FFmpeg.
|
|
145
145
|
|
|
146
146
|
## Catálogo comum do Backlog e da Automação
|
|
147
147
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import re
|
|
5
|
+
import shutil
|
|
5
6
|
import uuid
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
@@ -43,6 +44,22 @@ def _download_root() -> Path:
|
|
|
43
44
|
return storage.MEDIA_DOWNLOADS.resolve()
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def _deno_runtime_path() -> str | None:
|
|
48
|
+
"""Resolve the project-managed Deno binary, with a PATH fallback."""
|
|
49
|
+
try:
|
|
50
|
+
import deno # type: ignore
|
|
51
|
+
|
|
52
|
+
finder = getattr(deno, "find_deno_bin", None)
|
|
53
|
+
if callable(finder):
|
|
54
|
+
candidate = Path(str(finder())).expanduser()
|
|
55
|
+
if candidate.is_file():
|
|
56
|
+
return str(candidate.resolve())
|
|
57
|
+
except (ImportError, OSError, RuntimeError, TypeError, ValueError):
|
|
58
|
+
pass
|
|
59
|
+
candidate = shutil.which("deno")
|
|
60
|
+
return str(Path(candidate).resolve()) if candidate else None
|
|
61
|
+
|
|
62
|
+
|
|
46
63
|
def _safe_download_path(value: str | Path) -> Path | None:
|
|
47
64
|
root = _download_root()
|
|
48
65
|
candidate = Path(value)
|
|
@@ -136,6 +153,10 @@ def build_download_options(
|
|
|
136
153
|
"overwrites": False,
|
|
137
154
|
"paths": {"home": str(root)},
|
|
138
155
|
}
|
|
156
|
+
deno_path = _deno_runtime_path()
|
|
157
|
+
if deno_path:
|
|
158
|
+
# Equivalent to the CLI argument --js-runtimes deno:<path>.
|
|
159
|
+
options["js_runtimes"] = {"deno": deno_path}
|
|
139
160
|
if progress_hook is not None:
|
|
140
161
|
options["progress_hooks"] = [progress_hook]
|
|
141
162
|
if normalized_mode == "audio":
|
|
@@ -183,7 +204,13 @@ def media_download_file(record: dict[str, Any], filename: str) -> Path | None:
|
|
|
183
204
|
|
|
184
205
|
|
|
185
206
|
def dependency_status() -> dict[str, Any]:
|
|
186
|
-
|
|
207
|
+
deno_path = _deno_runtime_path()
|
|
208
|
+
return {
|
|
209
|
+
"yt_dlp": yt_dlp is not None,
|
|
210
|
+
"deno": bool(deno_path),
|
|
211
|
+
"deno_path": deno_path or "",
|
|
212
|
+
"ffmpeg_note": "A conversão/combinação de streams pode exigir FFmpeg.",
|
|
213
|
+
}
|
|
187
214
|
|
|
188
215
|
|
|
189
216
|
def _files_from_info(info: Any, started_at: float) -> list[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/requirements.txt
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", "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) {
|