@danhachuel/thunderbolt 0.2.60 → 0.2.61

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/app/main.py CHANGED
@@ -42,7 +42,7 @@ from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdap
42
42
  from integrations.postiz import PostizAdapter
43
43
  from integrations.upload_routing import OFFICIAL_DAILY_LIMIT, official_upload_count, upload_with_default_route
44
44
  from integrations.youtube_direct_upload import YouTubeDirectUploader
45
- from integrations.youtube_direct_credentials import delete_credentials_document, direct_account_status, document_status, ensure_credentials_document, merge_credentials_document, parse_credentials_document, save_credentials_document, update_credentials_document_session_info
45
+ from integrations.youtube_direct_credentials import delete_credentials_document, direct_account_status, document_status, ensure_credentials_document, load_credentials_document, merge_credentials_document, parse_credentials_document, save_credentials_document, update_credentials_document_session_info
46
46
  from integrations.youtube_batch import account_key as youtube_batch_account_key, account_status as youtube_batch_account_status, authorize_account as authorize_youtube_batch_account, delete_account_token as delete_youtube_batch_token, list_my_channels as list_youtube_batch_channels, loopback_redirect_uri
47
47
  from integrations.local_runtime import MoneyPrinterRuntime
48
48
  from integrations.moneyprinter_config import sync_moneyprinter_config
@@ -2571,7 +2571,7 @@ def render_settings():
2571
2571
  account_email_snapshot = str(batch_account.get("email") or "sem e-mail")
2572
2572
  account_label_snapshot = str(batch_account.get("label") or "Canais YouTube")
2573
2573
  ensure_credentials_document(STORAGE, batch_account, settings, channel_state)
2574
- direct_status = direct_account_status(STORAGE, batch_account)
2574
+ direct_status = direct_account_status(STORAGE, batch_account, settings)
2575
2575
  missing_document_parts = list(direct_status.get("missing_cookies", []))
2576
2576
  if not direct_status.get("has_session_info"):
2577
2577
  missing_document_parts.append("sessionInfo")
@@ -2678,6 +2678,31 @@ def render_settings():
2678
2678
  if youtube_accounts_missing_document:
2679
2679
  st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
2680
2680
 
2681
+ st.divider()
2682
+ st.markdown("### INNERTUBE_API_KEY")
2683
+ st.caption("Esta chave pertence à conta Google/YouTube seleccionada e fica guardada na configuração da conta. Não faz parte do documento de cookies/credenciais e não é editada no separador API Keys.")
2684
+ account_key_options = [str(account.get("id")) for account in batch_accounts if account.get("id")]
2685
+ account_key_labels = {str(account.get("id")): f"{account.get('label', 'Canais YouTube')} — {account.get('email', 'sem e-mail')}" for account in batch_accounts if account.get("id")}
2686
+ if account_key_options:
2687
+ with st.form("innertube_api_key_form"):
2688
+ selected_key_account_id = st.selectbox("Conta Google/YouTube", account_key_options, format_func=lambda value: account_key_labels.get(value, value), key="innertube_key_account")
2689
+ selected_key_account = next(account for account in batch_accounts if str(account.get("id")) == selected_key_account_id)
2690
+ current_innertube_api_key = direct_account_status(STORAGE, selected_key_account, settings).get("innertube_api_key", "")
2691
+ innertube_api_key_value = st.text_input("INNERTUBE_API_KEY", value=current_innertube_api_key, type="password", key=f"innertube_api_key_{selected_key_account_id}", help="Chave usada pelo Upload directo desta conta. Guarde-a aqui, separada do documento de cookies.")
2692
+ save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY", type="primary", use_container_width=True)
2693
+ if save_innertube_api_key:
2694
+ selected_key_account["innertube_api_key"] = innertube_api_key_value.strip()
2695
+ selected_key_account.pop("INNERTUBE_API_KEY", None)
2696
+ settings.pop("direct_innertube_api_key", None)
2697
+ settings["youtube_batch_accounts"] = batch_accounts
2698
+ write_json("settings.json", settings)
2699
+ document = load_credentials_document(STORAGE, selected_key_account, settings, channel_state, create=True)
2700
+ save_credentials_document(STORAGE, selected_key_account, document)
2701
+ st.success("INNERTUBE_API_KEY guardada na configuração da conta Google/YouTube, fora do documento de cookies.")
2702
+ st.rerun()
2703
+ else:
2704
+ st.info("Adicione primeiro uma conta Google/YouTube para configurar a INNERTUBE_API_KEY.")
2705
+
2681
2706
  st.divider()
