@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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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