@danhachuel/thunderbolt 0.2.67 → 0.2.68
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/MANUAL-INSTALACAO.md +3 -3
- package/README.md +5 -3
- package/app/main.py +278 -216
- package/package.json +1 -1
- package/seed/references/guide-instagram.md +179 -0
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -345,14 +345,14 @@ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
|
|
345
345
|
|
|
346
346
|
## 8. Configuração inicial da UI
|
|
347
347
|
|
|
348
|
-
Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Automação**, **Edição**, **Models AI** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **Models AI** contém **Personagens** e **
|
|
348
|
+
Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **Models AI** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **Models AI** contém **Personagens**, **Redes Sociais** e **Tutorial Meta**, nessa ordem. O **Tutorial Meta** apresenta o guia de configuração de Instagram e credenciais Meta para automações com n8n. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. O menu **Configurações** contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. Abra **Configurações > Contas Google** para gerir contas Google/YouTube, cartões, documentos de Upload directo, `sessionInfo`, `INNERTUBE_API_KEY`, contas adicionais e a configuração global do YouTube. Abra **Configurações > Configuração API** para configurar as restantes API Keys, providers, modelos, serviços e o **Teste de vozes**. A página **Configurações > Notificações** está reservada para futuros eventos locais. Para YouTube, preencha primeiro o par **OAuth Client ID + OAuth Client Secret** em **Contas Google** se pretende autorizar uploads. A **YouTube Data API Key** é uma credencial Google Cloud diferente e fica em **Contas Google**, apenas para o método oficial de métricas; Client ID + Client Secret não formam uma API Key.
|
|
349
349
|
|
|
350
350
|
| Configuração | Finalidade |
|
|
351
351
|
|---|---|
|
|
352
352
|
| Porta Streamlit | Definir a porta local da UI |
|
|
353
353
|
| Pasta MoneyPrinterTurbo | Indicar o clone local que será sincronizado com `config.toml` |
|
|
354
|
-
| YouTube Data API Key (opcional) |
|
|
355
|
-
| YouTube OAuth Client ID |
|
|
354
|
+
| YouTube Data API Key (opcional) | Em **Configurações > Contas Google**, permitir o método oficial Data API para métricas públicas; não é necessária para a página pública nem para OAuth |
|
|
355
|
+
| YouTube OAuth Client ID | Em **Configurações > Contas Google**, identificar a aplicação OAuth 2.0 do Google para autorizar operações autenticadas, como upload |
|
|
356
356
|
| YouTube OAuth Client Secret | Secret do mesmo cliente OAuth 2.0; não é uma Data API Key nem um token de acesso |
|
|
357
357
|
| OpenAI/ NVIDIA NIM API key | Credencial do OpenAI ou NVIDIA Build/NIM, guardada apenas no storage local |
|
|
358
358
|
| OpenAI/ NVIDIA NIM Base URL | Endpoint OpenAI-compatible; por padrão `https://integrate.api.nvidia.com/v1` |
|
package/README.md
CHANGED
|
@@ -38,11 +38,13 @@ Os dados são segredos de sessão. Os valores não aparecem em tabelas ou logs,
|
|
|
38
38
|
|
|
39
39
|
Os adaptadores do MoneyPrinterTurbo e de publicação nas plataformas são ligados pelas configurações locais e pelos pontos de integração em `integrations/`. A UI não inventa dados quando um serviço externo ou credencial não está disponível.
|
|
40
40
|
|
|
41
|
-
## Navegação da UI 0.2.
|
|
41
|
+
## Navegação da UI 0.2.68
|
|
42
42
|
|
|
43
|
-
A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Automação**, **Edição**, **Models AI** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes** e **Editor Python**, nessa ordem. **Models AI** é expansível e contém **Personagens** e **
|
|
43
|
+
A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **Models AI** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes** e **Editor Python**, nessa ordem. **Models AI** é expansível e contém **Personagens**, **Redes Sociais** e **Tutorial Meta**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
|
|
44
44
|
|
|
45
|
-
Dentro de **Configurações
|
|
45
|
+
Dentro de **Configurações > Contas Google**, a UI contém os cartões expansíveis de contas Google/YouTube, `sessionInfo`, documentos de Upload directo, `INNERTUBE_API_KEY`, o formulário **Adicionar outra conta Gmail** e a configuração global do YouTube (OAuth Client ID, OAuth Client Secret e YouTube Data API Key). A página **Configuração API** contém as restantes API Keys, providers, modelos, serviços, materiais, Nano Banana, TikTok, Postiz e o **Teste de vozes**.
|
|
46
|
+
|
|
47
|
+
A página **Models AI > 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** é o centro local reservado para futuros eventos de processamento, publicação e integrações.
|
|
46
48
|
|
|
47
49
|
## Canais Youtube — edição por cartão e vídeos recentes
|
|
48
50
|
|
package/app/main.py
CHANGED
|
@@ -1010,7 +1010,7 @@ def render_channels():
|
|
|
1010
1010
|
if lookup_mode.startswith("Página pública"):
|
|
1011
1011
|
result = youtube.fetch_channel_public(source)
|
|
1012
1012
|
elif not youtube.api_key:
|
|
1013
|
-
result = IntegrationResult(False, "A YouTube Data API Key não está configurada. Escolha a opção Página pública — sem API Key ou configure a chave em Configurações.", {"status": "api_key_not_configured"})
|
|
1013
|
+
result = IntegrationResult(False, "A YouTube Data API Key não está configurada. Escolha a opção Página pública — sem API Key ou configure a chave em Configurações > Contas Google.", {"status": "api_key_not_configured"})
|
|
1014
1014
|
else:
|
|
1015
1015
|
result = youtube.fetch_channel(source)
|
|
1016
1016
|
st.session_state["yt_import"] = result.data
|
|
@@ -1092,7 +1092,7 @@ def render_channels():
|
|
|
1092
1092
|
st.caption("Esta subaba usa a conta Google/YouTube seleccionada para listar os canais que ela gere. Não lê a caixa Gmail e não usa e-mails como pesquisa pública.")
|
|
1093
1093
|
accounts = [account for account in settings.get("youtube_batch_accounts", []) if isinstance(account, dict) and account.get("id")]
|
|
1094
1094
|
if not accounts:
|
|
1095
|
-
st.info("Configure primeiro uma conta em Configurações >
|
|
1095
|
+
st.info("Configure primeiro uma conta em Configurações > Configuração API > Contas Google/YouTube para canais em lote.")
|
|
1096
1096
|
else:
|
|
1097
1097
|
account_ids = [str(account["id"]) for account in accounts]
|
|
1098
1098
|
account_labels = {str(account["id"]): f"{account.get('email', 'Conta Google')} — {account.get('label', 'Canais YouTube')}" for account in accounts}
|
|
@@ -1545,7 +1545,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1545
1545
|
payload["thumbnail_path"] = thumbnail_path
|
|
1546
1546
|
payload["thumbnail_status"] = "generated"
|
|
1547
1547
|
else:
|
|
1548
|
-
st.caption("A imagem ainda não foi gerada. Configure a API key em
|
|
1548
|
+
st.caption("A imagem ainda não foi gerada. Configure a API key em Configuração API > API Keys.")
|
|
1549
1549
|
st.caption(f"Estado da thumbnail: {payload.get('thumbnail_status', 'prompt_ready')} · texto: {payload.get('thumbnail_text') or 'sem texto'}")
|
|
1550
1550
|
else:
|
|
1551
1551
|
topic_for_creative = str(st.session_state.get("new_video_topic", "") or "").strip()
|
|
@@ -1599,7 +1599,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1599
1599
|
payload["thumbnail_path"] = thumbnail_path
|
|
1600
1600
|
payload["thumbnail_status"] = "generated"
|
|
1601
1601
|
else:
|
|
1602
|
-
st.info("Escolha a variante e clique em **Gerar imagem da thumbnail com Nano Banana**. A API key é configurada em
|
|
1602
|
+
st.info("Escolha a variante e clique em **Gerar imagem da thumbnail com Nano Banana**. A API key é configurada em Configuração API > API Keys.")
|
|
1603
1603
|
st.session_state["new_video_creative_payload"] = payload
|
|
1604
1604
|
|
|
1605
1605
|
with st.form("new_video_form"):
|
|
@@ -2588,11 +2588,11 @@ def render_upload():
|
|
|
2588
2588
|
|
|
2589
2589
|
def render_upload_postiz():
|
|
2590
2590
|
st.subheader("Upload para Postiz")
|
|
2591
|
-
st.caption("O Thunderbolt envia primeiro o MP4 para o Postiz e cria um post na integração seleccionada. A API key e o servidor são configurados em
|
|
2591
|
+
st.caption("O Thunderbolt envia primeiro o MP4 para o Postiz e cria um post na integração seleccionada. A API key e o servidor são configurados em Configuração API.")
|
|
2592
2592
|
settings = read_json("settings.json", {})
|
|
2593
2593
|
postiz = PostizAdapter(settings)
|
|
2594
2594
|
if not settings.get("postiz_enabled"):
|
|
2595
|
-
st.warning("Postiz está desactivado. Active-o em
|
|
2595
|
+
st.warning("Postiz está desactivado. Active-o em Configuração API > API Keys e guarde a API key antes de enviar.")
|
|
2596
2596
|
if not postiz.api_key:
|
|
2597
2597
|
st.info("Nenhuma API key Postiz configurada.")
|
|
2598
2598
|
return
|
|
@@ -2619,7 +2619,7 @@ def render_upload_postiz():
|
|
|
2619
2619
|
format_func=lambda value: integration_labels.get(value, value),
|
|
2620
2620
|
key="postiz_upload_integration",
|
|
2621
2621
|
)
|
|
2622
|
-
st.caption("Para alterar a integração padrão, guarde o ID seleccionado em
|
|
2622
|
+
st.caption("Para alterar a integração padrão, guarde o ID seleccionado em Configuração API.")
|
|
2623
2623
|
else:
|
|
2624
2624
|
selected_integration = st.text_input("ID da integração Postiz", value=integration_default, key="postiz_upload_integration_manual", help="Carregue as integrações para seleccionar uma conta ou introduza o ID devolvido pela API do Postiz.").strip()
|
|
2625
2625
|
st.info("Carregue as integrações para descobrir os canais Postiz ligados à sua conta.")
|
|
@@ -2758,7 +2758,7 @@ def render_upload_conventional():
|
|
|
2758
2758
|
postiz_status = postiz.status()
|
|
2759
2759
|
(st.success if postiz_status.ok else st.warning)(postiz_status.message)
|
|
2760
2760
|
else:
|
|
2761
|
-
st.caption("Postiz está desactivado; active-o em
|
|
2761
|
+
st.caption("Postiz está desactivado; active-o em Configuração API para o usar como fallback final.")
|
|
2762
2762
|
status_cols = st.columns(2)
|
|
2763
2763
|
with status_cols[0]:
|
|
2764
2764
|
(st.success if status["agent"].ok else st.warning)(f"Agente: {status['agent'].message}")
|
|
@@ -2861,9 +2861,239 @@ def render_upload_conventional():
|
|
|
2861
2861
|
st.button("Preparar Facebook Pages", key=f"upload_facebook_{task['id']}", disabled=True, help="UI preparada; publicação Facebook Pages ainda não está activa.")
|
|
2862
2862
|
|
|
2863
2863
|
|
|
2864
|
+
def render_google_accounts():
|
|
2865
|
+
st.title("Contas Google")
|
|
2866
|
+
st.caption("Gestão das contas Google/YouTube, documentos de credenciais, canais em lote e credenciais globais do YouTube.")
|
|
2867
|
+
settings = read_json("settings.json", {})
|
|
2868
|
+
|
|
2869
|
+
st.subheader("Contas Google/YouTube — canais em lote")
|
|
2870
|
+
st.caption("Abra uma conta para ver e editar os detalhes. O e-mail identifica a conta Google; esta área não lê a caixa Gmail.")
|
|
2871
|
+
batch_accounts = [account for account in settings.get("youtube_batch_accounts", []) if isinstance(account, dict) and account.get("id")]
|
|
2872
|
+
channel_state = read_json("channels.json", [])
|
|
2873
|
+
youtube_accounts_missing_document: list[str] = []
|
|
2874
|
+
|
|
2875
|
+
for batch_account in batch_accounts:
|
|
2876
|
+
account_id = str(batch_account["id"])
|
|
2877
|
+
account_email_snapshot = str(batch_account.get("email") or "sem e-mail")
|
|
2878
|
+
account_label_snapshot = str(batch_account.get("label") or "Canais YouTube")
|
|
2879
|
+
ensure_credentials_document(STORAGE, batch_account, settings, channel_state)
|
|
2880
|
+
direct_status = direct_account_status(STORAGE, batch_account, settings)
|
|
2881
|
+
missing_document_parts = list(direct_status.get("missing_cookies", []))
|
|
2882
|
+
if not direct_status.get("has_session_info"):
|
|
2883
|
+
missing_document_parts.append("sessionInfo")
|
|
2884
|
+
if missing_document_parts:
|
|
2885
|
+
youtube_accounts_missing_document.append(account_email_snapshot)
|
|
2886
|
+
|
|
2887
|
+
with st.expander(f"{account_label_snapshot} — {account_email_snapshot}", expanded=False):
|
|
2888
|
+
with st.form(f"batch_account_form_{account_id}"):
|
|
2889
|
+
account_cols = st.columns(2)
|
|
2890
|
+
with account_cols[0]:
|
|
2891
|
+
account_label = st.text_input("Nome da conta", value=account_label_snapshot, key=f"batch_label_{account_id}")
|
|
2892
|
+
account_email = st.text_input("E-mail/Gmail da conta", value=account_email_snapshot if account_email_snapshot != "sem e-mail" else "", key=f"batch_email_{account_id}")
|
|
2893
|
+
account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
|
|
2894
|
+
with account_cols[1]:
|
|
2895
|
+
account_client_secret = st.text_input("OAuth Client Secret", value=str(batch_account.get("client_secret", "")), type="password", key=f"batch_client_secret_{account_id}")
|
|
2896
|
+
account_session_info = st.text_input(
|
|
2897
|
+
"sessionInfo token desta conta Google",
|
|
2898
|
+
value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
|
|
2899
|
+
type="password",
|
|
2900
|
+
key=f"batch_session_info_{account_id}",
|
|
2901
|
+
help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json; os cookies e restantes valores continuam exclusivamente no documento.",
|
|
2902
|
+
)
|
|
2903
|
+
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", use_container_width=True)
|
|
2904
|
+
|
|
2905
|
+
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
2906
|
+
st.caption("O documento padrão é criado automaticamente. Suba um JSON completo ou apenas o documento de cookies; os valores preenchidos são incorporados e mantidos em credentials.json.")
|
|
2907
|
+
document_upload = st.file_uploader(
|
|
2908
|
+
"Subir documento de cookies/credenciais",
|
|
2909
|
+
type=["json"],
|
|
2910
|
+
key=f"direct_credentials_document_{account_id}",
|
|
2911
|
+
help="Aceita o JSON do YouTube-Video-Upload-Frontend-Api. Um documento parcial de cookies também é incorporado sem apagar os restantes campos.",
|
|
2912
|
+
)
|
|
2913
|
+
if missing_document_parts:
|
|
2914
|
+
st.warning(f"Documento incompleto: {', '.join(missing_document_parts)}")
|
|
2915
|
+
else:
|
|
2916
|
+
st.success("Documento completo para a conta Google")
|
|
2917
|
+
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
2918
|
+
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", use_container_width=True)
|
|
2919
|
+
|
|
2920
|
+
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
2921
|
+
status_cols = st.columns(3)
|
|
2922
|
+
with status_cols[0]:
|
|
2923
|
+
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
2924
|
+
with status_cols[1]:
|
|
2925
|
+
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}", use_container_width=True):
|
|
2926
|
+
result = authorize_youtube_batch_account(batch_account, STORAGE)
|
|
2927
|
+
(st.success if result.ok else st.error)(result.message)
|
|
2928
|
+
if result.ok:
|
|
2929
|
+
st.rerun()
|
|
2930
|
+
with status_cols[2]:
|
|
2931
|
+
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", use_container_width=True):
|
|
2932
|
+
delete_youtube_batch_token(batch_account, STORAGE)
|
|
2933
|
+
delete_credentials_document(STORAGE, batch_account)
|
|
2934
|
+
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
2935
|
+
settings["youtube_batch_accounts"] = remaining_accounts
|
|
2936
|
+
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
2937
|
+
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
2938
|
+
for channel in channel_state:
|
|
2939
|
+
if str(channel.get("google_account_id") or "") == account_id:
|
|
2940
|
+
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
2941
|
+
write_json("channels.json", channel_state)
|
|
2942
|
+
write_json("settings.json", settings)
|
|
2943
|
+
st.rerun()
|
|
2944
|
+
|
|
2945
|
+
if save_account:
|
|
2946
|
+
if "@" not in account_email.strip():
|
|
2947
|
+
st.error("Informe um e-mail Google válido.")
|
|
2948
|
+
elif not account_client_id.strip() or not account_client_secret.strip():
|
|
2949
|
+
st.error("Informe o Client ID e o Client Secret desta conta.")
|
|
2950
|
+
else:
|
|
2951
|
+
for existing in batch_accounts:
|
|
2952
|
+
if str(existing.get("id")) == account_id:
|
|
2953
|
+
credentials_changed = any(existing.get(field, "") != value for field, value in (("email", account_email.strip()), ("client_id", account_client_id.strip()), ("client_secret", account_client_secret.strip())))
|
|
2954
|
+
if credentials_changed:
|
|
2955
|
+
delete_youtube_batch_token(existing, STORAGE)
|
|
2956
|
+
existing.update({"label": account_label.strip() or "Canais YouTube", "email": account_email.strip(), "client_id": account_client_id.strip(), "client_secret": account_client_secret.strip(), "sessionInfo": account_session_info.strip()})
|
|
2957
|
+
update_credentials_document_session_info(STORAGE, existing, account_session_info.strip())
|
|
2958
|
+
ensure_credentials_document(STORAGE, existing, settings, channel_state)
|
|
2959
|
+
settings["youtube_batch_accounts"] = batch_accounts
|
|
2960
|
+
write_json("settings.json", settings)
|
|
2961
|
+
st.success("Conta Google/YouTube guardada.")
|
|
2962
|
+
st.rerun()
|
|
2963
|
+
|
|
2964
|
+
if document_save:
|
|
2965
|
+
if document_upload is None:
|
|
2966
|
+
st.error("Seleccione um documento JSON de cookies/credenciais antes de guardar.")
|
|
2967
|
+
else:
|
|
2968
|
+
try:
|
|
2969
|
+
merge_credentials_document(
|
|
2970
|
+
STORAGE,
|
|
2971
|
+
batch_account,
|
|
2972
|
+
document_upload.getvalue(),
|
|
2973
|
+
document_upload.name,
|
|
2974
|
+
session_info_override=str(batch_account.get("sessionInfo") or ""),
|
|
2975
|
+
channels=channel_state,
|
|
2976
|
+
)
|
|
2977
|
+
st.success("Documento incorporado e guardado nesta conta Google.")
|
|
2978
|
+
st.rerun()
|
|
2979
|
+
except ValueError as exc:
|
|
2980
|
+
st.error(str(exc))
|
|
2981
|
+
|
|
2982
|
+
if youtube_accounts_missing_document:
|
|
2983
|
+
st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
|
|
2984
|
+
|
|
2985
|
+
st.divider()
|
|
2986
|
+
st.markdown("### INNERTUBE_API_KEY")
|
|
2987
|
+
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.")
|
|
2988
|
+
account_key_options = [str(account.get("id")) for account in batch_accounts if account.get("id")]
|
|
2989
|
+
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")}
|
|
2990
|
+
if account_key_options:
|
|
2991
|
+
with st.form("innertube_api_key_form"):
|
|
2992
|
+
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")
|
|
2993
|
+
selected_key_account = next(account for account in batch_accounts if str(account.get("id")) == selected_key_account_id)
|
|
2994
|
+
current_innertube_api_key = direct_account_status(STORAGE, selected_key_account, settings).get("innertube_api_key", "")
|
|
2995
|
+
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.")
|
|
2996
|
+
save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY", type="primary", use_container_width=True)
|
|
2997
|
+
if save_innertube_api_key:
|
|
2998
|
+
selected_key_account["innertube_api_key"] = innertube_api_key_value.strip()
|
|
2999
|
+
selected_key_account.pop("INNERTUBE_API_KEY", None)
|
|
3000
|
+
settings.pop("direct_innertube_api_key", None)
|
|
3001
|
+
settings["youtube_batch_accounts"] = batch_accounts
|
|
3002
|
+
write_json("settings.json", settings)
|
|
3003
|
+
document = load_credentials_document(STORAGE, selected_key_account, settings, channel_state, create=True)
|
|
3004
|
+
save_credentials_document(STORAGE, selected_key_account, document)
|
|
3005
|
+
st.success("INNERTUBE_API_KEY guardada na configuração da conta Google/YouTube, fora do documento de cookies.")
|
|
3006
|
+
st.rerun()
|
|
3007
|
+
else:
|
|
3008
|
+
st.info("Adicione primeiro uma conta Google/YouTube para configurar a INNERTUBE_API_KEY.")
|
|
3009
|
+
|
|
3010
|
+
st.divider()
|
|
3011
|
+
st.markdown("### Adicionar outra conta Gmail")
|
|
3012
|
+
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.")
|
|
3013
|
+
with st.form("add_batch_account_form"):
|
|
3014
|
+
add_cols = st.columns(2)
|
|
3015
|
+
with add_cols[0]:
|
|
3016
|
+
new_account_label = st.text_input("Nome da nova conta", value="Canais YouTube", key="new_batch_account_label")
|
|
3017
|
+
new_account_email = st.text_input("E-mail/Gmail", key="new_batch_account_email")
|
|
3018
|
+
new_account_client_id = st.text_input("OAuth Client ID", key="new_batch_account_client_id")
|
|
3019
|
+
with add_cols[1]:
|
|
3020
|
+
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
3021
|
+
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.")
|
|
3022
|
+
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.")
|
|
3023
|
+
add_account = st.form_submit_button("Adicionar conta Google/YouTube", type="primary", use_container_width=True)
|
|
3024
|
+
if add_account:
|
|
3025
|
+
document_error = ""
|
|
3026
|
+
if "@" not in new_account_email.strip():
|
|
3027
|
+
st.error("Informe um e-mail Google válido.")
|
|
3028
|
+
elif not new_account_client_id.strip() or not new_account_client_secret.strip():
|
|
3029
|
+
st.error("Informe o Client ID e o Client Secret da nova conta.")
|
|
3030
|
+
elif new_account_document is not None:
|
|
3031
|
+
try:
|
|
3032
|
+
raw_document = new_account_document.getvalue()
|
|
3033
|
+
json.loads(raw_document.decode("utf-8-sig", errors="replace"))
|
|
3034
|
+
except (UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc:
|
|
3035
|
+
document_error = f"O documento {new_account_document.name} deve ser JSON válido."
|
|
3036
|
+
st.error(document_error)
|
|
3037
|
+
if "@" in new_account_email.strip() and new_account_client_id.strip() and new_account_client_secret.strip() and not document_error:
|
|
3038
|
+
new_account = {"id": f"google_batch_{uuid.uuid4().hex[:12]}", "label": new_account_label.strip() or "Canais YouTube", "email": new_account_email.strip(), "client_id": new_account_client_id.strip(), "client_secret": new_account_client_secret.strip(), "sessionInfo": new_account_session_info.strip()}
|
|
3039
|
+
batch_accounts.append(new_account)
|
|
3040
|
+
settings["youtube_batch_accounts"] = batch_accounts
|
|
3041
|
+
settings["youtube_batch_selected_account_id"] = new_account["id"]
|
|
3042
|
+
write_json("settings.json", settings)
|
|
3043
|
+
ensure_credentials_document(STORAGE, new_account, settings, channel_state)
|
|
3044
|
+
if new_account_document is not None:
|
|
3045
|
+
try:
|
|
3046
|
+
merge_credentials_document(STORAGE, new_account, new_account_document.getvalue(), new_account_document.name, session_info_override=new_account_session_info.strip(), channels=channel_state)
|
|
3047
|
+
except ValueError as exc:
|
|
3048
|
+
st.error(str(exc))
|
|
3049
|
+
st.stop()
|
|
3050
|
+
st.success(f"Conta {new_account['email']} adicionada e documento incorporado.")
|
|
3051
|
+
else:
|
|
3052
|
+
st.warning(f"Conta {new_account['email']} adicionada. Foi criado um credentials.json padrão; suba o documento de cookies nesta conta quando estiver pronto.")
|
|
3053
|
+
st.rerun()
|
|
3054
|
+
|
|
3055
|
+
st.divider()
|
|
3056
|
+
st.subheader("Configuração global do YouTube")
|
|
3057
|
+
st.caption("Estas credenciais pertencem à aplicação Google/YouTube e ficam separadas das restantes APIs do Thunderbolt.")
|
|
3058
|
+
with st.form("google_global_api_settings_form"):
|
|
3059
|
+
google_api_cols = st.columns(2)
|
|
3060
|
+
with google_api_cols[0]:
|
|
3061
|
+
youtube_client_id = st.text_input(
|
|
3062
|
+
"YouTube OAuth Client ID",
|
|
3063
|
+
value=str(settings.get("youtube_client_id", "") or ""),
|
|
3064
|
+
key="google_page_youtube_client_id",
|
|
3065
|
+
help="Client ID OAuth 2.0 criado no Google Cloud para autorizar a conta YouTube.",
|
|
3066
|
+
)
|
|
3067
|
+
with google_api_cols[1]:
|
|
3068
|
+
youtube_client_secret = st.text_input(
|
|
3069
|
+
"YouTube OAuth Client Secret",
|
|
3070
|
+
value=str(settings.get("youtube_client_secret", "") or ""),
|
|
3071
|
+
type="password",
|
|
3072
|
+
key="google_page_youtube_client_secret",
|
|
3073
|
+
help="Client Secret do mesmo cliente OAuth 2.0. Não é uma API Key.",
|
|
3074
|
+
)
|
|
3075
|
+
st.caption(f"OAuth local: use um cliente do tipo Desktop app. Se o Google Cloud pedir uma URI autorizada, registe exactamente `{loopback_redirect_uri()}`.")
|
|
3076
|
+
youtube_api_key = st.text_input(
|
|
3077
|
+
"YouTube Data API Key (opcional)",
|
|
3078
|
+
value=str(settings.get("youtube_api_key", "") or ""),
|
|
3079
|
+
type="password",
|
|
3080
|
+
key="google_page_youtube_api_key",
|
|
3081
|
+
help="Credencial Google Cloud separada, usada apenas para métricas oficiais da YouTube Data API.",
|
|
3082
|
+
)
|
|
3083
|
+
save_google_global = st.form_submit_button("Guardar configuração global do YouTube", type="primary", use_container_width=True)
|
|
3084
|
+
if save_google_global:
|
|
3085
|
+
settings.update({
|
|
3086
|
+
"youtube_client_id": youtube_client_id.strip(),
|
|
3087
|
+
"youtube_client_secret": youtube_client_secret.strip(),
|
|
3088
|
+
"youtube_api_key": youtube_api_key.strip(),
|
|
3089
|
+
})
|
|
3090
|
+
write_json("settings.json", settings)
|
|
3091
|
+
st.success("Configuração global do YouTube guardada em Contas Google.")
|
|
3092
|
+
st.rerun()
|
|
3093
|
+
|
|
2864
3094
|
def render_settings():
|
|
2865
|
-
st.title("
|
|
2866
|
-
st.caption("Configuração
|
|
3095
|
+
st.title("Configuração API")
|
|
3096
|
+
st.caption("Configuração das APIs, providers, serviços e ferramentas técnicas usados pelo Thunderbolt. As credenciais ficam no storage local e não são enviadas para o GitHub.")
|
|
2867
3097
|
settings = read_json("settings.json", {})
|
|
2868
3098
|
|
|
2869
3099
|
def text_setting(label: str, key: str, *, secret: bool = False, help_text: str | None = None) -> str:
|
|
@@ -2875,209 +3105,13 @@ def render_settings():
|
|
|
2875
3105
|
key=f"settings_{key}",
|
|
2876
3106
|
)
|
|
2877
3107
|
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
with google_accounts_tab:
|
|
2881
|
-
st.subheader("Contas Google/YouTube — canais em lote")
|
|
2882
|
-
st.caption("Abra uma conta para ver e editar os detalhes. O e-mail identifica a conta Google; esta área não lê a caixa Gmail.")
|
|
2883
|
-
batch_accounts = [account for account in settings.get("youtube_batch_accounts", []) if isinstance(account, dict) and account.get("id")]
|
|
2884
|
-
channel_state = read_json("channels.json", [])
|
|
2885
|
-
youtube_accounts_missing_document: list[str] = []
|
|
2886
|
-
|
|
2887
|
-
for batch_account in batch_accounts:
|
|
2888
|
-
account_id = str(batch_account["id"])
|
|
2889
|
-
account_email_snapshot = str(batch_account.get("email") or "sem e-mail")
|
|
2890
|
-
account_label_snapshot = str(batch_account.get("label") or "Canais YouTube")
|
|
2891
|
-
ensure_credentials_document(STORAGE, batch_account, settings, channel_state)
|
|
2892
|
-
direct_status = direct_account_status(STORAGE, batch_account, settings)
|
|
2893
|
-
missing_document_parts = list(direct_status.get("missing_cookies", []))
|
|
2894
|
-
if not direct_status.get("has_session_info"):
|
|
2895
|
-
missing_document_parts.append("sessionInfo")
|
|
2896
|
-
if missing_document_parts:
|
|
2897
|
-
youtube_accounts_missing_document.append(account_email_snapshot)
|
|
2898
|
-
|
|
2899
|
-
with st.expander(f"{account_label_snapshot} — {account_email_snapshot}", expanded=False):
|
|
2900
|
-
with st.form(f"batch_account_form_{account_id}"):
|
|
2901
|
-
account_cols = st.columns(2)
|
|
2902
|
-
with account_cols[0]:
|
|
2903
|
-
account_label = st.text_input("Nome da conta", value=account_label_snapshot, key=f"batch_label_{account_id}")
|
|
2904
|
-
account_email = st.text_input("E-mail/Gmail da conta", value=account_email_snapshot if account_email_snapshot != "sem e-mail" else "", key=f"batch_email_{account_id}")
|
|
2905
|
-
account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
|
|
2906
|
-
with account_cols[1]:
|
|
2907
|
-
account_client_secret = st.text_input("OAuth Client Secret", value=str(batch_account.get("client_secret", "")), type="password", key=f"batch_client_secret_{account_id}")
|
|
2908
|
-
account_session_info = st.text_input(
|
|
2909
|
-
"sessionInfo token desta conta Google",
|
|
2910
|
-
value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
|
|
2911
|
-
type="password",
|
|
2912
|
-
key=f"batch_session_info_{account_id}",
|
|
2913
|
-
help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json; os cookies e restantes valores continuam exclusivamente no documento.",
|
|
2914
|
-
)
|
|
2915
|
-
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", use_container_width=True)
|
|
2916
|
-
|
|
2917
|
-
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
2918
|
-
st.caption("O documento padrão é criado automaticamente. Suba um JSON completo ou apenas o documento de cookies; os valores preenchidos são incorporados e mantidos em credentials.json.")
|
|
2919
|
-
document_upload = st.file_uploader(
|
|
2920
|
-
"Subir documento de cookies/credenciais",
|
|
2921
|
-
type=["json"],
|
|
2922
|
-
key=f"direct_credentials_document_{account_id}",
|
|
2923
|
-
help="Aceita o JSON do YouTube-Video-Upload-Frontend-Api. Um documento parcial de cookies também é incorporado sem apagar os restantes campos.",
|
|
2924
|
-
)
|
|
2925
|
-
if missing_document_parts:
|
|
2926
|
-
st.warning(f"Documento incompleto: {', '.join(missing_document_parts)}")
|
|
2927
|
-
else:
|
|
2928
|
-
st.success("Documento completo para a conta Google")
|
|
2929
|
-
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
2930
|
-
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", use_container_width=True)
|
|
2931
|
-
|
|
2932
|
-
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
2933
|
-
status_cols = st.columns(3)
|
|
2934
|
-
with status_cols[0]:
|
|
2935
|
-
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
2936
|
-
with status_cols[1]:
|
|
2937
|
-
if st.button("Autorizar/Reautorizar", key=f"batch_authorize_settings_{account_id}", use_container_width=True):
|
|
2938
|
-
result = authorize_youtube_batch_account(batch_account, STORAGE)
|
|
2939
|
-
(st.success if result.ok else st.error)(result.message)
|
|
2940
|
-
if result.ok:
|
|
2941
|
-
st.rerun()
|
|
2942
|
-
with status_cols[2]:
|
|
2943
|
-
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", use_container_width=True):
|
|
2944
|
-
delete_youtube_batch_token(batch_account, STORAGE)
|
|
2945
|
-
delete_credentials_document(STORAGE, batch_account)
|
|
2946
|
-
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
2947
|
-
settings["youtube_batch_accounts"] = remaining_accounts
|
|
2948
|
-
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
2949
|
-
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
2950
|
-
for channel in channel_state:
|
|
2951
|
-
if str(channel.get("google_account_id") or "") == account_id:
|
|
2952
|
-
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
2953
|
-
write_json("channels.json", channel_state)
|
|
2954
|
-
write_json("settings.json", settings)
|
|
2955
|
-
st.rerun()
|
|
2956
|
-
|
|
2957
|
-
if save_account:
|
|
2958
|
-
if "@" not in account_email.strip():
|
|
2959
|
-
st.error("Informe um e-mail Google válido.")
|
|
2960
|
-
elif not account_client_id.strip() or not account_client_secret.strip():
|
|
2961
|
-
st.error("Informe o Client ID e o Client Secret desta conta.")
|
|
2962
|
-
else:
|
|
2963
|
-
for existing in batch_accounts:
|
|
2964
|
-
if str(existing.get("id")) == account_id:
|
|
2965
|
-
credentials_changed = any(existing.get(field, "") != value for field, value in (("email", account_email.strip()), ("client_id", account_client_id.strip()), ("client_secret", account_client_secret.strip())))
|
|
2966
|
-
if credentials_changed:
|
|
2967
|
-
delete_youtube_batch_token(existing, STORAGE)
|
|
2968
|
-
existing.update({"label": account_label.strip() or "Canais YouTube", "email": account_email.strip(), "client_id": account_client_id.strip(), "client_secret": account_client_secret.strip(), "sessionInfo": account_session_info.strip()})
|
|
2969
|
-
update_credentials_document_session_info(STORAGE, existing, account_session_info.strip())
|
|
2970
|
-
ensure_credentials_document(STORAGE, existing, settings, channel_state)
|
|
2971
|
-
settings["youtube_batch_accounts"] = batch_accounts
|
|
2972
|
-
write_json("settings.json", settings)
|
|
2973
|
-
st.success("Conta Google/YouTube guardada.")
|
|
2974
|
-
st.rerun()
|
|
2975
|
-
|
|
2976
|
-
if document_save:
|
|
2977
|
-
if document_upload is None:
|
|
2978
|
-
st.error("Seleccione um documento JSON de cookies/credenciais antes de guardar.")
|
|
2979
|
-
else:
|
|
2980
|
-
try:
|
|
2981
|
-
merge_credentials_document(
|
|
2982
|
-
STORAGE,
|
|
2983
|
-
batch_account,
|
|
2984
|
-
document_upload.getvalue(),
|
|
2985
|
-
document_upload.name,
|
|
2986
|
-
session_info_override=str(batch_account.get("sessionInfo") or ""),
|
|
2987
|
-
channels=channel_state,
|
|
2988
|
-
)
|
|
2989
|
-
st.success("Documento incorporado e guardado nesta conta Google.")
|
|
2990
|
-
st.rerun()
|
|
2991
|
-
except ValueError as exc:
|
|
2992
|
-
st.error(str(exc))
|
|
2993
|
-
|
|
2994
|
-
if youtube_accounts_missing_document:
|
|
2995
|
-
st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
|
|
2996
|
-
|
|
2997
|
-
st.divider()
|
|
2998
|
-
st.markdown("### INNERTUBE_API_KEY")
|
|
2999
|
-
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.")
|
|
3000
|
-
account_key_options = [str(account.get("id")) for account in batch_accounts if account.get("id")]
|
|
3001
|
-
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")}
|
|
3002
|
-
if account_key_options:
|
|
3003
|
-
with st.form("innertube_api_key_form"):
|
|
3004
|
-
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")
|
|
3005
|
-
selected_key_account = next(account for account in batch_accounts if str(account.get("id")) == selected_key_account_id)
|
|
3006
|
-
current_innertube_api_key = direct_account_status(STORAGE, selected_key_account, settings).get("innertube_api_key", "")
|
|
3007
|
-
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.")
|
|
3008
|
-
save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY", type="primary", use_container_width=True)
|
|
3009
|
-
if save_innertube_api_key:
|
|
3010
|
-
selected_key_account["innertube_api_key"] = innertube_api_key_value.strip()
|
|
3011
|
-
selected_key_account.pop("INNERTUBE_API_KEY", None)
|
|
3012
|
-
settings.pop("direct_innertube_api_key", None)
|
|
3013
|
-
settings["youtube_batch_accounts"] = batch_accounts
|
|
3014
|
-
write_json("settings.json", settings)
|
|
3015
|
-
document = load_credentials_document(STORAGE, selected_key_account, settings, channel_state, create=True)
|
|
3016
|
-
save_credentials_document(STORAGE, selected_key_account, document)
|
|
3017
|
-
st.success("INNERTUBE_API_KEY guardada na configuração da conta Google/YouTube, fora do documento de cookies.")
|
|
3018
|
-
st.rerun()
|
|
3019
|
-
else:
|
|
3020
|
-
st.info("Adicione primeiro uma conta Google/YouTube para configurar a INNERTUBE_API_KEY.")
|
|
3021
|
-
|
|
3022
|
-
st.divider()
|
|
3023
|
-
st.markdown("### Adicionar outra conta Gmail")
|
|
3024
|
-
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.")
|
|
3025
|
-
with st.form("add_batch_account_form"):
|
|
3026
|
-
add_cols = st.columns(2)
|
|
3027
|
-
with add_cols[0]:
|
|
3028
|
-
new_account_label = st.text_input("Nome da nova conta", value="Canais YouTube", key="new_batch_account_label")
|
|
3029
|
-
new_account_email = st.text_input("E-mail/Gmail", key="new_batch_account_email")
|
|
3030
|
-
new_account_client_id = st.text_input("OAuth Client ID", key="new_batch_account_client_id")
|
|
3031
|
-
with add_cols[1]:
|
|
3032
|
-
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
3033
|
-
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.")
|
|
3034
|
-
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.")
|
|
3035
|
-
add_account = st.form_submit_button("Adicionar conta Google/YouTube", type="primary", use_container_width=True)
|
|
3036
|
-
if add_account:
|
|
3037
|
-
document_error = ""
|
|
3038
|
-
if "@" not in new_account_email.strip():
|
|
3039
|
-
st.error("Informe um e-mail Google válido.")
|
|
3040
|
-
elif not new_account_client_id.strip() or not new_account_client_secret.strip():
|
|
3041
|
-
st.error("Informe o Client ID e o Client Secret da nova conta.")
|
|
3042
|
-
elif new_account_document is not None:
|
|
3043
|
-
try:
|
|
3044
|
-
raw_document = new_account_document.getvalue()
|
|
3045
|
-
json.loads(raw_document.decode("utf-8-sig", errors="replace"))
|
|
3046
|
-
except (UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc:
|
|
3047
|
-
document_error = f"O documento {new_account_document.name} deve ser JSON válido."
|
|
3048
|
-
st.error(document_error)
|
|
3049
|
-
if "@" in new_account_email.strip() and new_account_client_id.strip() and new_account_client_secret.strip() and not document_error:
|
|
3050
|
-
new_account = {"id": f"google_batch_{uuid.uuid4().hex[:12]}", "label": new_account_label.strip() or "Canais YouTube", "email": new_account_email.strip(), "client_id": new_account_client_id.strip(), "client_secret": new_account_client_secret.strip(), "sessionInfo": new_account_session_info.strip()}
|
|
3051
|
-
batch_accounts.append(new_account)
|
|
3052
|
-
settings["youtube_batch_accounts"] = batch_accounts
|
|
3053
|
-
settings["youtube_batch_selected_account_id"] = new_account["id"]
|
|
3054
|
-
write_json("settings.json", settings)
|
|
3055
|
-
ensure_credentials_document(STORAGE, new_account, settings, channel_state)
|
|
3056
|
-
if new_account_document is not None:
|
|
3057
|
-
try:
|
|
3058
|
-
merge_credentials_document(STORAGE, new_account, new_account_document.getvalue(), new_account_document.name, session_info_override=new_account_session_info.strip(), channels=channel_state)
|
|
3059
|
-
except ValueError as exc:
|
|
3060
|
-
st.error(str(exc))
|
|
3061
|
-
st.stop()
|
|
3062
|
-
st.success(f"Conta {new_account['email']} adicionada e documento incorporado.")
|
|
3063
|
-
else:
|
|
3064
|
-
st.warning(f"Conta {new_account['email']} adicionada. Foi criado um credentials.json padrão; suba o documento de cookies nesta conta quando estiver pronto.")
|
|
3065
|
-
st.rerun()
|
|
3108
|
+
api_keys_tab, voice_test_tab = st.tabs(["API Keys", "Teste de vozes"])
|
|
3066
3109
|
|
|
3067
3110
|
with api_keys_tab:
|
|
3068
3111
|
with st.form("settings_form"):
|
|
3069
3112
|
st.subheader("API Keys")
|
|
3070
3113
|
port = st.number_input("Porta Streamlit", 1, 65535, int(settings.get("port", 3030)))
|
|
3071
3114
|
moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
|
|
3072
|
-
st.markdown("**YouTube — OAuth 2.0 e consulta pública**")
|
|
3073
|
-
st.caption("Para autorizar uploads, preencha apenas o YouTube OAuth Client ID e o YouTube OAuth Client Secret. Depois, autorize o agente na aba Upload. Estes dados identificam a aplicação OAuth; não são uma Data API Key nem um token de acesso.")
|
|
3074
|
-
st.info(f"OAuth local: use um cliente do tipo Desktop app. Se o Google Cloud pedir uma URI autorizada, registe exactamente `{loopback_redirect_uri()}`.")
|
|
3075
|
-
youtube_cols = st.columns(2)
|
|
3076
|
-
with youtube_cols[0]:
|
|
3077
|
-
youtube_client_id = text_setting("YouTube OAuth Client ID", "youtube_client_id", help_text="Client ID do OAuth 2.0 criado no Google Cloud. É usado para iniciar a autorização da conta YouTube.")
|
|
3078
|
-
with youtube_cols[1]:
|
|
3079
|
-
youtube_client_secret = text_setting("YouTube OAuth Client Secret", "youtube_client_secret", secret=True, help_text="Client Secret do mesmo cliente OAuth 2.0. Não é uma API Key.")
|
|
3080
|
-
|
|
3081
3115
|
with st.expander("Niche Finder — execução remota no Kaggle", expanded=True):
|
|
3082
3116
|
st.caption("O dataset permanece no Kaggle. O Thunderbolt usa estas credenciais apenas para publicar/executar a kernel e obter os resultados pequenos da análise.")
|
|
3083
3117
|
kaggle_cols = st.columns(3)
|
|
@@ -3100,10 +3134,6 @@ def render_settings():
|
|
|
3100
3134
|
with apify_cols[3]:
|
|
3101
3135
|
apify_run_timeout = st.number_input("Limite da execução (s)", min_value=30, max_value=7200, value=int(settings.get("apify_run_timeout_seconds", 900)), step=30)
|
|
3102
3136
|
|
|
3103
|
-
with st.expander("Consulta oficial de métricas — opcional"):
|
|
3104
|
-
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.")
|
|
3105
|
-
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.")
|
|
3106
|
-
|
|
3107
3137
|
with st.expander("Serviço, materiais e rede"):
|
|
3108
3138
|
cols = st.columns(2)
|
|
3109
3139
|
with cols[0]:
|
|
@@ -3260,8 +3290,7 @@ def render_settings():
|
|
|
3260
3290
|
save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
|
|
3261
3291
|
if save_all_settings:
|
|
3262
3292
|
settings.update({
|
|
3263
|
-
"port": port, "moneyprinter_path": moneyprinter_path,
|
|
3264
|
-
"youtube_client_id": youtube_client_id, "youtube_client_secret": youtube_client_secret,
|
|
3293
|
+
"port": port, "moneyprinter_path": moneyprinter_path,
|
|
3265
3294
|
"kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
|
|
3266
3295
|
"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),
|
|
3267
3296
|
"log_level": log_level, "listen_host": listen_host, "listen_port": listen_port, "video_source": video_source,
|
|
@@ -3329,6 +3358,32 @@ def render_settings():
|
|
|
3329
3358
|
st.warning("A amostra de voz anterior não é um ficheiro de áudio legível e foi removida do estado local. Teste a voz novamente.")
|
|
3330
3359
|
|
|
3331
3360
|
|
|
3361
|
+
def render_notifications():
|
|
3362
|
+
st.title("Notificações")
|
|
3363
|
+
st.caption("Centro de notificações locais do Thunderbolt.")
|
|
3364
|
+
st.info("Ainda não existem notificações registadas. Os eventos de processamento, publicação e integração serão ligados aqui numa etapa posterior.")
|
|
3365
|
+
st.divider()
|
|
3366
|
+
st.subheader("Preferências de notificação")
|
|
3367
|
+
st.caption("Os canais de e-mail, push e webhook ainda não estão activos nesta versão.")
|
|
3368
|
+
st.checkbox("Notificações no painel", value=True, disabled=True, key="notifications_panel_preview")
|
|
3369
|
+
st.checkbox("Notificações por e-mail", value=False, disabled=True, key="notifications_email_preview")
|
|
3370
|
+
|
|
3371
|
+
|
|
3372
|
+
def render_models_ai_tutorial():
|
|
3373
|
+
tutorial_url = "https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md"
|
|
3374
|
+
tutorial_path = ROOT / "seed" / "references" / "guide-instagram.md"
|
|
3375
|
+
st.title("Tutorial Meta")
|
|
3376
|
+
st.caption("Guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n.")
|
|
3377
|
+
st.markdown(f"[Abrir fonte original no GitHub]({tutorial_url})")
|
|
3378
|
+
try:
|
|
3379
|
+
tutorial_content = tutorial_path.read_text(encoding="utf-8").strip()
|
|
3380
|
+
except OSError:
|
|
3381
|
+
tutorial_content = ""
|
|
3382
|
+
if not tutorial_content:
|
|
3383
|
+
st.error("O conteúdo local do tutorial não está disponível. Consulte a fonte original no GitHub.")
|
|
3384
|
+
return
|
|
3385
|
+
st.markdown(tutorial_content, unsafe_allow_html=True)
|
|
3386
|
+
|
|
3332
3387
|
def render_mcp():
|
|
3333
3388
|
st.title("MCP")
|
|
3334
3389
|
st.caption("Clientes externos, servidor MCP do Thunderbolt e a skill local ficam separados para evitar confundir funções diferentes.")
|
|
@@ -3615,12 +3670,15 @@ def main():
|
|
|
3615
3670
|
models_ai_items = [
|
|
3616
3671
|
("Personagens", ":material/person:", "Personagens"),
|
|
3617
3672
|
("Redes Sociais", ":material/share:", "Redes Sociais"),
|
|
3673
|
+
("Tutorial Meta", ":material/menu_book:", "Tutorial Meta"),
|
|
3618
3674
|
]
|
|
3619
3675
|
settings_items = [
|
|
3620
3676
|
("Canais Youtube", ":material/ondemand_video:", "Canais Youtube"),
|
|
3621
3677
|
("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
|
|
3622
3678
|
("MCP", ":material/hub:", "MCP"),
|
|
3623
|
-
("
|
|
3679
|
+
("Contas Google", ":material/account_circle:", "Contas Google"),
|
|
3680
|
+
("Configuração API", ":material/settings:", "Configuração API"),
|
|
3681
|
+
("Notificações", ":material/notifications:", "Notificações"),
|
|
3624
3682
|
]
|
|
3625
3683
|
niche_finder_items = [
|
|
3626
3684
|
("Niche Finder Kaggle", ":material/search:", "Niche Finder Kaggle"),
|
|
@@ -3648,7 +3706,8 @@ def main():
|
|
|
3648
3706
|
"Automação": "Automação Youtube",
|
|
3649
3707
|
"Canais": "Canais Youtube",
|
|
3650
3708
|
"Blueprints": "Blueprints Youtube",
|
|
3651
|
-
"Configurações Técnicas": "
|
|
3709
|
+
"Configurações Técnicas": "Configuração API",
|
|
3710
|
+
"Contas Google/YouTube — canais em lote": "Contas Google",
|
|
3652
3711
|
}
|
|
3653
3712
|
current_page = aliases.get(st.session_state.get("page", "Início"), st.session_state.get("page", "Início"))
|
|
3654
3713
|
if current_page not in {item[0] for item in top_pages + pipeline_items + pipeline_tiktok_items + automation_items + edition_items + models_ai_items + niche_finder_items + settings_items}:
|
|
@@ -3713,13 +3772,16 @@ def main():
|
|
|
3713
3772
|
"Cortes": render_cuts,
|
|
3714
3773
|
"Editor Python": render_python_editor,
|
|
3715
3774
|
"Models AI": lambda: render_edit_placeholder("Models AI", "Seleccione uma das abas Models AI no menu expansível."),
|
|
3775
|
+
"Tutorial Meta": render_models_ai_tutorial,
|
|
3716
3776
|
"Personagens": lambda: render_edit_placeholder("Personagens", "Área reservada para a futura funcionalidade de personagens."),
|
|
3717
3777
|
"Redes Sociais": lambda: render_edit_placeholder("Redes Sociais", "Área reservada para a futura funcionalidade de redes sociais."),
|
|
3718
3778
|
"Upload": render_upload,
|
|
3719
3779
|
"Canais Youtube": render_channels,
|
|
3720
3780
|
"Blueprints Youtube": render_blueprints,
|
|
3721
3781
|
"MCP": render_mcp,
|
|
3722
|
-
"
|
|
3782
|
+
"Contas Google": render_google_accounts,
|
|
3783
|
+
"Configuração API": render_settings,
|
|
3784
|
+
"Notificações": render_notifications,
|
|
3723
3785
|
}
|
|
3724
3786
|
renderers.get(current_page, render_dashboard)()
|
|
3725
3787
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Setting up Instagram for automations with n8n
|
|
2
|
+
|
|
3
|
+
## [📚 Join our Skool community for support, premium content and more!](https://www.skool.com/ai-agents-az/about?gw8)
|
|
4
|
+
|
|
5
|
+
### Be part of a growing community and help us create more content like this
|
|
6
|
+
|
|
7
|
+
## Convert the Instagram account to a professional account
|
|
8
|
+
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
> Converting the Instagram account to a professional (business or creator) account will make the account public
|
|
11
|
+
|
|
12
|
+
In the Instagram application, follow these steps to convert your account to a professional account.
|
|
13
|
+
|
|
14
|
+
<table>
|
|
15
|
+
<tr>
|
|
16
|
+
<td>
|
|
17
|
+
1. Open the settings, and select `Account type and tools`
|
|
18
|
+
<img src="https://github.com/user-attachments/assets/adffda31-f8ce-4a49-a971-40e5cd5f5395" alt="" />
|
|
19
|
+
</td>
|
|
20
|
+
<td>
|
|
21
|
+
2. Click on `Switch to professinal account`
|
|
22
|
+
<img src="https://github.com/user-attachments/assets/5ea13aeb-b6ac-40b4-906f-2fc563625c52" alt="" />
|
|
23
|
+
</td>
|
|
24
|
+
<td>
|
|
25
|
+
3. Click on `Next`
|
|
26
|
+
<img src="https://github.com/user-attachments/assets/10942cc8-6045-4ca1-a14a-241a2d94591c" />
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
<tr>
|
|
30
|
+
<td>
|
|
31
|
+
4. Select the type of account your want to create
|
|
32
|
+
<img src="https://github.com/user-attachments/assets/c9e19bf8-b48d-49e7-b560-e8edf423233d" alt="" />
|
|
33
|
+
</td>
|
|
34
|
+
<td>
|
|
35
|
+
5. Select the right category, that describe your account
|
|
36
|
+
<img src="https://github.com/user-attachments/assets/eb4680b2-1d38-476c-8fb0-c428d1e12db9" alt="" />
|
|
37
|
+
</td>
|
|
38
|
+
<td>
|
|
39
|
+
6. Confirm the choice
|
|
40
|
+
<img src="https://github.com/user-attachments/assets/22ee13cf-f751-4cea-9fc0-be466c885b02" alt="" />
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
</table>
|
|
44
|
+
|
|
45
|
+
## Create credentials to use with n8n
|
|
46
|
+
|
|
47
|
+
### 1. Create a Facebook page
|
|
48
|
+
|
|
49
|
+
Navigate to [https://www.facebook.com/pages/create](https://www.facebook.com/pages/create) to create a new Facebook page.
|
|
50
|
+
Fill out the required parameters (name, category) and create the page.
|
|
51
|
+
|
|
52
|
+
<img width="1554" alt="Screenshot 2025-05-05 at 10 02 00 AM" src="https://github.com/user-attachments/assets/f0e321a2-7b85-4b23-b85b-f8e63281b6b9" />
|
|
53
|
+
|
|
54
|
+
Now, you need to be acting on behalf of the page - if it's not active for some reason, select it from the menu.
|
|
55
|
+
|
|
56
|
+
<img width="372" alt="image" src="https://github.com/user-attachments/assets/7aaaccc8-4b0a-4f72-a8c9-f2ecc453584a" />
|
|
57
|
+
|
|
58
|
+
### 2. Connect Instagram account to the Facebook page
|
|
59
|
+
|
|
60
|
+
Click on your page's name on the left side.
|
|
61
|
+
|
|
62
|
+
<img width="379" alt="image" src="https://github.com/user-attachments/assets/fd1ed1d8-8890-4636-a4ae-0496db252283" />
|
|
63
|
+
|
|
64
|
+
Select `Settings` from the menu.
|
|
65
|
+
|
|
66
|
+
<img width="368" alt="image" src="https://github.com/user-attachments/assets/641904ec-764e-4d56-bede-618da33db7fe" />
|
|
67
|
+
|
|
68
|
+
Scroll down to `Permissions` and select `Linked accounts`.
|
|
69
|
+
|
|
70
|
+
<img width="369" alt="image" src="https://github.com/user-attachments/assets/a024ad7e-8358-4329-a864-c7b621810405" />
|
|
71
|
+
|
|
72
|
+
Select `Instagram`.
|
|
73
|
+
|
|
74
|
+
<img width="740" alt="image" src="https://github.com/user-attachments/assets/141164a0-c88e-46a0-a335-466f81297bc6" />
|
|
75
|
+
|
|
76
|
+
Click on `Connect account` and sign in with your Instagram account.
|
|
77
|
+
|
|
78
|
+
<img width="708" alt="image" src="https://github.com/user-attachments/assets/5324b945-621b-4f2b-b513-9b51e5b5c071" />
|
|
79
|
+
|
|
80
|
+
Click on `Connect`.
|
|
81
|
+
|
|
82
|
+
<img width="573" alt="image" src="https://github.com/user-attachments/assets/85cad094-a815-441a-a0e5-84081fd0ee2d" />
|
|
83
|
+
|
|
84
|
+
Click on `Confirm`.
|
|
85
|
+
|
|
86
|
+
<img width="570" alt="image" src="https://github.com/user-attachments/assets/df5bb377-22a8-41de-b99d-0f80052cb372" />
|
|
87
|
+
|
|
88
|
+
Click on `Continue`.
|
|
89
|
+
|
|
90
|
+
<img width="561" alt="image" src="https://github.com/user-attachments/assets/81fd3143-f00b-4e33-9521-12c0c9ce623d" />
|
|
91
|
+
|
|
92
|
+
You are done.
|
|
93
|
+
|
|
94
|
+
<img width="713" alt="image" src="https://github.com/user-attachments/assets/ce4c0bbe-beed-4c6c-8731-23216efbccb5" />
|
|
95
|
+
|
|
96
|
+
### 3. Create a Facebook application
|
|
97
|
+
|
|
98
|
+
Head to [https://developers.facebook.com/apps](https://developers.facebook.com/apps) and click on `Create app`
|
|
99
|
+
|
|
100
|
+
<img width="1027" alt="image" src="https://github.com/user-attachments/assets/ca5c79b9-974a-4c55-8f88-6def8c1b98d1" />
|
|
101
|
+
|
|
102
|
+
Select the `Other` use case and hit `Next`.
|
|
103
|
+
|
|
104
|
+
<img width="1033" alt="image" src="https://github.com/user-attachments/assets/31f296bd-3b47-432b-9037-7974e821a311" />
|
|
105
|
+
|
|
106
|
+
Select `Business` app type.
|
|
107
|
+
|
|
108
|
+
<img width="818" alt="image" src="https://github.com/user-attachments/assets/2ab06dd1-33ba-4d87-8365-3637c8327877" />
|
|
109
|
+
|
|
110
|
+
Review, and click on `Create app`
|
|
111
|
+
|
|
112
|
+
<img width="815" alt="image" src="https://github.com/user-attachments/assets/c8cc8e1d-c006-428a-9bfa-8cc78a2932c9" />
|
|
113
|
+
|
|
114
|
+
Add the Instagram product to your app.
|
|
115
|
+
|
|
116
|
+
<img width="1008" alt="image" src="https://github.com/user-attachments/assets/4186f90b-b619-4c1c-9ea8-bfc5294eaeb4" />
|
|
117
|
+
|
|
118
|
+
You don't need to configure it, just leave it as it is.
|
|
119
|
+
|
|
120
|
+
### 4. Create an access token
|
|
121
|
+
|
|
122
|
+
From the tools menu, select the `Graph API Explorer`
|
|
123
|
+
|
|
124
|
+
<img width="877" alt="image" src="https://github.com/user-attachments/assets/006a40a7-894a-4bd0-9483-0d495b77206f" />
|
|
125
|
+
|
|
126
|
+
You'll see this interface below.
|
|
127
|
+
|
|
128
|
+
<img width="541" alt="image" src="https://github.com/user-attachments/assets/414ece47-2f1d-4d08-9998-ada85ffb065f" />
|
|
129
|
+
|
|
130
|
+
1. Make sure the Meta app is the one you just created
|
|
131
|
+
2. In the permissions panel, add the following permissions
|
|
132
|
+
|
|
133
|
+
- `pages_show_list`
|
|
134
|
+
- `business_management`
|
|
135
|
+
- `instagram_basic`
|
|
136
|
+
- `instagram_content_publish`
|
|
137
|
+
- `pages_read_engagement`
|
|
138
|
+
|
|
139
|
+
3. Click on `Generate Access Token`
|
|
140
|
+
|
|
141
|
+
4. You'll be prompted to select the Facebook page you give access to, the business account and the Instagram account. Select all of them. During the process, take a not of the **Instagram account id** you are connecting, you'll need that later.
|
|
142
|
+
|
|
143
|
+
<table>
|
|
144
|
+
<tr>
|
|
145
|
+
<td>
|
|
146
|
+
<img width="564" alt="Screenshot 2025-05-09 at 11 07 22 AM" src="https://github.com/user-attachments/assets/6b77415e-1ca2-47f0-b046-974fd391c90a" />
|
|
147
|
+
</td>
|
|
148
|
+
<td>
|
|
149
|
+
<img width="562" alt="Screenshot 2025-05-09 at 11 07 27 AM" src="https://github.com/user-attachments/assets/353b9973-426b-4034-b5fc-e77840ca0d18" />
|
|
150
|
+
</td>
|
|
151
|
+
<td>
|
|
152
|
+
<img width="562" alt="Screenshot 2025-05-09 at 11 08 35 AM" src="https://github.com/user-attachments/assets/58132694-1376-4c40-9d20-61cb8fdb9df5" />
|
|
153
|
+
</td>
|
|
154
|
+
<td>
|
|
155
|
+
<img width="562" alt="Screenshot 2025-05-09 at 11 08 58 AM" src="https://github.com/user-attachments/assets/c2676755-f12f-49d6-9a92-49ea688fc832" />
|
|
156
|
+
</td>
|
|
157
|
+
</tr>
|
|
158
|
+
</table>
|
|
159
|
+
|
|
160
|
+
5. Copy the access token, and head to the [Facebook token debugger tool](https://developers.facebook.com/tools/debug/accesstoken) and paste in the access token, and click `debug`. Scroll to the very bottom of the page, and click on `Extend Access Token`. Copy the long-lived access token.
|
|
161
|
+
|
|
162
|
+
6. Go back to the `Graph API Explorer` and paste the access token to the access token field and add `me/accounts` to the graph path and click `Submit`. In the results, you'll find field `access_token` that contains the long-lived page access token that will never expire. Copy that.
|
|
163
|
+
|
|
164
|
+
<img width="693" alt="image" src="https://github.com/user-attachments/assets/0e1bd821-5c27-4c9a-b14e-eeed9c17850f" />
|
|
165
|
+
|
|
166
|
+
7. (optional) you can verify the expiration of the token, if you paste it to the [Facebook token debugger tool](https://developers.facebook.com/tools/debug/accesstoken).
|
|
167
|
+
|
|
168
|
+
8. (optional) you can also get the Instagram Business Account's id, if you click on the id of the page in the Graph API explorer, and add `?fields=instagram_business_account` after the id, then click `Submit`
|
|
169
|
+
|
|
170
|
+
<img width="766" alt="image" src="https://github.com/user-attachments/assets/ac0cb186-849a-4c55-8f88-6def8c1b98d1" />
|
|
171
|
+
|
|
172
|
+
**If you did everything above, you'll have two things**
|
|
173
|
+
|
|
174
|
+
1. A page token, that won't expire to manage your Instagram account. You will use the page token to setup the Facebook Graph node in n8n.
|
|
175
|
+
2. The Instagram account id which you'll need to set in the `Configure` node in the n8n workflow
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
> **Source:** [ai_agents_az/episode_8/guide-instagram.md](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md)
|