2682
2707
  st.markdown("### Adicionar outra conta Gmail")
2683
2708
  st.caption("Este formulário fica fora dos cartões das contas existentes. A associação de canais não depende da completude deste documento; ela apenas ficará pendente para Upload directo até os campos serem preenchidos.")
@@ -2689,7 +2714,7 @@ def render_settings():
2689
2714
  new_account_client_id = st.text_input("OAuth Client ID", key="new_batch_account_client_id")
2690
2715
  with add_cols[1]:
2691
2716
  new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
2692
- new_account_session_info = st.text_input("sessionInfo token desta conta Google", type="password", key="new_batch_account_session_info", help="Token sessionInfo desta conta. Os cookies, INNERTUBE_API_KEY, chunk_size e delegated_session_ids continuam apenas no documento.")
2717
+ new_account_session_info = st.text_input("sessionInfo token desta conta Google", type="password", key="new_batch_account_session_info", help="Token sessionInfo desta conta. Os cookies e delegated_session_ids ficam no documento; a INNERTUBE_API_KEY é configurada no bloco próprio acima.")
2693
2718
  new_account_document = st.file_uploader("Documento de cookies/credenciais opcional", type=["json"], key="new_batch_account_credentials_document", help="Pode subir agora um JSON completo ou apenas o documento de cookies. Se não subir, será criado um credentials.json padrão vazio.")
2694
2719
  add_account = st.form_submit_button("Adicionar conta Google/YouTube", type="primary", use_container_width=True)
2695
2720
  if add_account:
@@ -2763,10 +2788,6 @@ def render_settings():
2763
2788
  st.caption("A YouTube Data API Key é uma credencial Google Cloud separada do OAuth. Só é necessária se escolher o método YouTube Data API para consultar métricas oficiais. Não é necessária para Página pública — sem API Key, para autorizar OAuth ou para fazer upload.")
2764
2789
  youtube_api_key = text_setting("YouTube Data API Key (opcional)", "youtube_api_key", secret=True, help_text="Credencial separada, criada em Google Cloud > APIs e serviços > Credenciais > Chave de API. Não cole aqui o Client ID nem o Client Secret.")
2765
2790
 
2766
- st.caption("As credenciais e parâmetros do Upload directo — cookies, sessionInfo, INNERTUBE_API_KEY, chunk_size e DELEGATED_SESSION_ID — são lidos exclusivamente do documento JSON por conta Google. Não são editados nesta UI.")
2767
- direct_innertube_api_key = str(settings.get("direct_innertube_api_key", "") or "")
2768
- direct_chunk_size = int(settings.get("direct_chunk_size", 262144) or 262144)
2769
-
2770
2791
  with st.expander("Serviço, materiais e rede"):
2771
2792
  cols = st.columns(2)
2772
2793
  with cols[0]:
@@ -2917,7 +2938,6 @@ def render_settings():
2917
2938
  "youtube_client_id": youtube_client_id, "youtube_client_secret": youtube_client_secret,
2918
2939
  "kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
2919
2940
  "apify_api_token": apify_api_token.strip(), "apify_actor_id": apify_actor_id.strip() or DEFAULT_ACTOR_ID, "apify_poll_interval_seconds": int(apify_poll_interval), "apify_run_timeout_seconds": int(apify_run_timeout),
2920
- "direct_innertube_api_key": direct_innertube_api_key, "direct_chunk_size": direct_chunk_size,
2921
2941
  "log_level": log_level, "listen_host": listen_host, "listen_port": listen_port, "video_source": video_source,
2922
2942
  "endpoint": endpoint, "proxy_http": proxy_http, "proxy_https": proxy_https, "match_materials_to_script": match_materials_to_script,
2923
2943
  "llm_provider": llm_provider, "openai_api_key": openai_api_key, "openai_base_url": openai_base_url, "openai_model_name": openai_model_name,
