@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 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
- return {"yt_dlp": yt_dlp is not None, "ffmpeg_note": "A conversão/combinação de streams pode exigir FFmpeg."}
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
- 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.85",
3
+ "version": "0.5.87",
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/requirements.txt CHANGED
@@ -15,7 +15,8 @@ scikit-learn>=1.3,<2
15
15
  mlxtend>=0.23,<1
16
16
  plotly>=5.18,<7
17
17
  seaborn>=0.13,<1
18
- yt-dlp>=2025.1.15,<2027
18
+ yt-dlp[default]>=2025.1.15,<2027
19
+ deno>=2.3,<3
19
20
  youtube-transcript-api>=1.2.0,<2
20
21
  ytmusicapi>=1.12,<2
21
22
  pushtunes>=2.15,<3
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", "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) {