@@ -101,8 +101,7 @@ def parse_credentials_document(content: bytes, filename: str = DIRECT_DOCUMENT_N
101
101
  raise ValueError(f"Faltam cookies obrigatórios no documento {filename}: {', '.join(missing)}.")
102
102
  if not document["sessionInfo"]:
103
103
  raise ValueError(f"Falta sessionInfo no documento {filename}.")
104
- if not document["INNERTUBE_API_KEY"]:
105
- raise ValueError(f"Falta INNERTUBE_API_KEY no documento {filename}.")
104
+ # INNERTUBE_API_KEY é configurada separadamente na UI de Contas Google/YouTube.
106
105
  return document
107
106
 
108
107
 
@@ -128,6 +127,20 @@ def _account_session_info(account: dict[str, Any]) -> str:
128
127
  return str(account.get("sessionInfo") or account.get("session_info") or account.get("direct_session_info") or "").strip()
129
128
 
130
129
 
130
+ def account_innertube_api_key(account: dict[str, Any] | None, document: dict[str, Any] | None = None, settings: dict[str, Any] | None = None) -> str:
131
+ """Return the account-level key; document value is only a legacy migration fallback."""
132
+ account = account or {}
133
+ document = document or {}
134
+ settings = settings or {}
135
+ return str(
136
+ account.get("innertube_api_key")
137
+ or account.get("INNERTUBE_API_KEY")
138
+ or settings.get("direct_innertube_api_key")
139
+ or document.get("INNERTUBE_API_KEY")
140
+ or ""
141
+ ).strip()
142
+
143
+
131
144
  def _normalise_document(raw: Any, account: dict[str, Any]) -> dict[str, Any]:
132
145
  raw = raw if isinstance(raw, dict) else {}
133
146
  cookies = _normalise_pairs(raw.get("cookies", raw))
@@ -187,7 +200,7 @@ def save_credentials_document(storage_root: Path, account: dict[str, Any], docum
187
200
  "email": normalised["email"],
188
201
  "sessionInfo": normalised["sessionInfo"],
189
202
  "cookies": normalised["cookies"],
190
- "INNERTUBE_API_KEY": normalised["INNERTUBE_API_KEY"],
203
+ # INNERTUBE_API_KEY não pertence ao documento de cookies/credenciais.
191
204
  "chunk_size": normalised["chunk_size"],
192
205
  "delegated_session_ids": normalised["delegated_session_ids"],
193
206
  }
@@ -277,7 +290,7 @@ def merge_credentials_document(
277
290
 
278
291
  The upload may contain only cookies or the full Frontend API document. Existing
279
292
  non-empty values are preserved, so uploading cookies never discards sessionInfo,
280
- INNERTUBE_API_KEY, chunk_size or delegated channel IDs.
293
+ chunk_size or delegated channel IDs. INNERTUBE_API_KEY is configured separately.
281
294
  """
282
295
  try:
283
296
  raw = json.loads(content.decode("utf-8-sig", errors="replace"))
@@ -295,8 +308,8 @@ def merge_credentials_document(
295
308
  incoming_session = _placeholder_to_empty(session_info_override) or incoming.get("sessionInfo", "")
296
309
  if incoming_session:
297
310
  merged["sessionInfo"] = incoming_session
298
- if incoming.get("INNERTUBE_API_KEY"):
299
- merged["INNERTUBE_API_KEY"] = incoming["INNERTUBE_API_KEY"]
311
+ # INNERTUBE_API_KEY recebida num JSON é deliberadamente ignorada: a fonte oficial
312
+ # é a configuração separada da secção Contas Google/YouTube.
300
313
  if "chunk_size" in raw:
301
314
  merged["chunk_size"] = incoming["chunk_size"]
302
315
  for key, value in incoming.get("delegated_session_ids", {}).items():
@@ -325,14 +338,16 @@ def document_status(storage_root: Path, account: dict[str, Any], channel: dict[s
325
338
  document = load_credentials_document(storage_root, account, settings, channels, create=True)
326
339
  missing_cookies = [key for key in COOKIE_KEYS if not document["cookies"].get(key)]
327
340
  delegated = delegated_session_id(document, channel or {}) if channel else ""
341
+ innertube_api_key = account_innertube_api_key(account, document, settings)
328
342
  return {
329
343
  "document_file": str(path),
330
344
  "document_exists": path.exists(),
331
345
  "missing_cookies": missing_cookies,
332
346
  "has_session_info": bool(document.get("sessionInfo")),
333
- "has_innertube_api_key": bool(document.get("INNERTUBE_API_KEY")),
347
+ "has_innertube_api_key": bool(innertube_api_key),
348
+ "innertube_api_key": innertube_api_key,
334
349
  "has_delegated_session_id": bool(delegated) if channel is not None else None,
335
- "ready": not missing_cookies and bool(document.get("sessionInfo")) and bool(document.get("INNERTUBE_API_KEY")) and (channel is None or bool(delegated)),
350
+ "ready": not missing_cookies and bool(document.get("sessionInfo")) and bool(innertube_api_key) and (channel is None or bool(delegated)),
336
351
  }
337
352
 
338
353
 
@@ -364,9 +379,9 @@ def save_cookie_file(storage_root: Path, account: dict[str, Any], content: bytes
364
379
  return destination
365
380
 
366
381
 
367
- def direct_account_status(storage_root: Path, account: dict[str, Any]) -> dict[str, Any]:
368
- """Backward-compatible account status; the source of truth is credentials.json."""
369
- status = document_status(storage_root, account)
382
+ def direct_account_status(storage_root: Path, account: dict[str, Any], settings: dict[str, Any] | None = None) -> dict[str, Any]:
383
+ """Return account status; cookies stay in credentials.json and the API key is separate."""
384
+ status = document_status(storage_root, account, settings=settings)
370
385
  return {
371
386
  "cookie_file": status["document_file"],
372
387
  "document_exists": status["document_exists"],
@@ -374,6 +389,7 @@ def direct_account_status(storage_root: Path, account: dict[str, Any]) -> dict[s
374
389
  "missing_cookies": status["missing_cookies"],
375
390
  "has_session_info": status["has_session_info"],
376
391
  "has_innertube_api_key": status["has_innertube_api_key"],
392
+ "innertube_api_key": status.get("innertube_api_key", ""),
377
393
  "ready": status["ready"],
378
394
  "document_file": status["document_file"],
379
395
  }
@@ -9,7 +9,7 @@ from dataclasses import dataclass
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
 
12
- from integrations.youtube_direct_credentials import COOKIE_KEYS, delegated_session_id, load_credentials_document
12
+ from integrations.youtube_direct_credentials import COOKIE_KEYS, account_innertube_api_key, delegated_session_id, load_credentials_document
13
13
  from urllib.parse import quote
14
14
 
15
15
  import requests
@@ -49,7 +49,8 @@ def _session_info(settings: dict[str, Any], channel: dict[str, Any], account: di
49
49
 
50
50
 
51
51
  def _innertube_api_key(settings: dict[str, Any], channel: dict[str, Any], account: dict[str, Any] | None = None, storage_root: Path | None = None) -> str:
52
- return str(_direct_document(settings, channel, account, storage_root).get("INNERTUBE_API_KEY", "") or "").strip()
52
+ document = _direct_document(settings, channel, account, storage_root)
53
+ return account_innertube_api_key(account, document, settings)
53
54
 
54
55
 
55
56
  def _delegated_session(settings: dict[str, Any], channel: dict[str, Any], account: dict[str, Any] | None = None, storage_root: Path | None = None) -> str:
@@ -96,7 +97,7 @@ def validate_direct_upload(video_path: str | Path, channel: dict[str, Any], sett
96
97
  if not _session_info(settings, channel, account, storage_root):
97
98
  return "Falta sessionInfo no documento de credenciais da conta."
98
99
  if not _innertube_api_key(settings, channel, account, storage_root):
99
- return "Falta INNERTUBE_API_KEY no documento de credenciais da conta."
100
+ return "Falta INNERTUBE_API_KEY no cartão da conta Google/YouTube."
100
101
  if not _delegated_session(settings, channel, account, storage_root):
101
102
  return "Falta DELEGATED_SESSION_ID deste canal no documento de credenciais da conta."
102
103
  return None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.60",
3
+ "version": "0.2.61",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "main": "scripts/cli.mjs",
6
6
  "type": "module",