@danhachuel/thunderbolt 0.2.47 → 0.2.48

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Este manual descreve a instalação local da UI Thunderbolt, baseada no MoneyPrinterTurbo, utilizando o pacote npm `@danhachuel/thunderbolt`. O fluxo recomendado instala automaticamente o ambiente Python, as dependências da aplicação, as dependências do MoneyPrinterTurbo, o Streamlit e o suporte FFmpeg através de `imageio-ffmpeg`.
4
4
 
5
- > **Versão deste manual:** 0.2.47
5
+ > **Versão deste manual:** 0.2.48
6
6
  > **Pacote npm:** `@danhachuel/thunderbolt`
7
7
  > **Porta padrão da UI:** `localhost:3030`
8
8
  > **Repositório:** [github.com/DanHachuel/thunderbolt](https://github.com/DanHachuel/thunderbolt)
@@ -100,13 +100,13 @@ Execute:
100
100
  Windows PowerShell ou MobaXterm:
101
101
 
102
102
  ```powershell
103
- npx.cmd --yes @danhachuel/thunderbolt@0.2.47 install
103
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.48 install
104
104
  ```
105
105
 
106
106
  Linux/macOS:
107
107
 
108
108
  ```bash
109
- npx --yes @danhachuel/thunderbolt@0.2.47 install
109
+ npx --yes @danhachuel/thunderbolt@0.2.48 install
110
110
  ```
111
111
 
112
112
  A instalação normal é **segura para actualizações**: preserva `storage`, Blueprints, Brandings, configurações e artefactos do utilizador. Remove apenas `.venv`, o clone técnico do MoneyPrinterTurbo e dependências que serão recriadas. Uma pasta antiga sem dados do utilizador, como `C:\Users\<utilizador>\AppData\Local\hermes` da tentativa incompleta, pode ser removida; uma pasta antiga que contenha Blueprints, Brandings ou storage é preservada e apenas avisada no terminal. Feche processos Python, Node, Streamlit e MobaXterm que estejam a usar as pastas antes de executar.
@@ -392,21 +392,21 @@ Após iniciar a aplicação, valide o seguinte percurso:
392
392
  7. **Criação de Vídeos > Vídeos:** verifique o estado `to_do` e os botões **Iniciar** e **Parar** dentro da subaba. Confirme também que **Criação de Músicas** mostra o mesmo fluxo com título próprio.
393
393
  8. **Upload:** configure o OAuth Client ID e Secret, autorize primeiro o **youtube-automation-agent** na própria aba, confirme o estado **pronto para publicar**, preencha título/descrição/tags e publique um MP4 real. O botão **Autorizar fallback OAuth** existe apenas para redundância; a Data API Key, se configurada, é exclusivamente para consultas oficiais públicas e nunca substitui OAuth.
394
394
  9. **Edição > Limpador de Metadados:** suba um vídeo de terceiro, preencha título, preview, links, timestamps e tags, aplique a limpeza e descarregue a cópia limpa e o manifesto JSON. O original fica preservado e vídeos das páginas de criação não são aceites nesta área.
395
- 10. **Configurações > Configurações Técnicas:** confirme que os caminhos e credenciais estão locais e não aparecem no Git. Na secção **Contas Google/YouTube — canais em lote**, adicione cada conta com o seu e-mail/Gmail, OAuth Client ID e OAuth Client Secret próprios; o e-mail identifica a conta e não é usado para ler a caixa Gmail.
395
+ 10. **Configurações > Configurações Técnicas:** confirme que os caminhos e credenciais estão locais e não aparecem no Git. Na secção **Contas Google/YouTube — canais em lote**, adicione cada conta com o seu e-mail/Gmail, OAuth Client ID, OAuth Client Secret e `sessionInfo` próprios; use **Repetir campos para nova conta** para preparar contas adicionais e o ícone **Apagar conta** para eliminar individualmente a conta, tokens, documento JSON e associações de canais.
396
396
  11. **OpenAI/ NVIDIA NIM:** em **LLM — providers e modelos**, seleccione `openai`, confirme a Base URL, introduza a API key e clique em **Consultar/actualizar modelos NIM**. Escolha um modelo da lista ou utilize o fallback manual e guarde as configurações. Confirme que a consulta só ocorre após o clique e que um erro de endpoint não interrompe a UI.
397
397
  12. **Criação de Vídeos:** confirme que `Pexels/Pixabay` substitui o label antigo, que `Estilo IA` aparece apenas em `full_ia` e que `Apenas Música` exige áudio e guarda `background_mode=none`.
398
398
  13. **Automação:** no cartão de cada canal, escolha o **Blueprint padrão** e a **Voz padrão**, clique em `Guardar` e confirme que o resumo do cartão se actualiza. Configure também **Automação ON** e um horário `HH:MM`; confirme a lista de vídeos cadastrados. Os mesmos defaults aparecem em **Canais** e são usados em novas tarefas. Inicie o Thunderbolt pelo launcher, confirme o aviso verde **Worker activo** e verifique que o relógio apresentado corresponde ao computador. O worker cria no máximo um lote por canal por dia quando o horário local coincide.
399
399
  14. **Configurações > Configurações Técnicas > Teste de vozes:** teste Edge/Azure ou provider configurado e confirme reprodução/download sem criação de tarefa.
400
- 15. **Upload directo:** em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada cartão Gmail apresenta apenas o uploader **Documento de credenciais desta conta Google**. O documento JSON reúne `SID`, `SSID`, `HSID`, `APISID`, `SAPISID`, `sessionInfo`, `INNERTUBE_API_KEY`, `chunk_size` e `delegated_session_ids` por canal. Em **Canais > Canais cadastrados**, associe cada canal à conta Google do documento; a UI não mostra nem edita o `DELEGATED_SESSION_ID`. O documento é guardado em `storage/youtube_direct_accounts/<id-da-conta>/credentials.json`, e o uploader lê todos os valores exclusivamente desse documento. Não existem campos técnicos do Upload directo na parte inferior da UI e o método não extrai cookies automaticamente do navegador.
400
+ 15. **Upload directo:** em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada cartão Gmail apresenta o uploader **Documento de credenciais desta conta Google** e o campo **sessionInfo token desta conta Google**. O sessionInfo é guardado por conta e sincronizado no documento JSON, que reúne `SID`, `SSID`, `HSID`, `APISID`, `INNERTUBE_API_KEY`, `chunk_size` e `delegated_session_ids` por canal. Use **Repetir campos para nova conta** para preencher o formulário seguinte e o ícone **Apagar conta** para remover a conta e os dados privados associados. Em **Canais > Canais cadastrados**, associe cada canal à conta Google do documento; a UI não mostra nem edita o `DELEGATED_SESSION_ID`. O documento é guardado em `storage/youtube_direct_accounts/<id-da-conta>/credentials.json`, e o uploader lê os valores directos exclusivamente desse documento. Não existem campos técnicos de cookies, INNERTUBE_API_KEY, chunk_size ou DELEGATED_SESSION_ID na parte inferior da UI e o método não extrai cookies automaticamente do navegador.
401
401
  16. **Configurações > MCP > Client MCP:** confirme que Short Video Maker, AutoVio, OpenMontage e OpenCut aparecem com as portas padrão editáveis. O estado **Activo** é uma preferência local; a detecção deve indicar **Não detectado** quando os serviços externos não estiverem instalados ou iniciados.
402
402
  17. **Configurações > MCP > Servidor MCP:** abra a subaba, mantenha o host `127.0.0.1`, active **Servidor MCP ON** e clique em **Guardar e iniciar Servidor MCP**. Confirme o endpoint `/mcp` e o health endpoint `/health`. Mantenha **Permitir ferramentas de escrita** desactivado até precisar que um agente crie lotes.
403
403
  18. **Configurações > MCP > Skill:** clique em **Guardar skill localmente** e confirme o ficheiro em `storage/skills/moneyprinterturbo-video.md`; opcionalmente use **Descarregar skill .md** para obter a cópia através do navegador.
404
404
 
405
405
  ## Canais em lote por conta Google/YouTube
406
406
 
407
- A subaba **Canais em lote gmail**, dentro de **Canais**, não lê e-mails, mensagens, contactos ou a caixa Gmail. O nome identifica a conta Google/Gmail que gere os canais YouTube. Configure cada conta em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, preenchendo e-mail, OAuth Client ID e OAuth Client Secret.
407
+ A subaba **Canais em lote gmail**, dentro de **Canais**, não lê e-mails, mensagens, contactos ou a caixa Gmail. O nome identifica a conta Google/Gmail que gere os canais YouTube. Configure cada conta em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, preenchendo e-mail, OAuth Client ID, OAuth Client Secret e o `sessionInfo token desta conta Google`.
408
408
 
409
- Na subaba, seleccione a conta e clique em **Autorizar conta Google**. A autorização abre o browser do sistema e guarda um refresh token separado para essa conta. Depois clique em **Listar canais desta conta**. O Thunderbolt chama a YouTube Data API com `channels.list`, `mine=true`, `part=snippet,contentDetails,statistics`, suporta páginas sucessivas e apresenta os canais encontrados para selecção.
409
+ Na subaba, seleccione a conta e clique em **Autorizar conta Google**. A autorização abre o browser do sistema e guarda um refresh token separado para essa conta. Use um cliente OAuth do tipo **Desktop app**; o Thunderbolt usa a URI loopback `http://127.0.0.1:8765/`. Se usar um cliente Web application, adicione exactamente essa URI em Google Cloud > APIs e serviços > Credenciais > URIs de redireccionamento autorizados, incluindo a porta e a barra final. Caso contrário, o Google devolve `Erro 400: redirect_uri_mismatch`. Depois clique em **Listar canais desta conta**. O Thunderbolt chama a YouTube Data API com `channels.list`, `mine=true`, `part=snippet,contentDetails,statistics`, suporta páginas sucessivas e apresenta os canais encontrados para selecção.
410
410
 
411
411
  Escolha os canais e os defaults de Blueprint, voz, idioma e estilo wide, depois clique em **Cadastrar canais seleccionados**. A importação usa `youtube_channel_id` para não duplicar canais já cadastrados, preserva os registos existentes e identifica a origem como `youtube_data_api_oauth_mine`. A API Key global não substitui OAuth neste fluxo; permanece reservada para consultas públicas/métricas.
412
412
 
@@ -418,7 +418,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
418
418
 
419
419
  Os parâmetros da UI são número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, engagement, intervalo de datas e tags, todos dentro da área principal da aba. O núcleo normaliza os dados, calcula engagement, aplica filtros, faz transformação logarítmica e standardização, executa K-Means e calcula itemsets/regras com FP-Growth. Não são apresentados resultados até ao primeiro clique em **Analisar Nichos**; o mesmo botão aplica alterações posteriores aos filtros. Os resultados são DataFrames de clusters, itemsets frequentes, regras de associação e dados analisados; o gráfico de dispersão é criado nativamente com Plotly.
420
420
 
421
- As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.47 install`; o instalador detecta e reutiliza o que já estiver válido.
421
+ As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.48 install`; o instalador detecta e reutiliza o que já estiver válido.
422
422
 
423
423
  ### Niche Finder Apify
424
424
 
@@ -448,7 +448,7 @@ A área **Teste de vozes**, dentro de **Configurações > Configurações Técni
448
448
 
449
449
  ## Upload directo
450
450
 
451
- Em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada cartão Gmail mostra apenas o uploader **Documento de credenciais desta conta Google**. O documento JSON único pode conter cookies, sessionInfo, INNERTUBE_API_KEY, chunk_size e o mapa de IDs delegados por canal. Os valores nunca aparecem como inputs separados; o ficheiro validado é guardado por conta em `storage/youtube_direct_accounts/<id-da-conta>/credentials.json`.
451
+ Em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada cartão Gmail mostra o uploader **Documento de credenciais desta conta Google** e o input **sessionInfo token desta conta Google**. O documento JSON único contém cookies, INNERTUBE_API_KEY, chunk_size e o mapa de IDs delegados por canal; o sessionInfo preenchido na conta é sincronizado no mesmo ficheiro, guardado em `storage/youtube_direct_accounts/<id-da-conta>/credentials.json`.
452
452
 
453
453
  Em **Canais > Canais cadastrados**, a secção **Upload directo — documento da conta deste canal** mostra apenas a conta Google associada e confirma se o documento contém o ID delegado desse canal. O `DELEGATED_SESSION_ID` permanece exclusivamente no mapa do documento; o upload bloqueia a operação se faltar qualquer elemento técnico.
454
454
 
package/README.md CHANGED
@@ -22,12 +22,12 @@ A primeira versão implementa a camada UI independente com:
22
22
  | Edição | Menu expansível abaixo de Automação com Limpador de Metadados funcional, Cortes reservado e Editor Python inspirado no PYEdit para vídeos e scripts locais |
23
23
  | Upload | YouTube via `youtube-automation-agent` adaptado internamente, OAuth directo de redundância, Upload directo via sessão Frontend API com cookies/sessionInfo por conta e DELEGATED_SESSION_ID por canal, TikTok, Instagram e Facebook Pages no front end |
24
24
  | MCP | Catálogo local opcional de Short Video Maker, AutoVio, OpenMontage e OpenCut, com portas editáveis e activação |
25
- | Configurações Técnicas | Provedores LLM, OpenAI/NVIDIA NIM com API key, Base URL e selector de modelos, TTS/voz, preview de vozes, Suno, materiais, Whisper, FFmpeg, OAuth YouTube, contas Google/YouTube para canais em lote com e-mail, Client ID e Client Secret por conta, cookies SID/SSID/HSID/APISID/SAPISID e sessionInfo por conta, Data API Key opcional, Kaggle Username/API Key, Apify API Token/Actor ID/limites, Upload directo, TikTok Client ID/Secret e Upload-Post |
25
+ | Configurações Técnicas | Provedores LLM, OpenAI/NVIDIA NIM com API key, Base URL e selector de modelos, TTS/voz, preview de vozes, Suno, materiais, Whisper, FFmpeg, OAuth YouTube, contas Google/YouTube para canais em lote com e-mail, Client ID, Client Secret e sessionInfo por conta, repetição dos campos para novas contas, eliminação individual e diagnóstico da URI OAuth, cookies SID/SSID/HSID/APISID/SAPISID no documento JSON, Data API Key opcional, Kaggle Username/API Key, Apify API Token/Actor ID/limites, Upload directo, TikTok Client ID/Secret e Upload-Post |
26
26
  | Launcher | Execução via `npx`, instalação assistida, diagnóstico e preparação para distribuição |
27
27
 
28
28
  ## Upload directo — credenciais por conta e por canal
29
29
 
30
- O Upload directo baseado no [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api) usa um único documento JSON por conta Google. Em **Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada cartão Gmail possui apenas o uploader **Documento de credenciais desta conta Google**. Esse documento reúne `SID`, `SSID`, `HSID`, `APISID`, `SAPISID`, `sessionInfo`, `INNERTUBE_API_KEY`, `chunk_size` e o mapa `delegated_session_ids` por canal. A UI não contém inputs separados para nenhum destes valores.
30
+ O Upload directo baseado no [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api) usa um único documento JSON por conta Google. Em **Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada cartão Gmail possui o uploader **Documento de credenciais desta conta Google** e o campo específico **sessionInfo token desta conta Google**. O token é guardado por conta e sincronizado no `credentials.json`; o documento continua a reunir `SID`, `SSID`, `HSID`, `APISID`, `INNERTUBE_API_KEY`, `chunk_size` e o mapa `delegated_session_ids` por canal. A UI não contém inputs separados para cookies, INNERTUBE_API_KEY, chunk_size ou DELEGATED_SESSION_ID. O botão **Repetir campos para nova conta** preenche o formulário seguinte com os valores da conta actual, e **Apagar conta** remove a conta, o token OAuth, o documento de credenciais e as associações de canais.
31
31
 
32
32
  O documento é validado e guardado por Gmail em `storage/youtube_direct_accounts/<id-da-conta>/credentials.json`, com permissões locais restritas. Em **Canais > Canais cadastrados**, a UI apenas associa o canal à conta Google do documento; não mostra nem edita o `DELEGATED_SESSION_ID`. O uploader procura o ID do canal dentro do mapa `delegated_session_ids` do documento e usa-o como `pageId`/`onBehalfOfUser`. O Upload directo fica bloqueado quando faltar qualquer elemento no documento. Não existem campos técnicos do Upload directo na parte inferior de Configurações Técnicas.
33
33
 
@@ -36,7 +36,7 @@ Os dados são segredos de sessão. Os valores não aparecem em tabelas ou logs,
36
36
 
37
37
  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.
38
38
 
39
- ## Navegação da UI 0.2.47
39
+ ## Navegação da UI 0.2.48
40
40
 
41
41
  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** e **Upload**. **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 **Redes Sociais**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais**, **Blueprints**, **MCP** e **Configurações Técnicas**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
42
42
 
@@ -48,6 +48,12 @@ A Base URL predefinida para NVIDIA NIM é `https://integrate.api.nvidia.com/v1`.
48
48
 
49
49
  A consulta só é executada quando o utilizador carrega no botão; a aplicação não envia a API key ao abrir a página. Se o endpoint estiver offline, recusar a credencial ou não disponibilizar `/models`, a UI mostra o erro sem expor a API key e mantém a opção **Escrever modelo manualmente**. Também é possível manter um endpoint local NIM ou outro servidor OpenAI-compatible substituindo a Base URL.
50
50
 
51
+ ## Contas Google/YouTube e OAuth local
52
+
53
+ Para autorizar uma conta, use no Google Cloud um cliente OAuth do tipo **Desktop app**. O Thunderbolt usa o loopback local determinístico `http://127.0.0.1:8765/`, abre o browser e guarda o token por conta. Se estiver a usar um cliente do tipo Web application, adicione exactamente essa URI em **Google Cloud > APIs e serviços > Credenciais > URIs de redireccionamento autorizados**; a URI deve incluir a porta e a barra final. O erro `400: redirect_uri_mismatch` significa que a URI registada no cliente Google não coincide exactamente com a enviada pela aplicação.
54
+
55
+ A área **Contas Google/YouTube — canais em lote** permite manter várias contas. Cada cartão tem e-mail, OAuth Client ID, OAuth Client Secret, sessionInfo e documento de Upload directo. Use **Repetir campos para nova conta** para preparar rapidamente outra conta, ou **Apagar conta** para remover a conta, os tokens e os dados directos associados. O sessionInfo é sincronizado com o documento JSON da própria conta; cookies e os restantes parâmetros de Upload directo permanecem apenas nesse documento.
56
+
51
57
  ## Instalação
52
58
 
53
59
  Recomenda-se Python 3.11 ou superior e Node.js 18 ou superior.
@@ -133,9 +139,9 @@ thunderbolt
133
139
  No Windows PowerShell, se `npx` for bloqueado por `npx.ps1`, use directamente `npx.cmd`:
134
140
 
135
141
  ```powershell
136
- npx.cmd --yes @danhachuel/thunderbolt@0.2.47 install
137
- npx.cmd --yes @danhachuel/thunderbolt@0.2.47 doctor
138
- npx.cmd --yes @danhachuel/thunderbolt@0.2.47
142
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.48 install
143
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.48 doctor
144
+ npx.cmd --yes @danhachuel/thunderbolt@0.2.48
139
145
  ```
140
146
 
141
147
  Como alternativa, pode permitir scripts para o seu utilizador:
package/app/main.py CHANGED
@@ -36,8 +36,8 @@ from hermes_ui.music import list_music_files, materialize_suno_audio, request_su
36
36
  from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
37
37
  from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter
38
38
  from integrations.youtube_direct_upload import YouTubeDirectUploader
39
- from integrations.youtube_direct_credentials import direct_account_status, document_status, parse_credentials_document, save_credentials_document
40
- 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
39
+ from integrations.youtube_direct_credentials import delete_credentials_document, direct_account_status, document_status, parse_credentials_document, save_credentials_document, update_credentials_document_session_info
40
+ 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
41
41
  from integrations.local_runtime import MoneyPrinterRuntime
42
42
  from integrations.moneyprinter_config import sync_moneyprinter_config
43
43
  from integrations.openai_model_discovery import DEFAULT_NVIDIA_NIM_BASE_URL, ModelDiscoveryError, fetch_openai_compatible_models
@@ -1466,6 +1466,13 @@ def render_settings():
1466
1466
  account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
1467
1467
  with account_cols[3]:
1468
1468
  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}")
1469
+ account_session_info = st.text_input(
1470
+ "sessionInfo token desta conta Google",
1471
+ value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
1472
+ type="password",
1473
+ key=f"batch_session_info_{account_id}",
1474
+ help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json; os cookies e restantes valores continuam exclusivamente no documento.",
1475
+ )
1469
1476
  save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", use_container_width=True)
1470
1477
  direct_status = direct_account_status(STORAGE, batch_account)
1471
1478
  st.markdown("**Credenciais do Upload directo desta conta Google**")
@@ -1498,14 +1505,30 @@ def render_settings():
1498
1505
  if result.ok:
1499
1506
  st.rerun()
1500
1507
  with status_cols[2]:
1501
- if st.button("Remover conta", key=f"batch_remove_settings_{account_id}", use_container_width=True):
1508
+ if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", use_container_width=True):
1502
1509
  delete_youtube_batch_token(batch_account, STORAGE)
1510
+ delete_credentials_document(STORAGE, batch_account)
1503
1511
  remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
1504
1512
  settings["youtube_batch_accounts"] = remaining_accounts
1505
1513
  if settings.get("youtube_batch_selected_account_id") == account_id:
1506
1514
  settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
1515
+ channels = read_json("channels.json", [])
1516
+ channels_changed = False
1517
+ for channel in channels:
1518
+ if str(channel.get("google_account_id") or "") == account_id:
1519
+ channel.update({"google_account_id": "", "google_account_email": ""})
1520
+ channels_changed = True
1521
+ if channels_changed:
1522
+ write_json("channels.json", channels)
1507
1523
  write_json("settings.json", settings)
1508
1524
  st.rerun()
1525
+ if st.button("Repetir campos para nova conta", icon=":material/content_copy:", key=f"batch_repeat_settings_{account_id}", use_container_width=True):
1526
+ st.session_state["new_batch_account_label"] = account_label
1527
+ st.session_state["new_batch_account_email"] = account_email
1528
+ st.session_state["new_batch_account_client_id"] = account_client_id
1529
+ st.session_state["new_batch_account_client_secret"] = account_client_secret
1530
+ st.session_state["new_batch_account_session_info"] = account_session_info
1531
+ st.rerun()
1509
1532
  if save_account:
1510
1533
  if "@" not in account_email.strip():
1511
1534
  st.error("Informe um e-mail Google válido.")
@@ -1517,7 +1540,8 @@ def render_settings():
1517
1540
  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())))
1518
1541
  if credentials_changed:
1519
1542
  delete_youtube_batch_token(existing, STORAGE)
1520
- 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()})
1543
+ 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()})
1544
+ update_credentials_document_session_info(STORAGE, existing, account_session_info.strip())
1521
1545
  settings["youtube_batch_accounts"] = batch_accounts
1522
1546
  write_json("settings.json", settings)
1523
1547
  st.success("Conta Google/YouTube guardada.")
@@ -1528,7 +1552,7 @@ def render_settings():
1528
1552
  st.error("Seleccione o documento JSON completo desta conta Google antes de guardar.")
1529
1553
  else:
1530
1554
  try:
1531
- document = parse_credentials_document(uploaded_document.getvalue(), uploaded_document.name)
1555
+ document = parse_credentials_document(uploaded_document.getvalue(), uploaded_document.name, session_info_override=account_session_info.strip())
1532
1556
  document["account_id"] = account_id
1533
1557
  document["email"] = str(batch_account.get("email", ""))
1534
1558
  save_credentials_document(STORAGE, batch_account, document)
@@ -1548,8 +1572,9 @@ def render_settings():
1548
1572
  new_account_client_id = st.text_input("OAuth Client ID", key="new_batch_account_client_id")
1549
1573
  with add_cols[3]:
1550
1574
  new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
1551
- new_account_document = st.file_uploader("Documento de credenciais desta conta Google", type=["json"], key="new_batch_account_credentials_document", help="Documento JSON único com cookies SID/SSID/HSID/APISID/SAPISID, sessionInfo, INNERTUBE_API_KEY, chunk_size e delegated_session_ids.")
1552
- st.caption("O documento é guardado em storage/youtube_direct_accounts/<id-da-conta>/credentials.json. Não existem inputs separados para cookies ou sessionInfo.")
1575
+ 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 JSON.")
1576
+ new_account_document = st.file_uploader("Documento de credenciais desta conta Google", type=["json"], key="new_batch_account_credentials_document", help="Documento JSON único com cookies SID/SSID/HSID/APISID/SAPISID, INNERTUBE_API_KEY, chunk_size e delegated_session_ids. O sessionInfo pode ser preenchido no campo acima ou no documento.")
1577
+ st.caption("O documento é guardado em storage/youtube_direct_accounts/<id-da-conta>/credentials.json. O campo sessionInfo é específico desta conta; os cookies e restantes credenciais continuam apenas no documento.")
1553
1578
  add_account = st.form_submit_button("Adicionar conta Google/YouTube com documento de Upload directo", use_container_width=True)
1554
1579
  if add_account:
1555
1580
  document_error = ""
@@ -1559,14 +1584,14 @@ def render_settings():
1559
1584
  st.error("Informe o Client ID e o Client Secret da nova conta.")
1560
1585
  elif new_account_document is not None:
1561
1586
  try:
1562
- parse_credentials_document(new_account_document.getvalue(), new_account_document.name)
1587
+ parse_credentials_document(new_account_document.getvalue(), new_account_document.name, session_info_override=new_account_session_info.strip())
1563
1588
  except ValueError as exc:
1564
1589
  document_error = str(exc)
1565
1590
  st.error(document_error)
1566
1591
  if "@" in new_account_email.strip() and new_account_client_id.strip() and new_account_client_secret.strip() and not document_error:
1567
- 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()}
1592
+ 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()}
1568
1593
  if new_account_document is not None:
1569
- document = parse_credentials_document(new_account_document.getvalue(), new_account_document.name)
1594
+ document = parse_credentials_document(new_account_document.getvalue(), new_account_document.name, session_info_override=new_account_session_info.strip())
1570
1595
  document["account_id"] = new_account["id"]
1571
1596
  document["email"] = new_account["email"]
1572
1597
  save_credentials_document(STORAGE, new_account, document)
@@ -1586,6 +1611,7 @@ def render_settings():
1586
1611
  moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
1587
1612
  st.markdown("**YouTube — OAuth 2.0 e consulta pública**")
1588
1613
  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.")
1614
+ 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()}`.")
1589
1615
  youtube_cols = st.columns(2)
1590
1616
  with youtube_cols[0]:
1591
1617
  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.")
@@ -11,6 +11,36 @@ from typing import Any
11
11
  from integrations.platforms import IntegrationResult
12
12
 
13
13
  BATCH_SCOPES = ["https://www.googleapis.com/auth/youtube.readonly"]
14
+ DEFAULT_LOOPBACK_HOST = "127.0.0.1"
15
+ DEFAULT_LOOPBACK_PORT = 8765
16
+
17
+
18
+ def loopback_host() -> str:
19
+ return _text(os.getenv("THUNDERBOLT_OAUTH_LOOPBACK_HOST")) or DEFAULT_LOOPBACK_HOST
20
+
21
+
22
+ def loopback_port() -> int:
23
+ try:
24
+ port = int(os.getenv("THUNDERBOLT_OAUTH_LOOPBACK_PORT", str(DEFAULT_LOOPBACK_PORT)))
25
+ except (TypeError, ValueError):
26
+ port = DEFAULT_LOOPBACK_PORT
27
+ return port if 1024 <= port <= 65535 else DEFAULT_LOOPBACK_PORT
28
+
29
+
30
+ def loopback_redirect_uri() -> str:
31
+ return f"http://{loopback_host()}:{loopback_port()}/"
32
+
33
+
34
+ def _authorization_error_message(email: str, exc: Exception) -> str:
35
+ detail = str(exc)
36
+ if "redirect_uri_mismatch" in detail.lower():
37
+ return (
38
+ f"A autorização da conta {email} foi rejeitada pelo Google (redirect_uri_mismatch). "
39
+ f"Use um cliente OAuth do tipo Desktop app ou adicione exactamente {loopback_redirect_uri()} "
40
+ "em Google Cloud > APIs e serviços > Credenciais > URIs de redireccionamento autorizados. "
41
+ "Não use uma URI sem a porta, com localhost diferente ou sem a barra final."
42
+ )
43
+ return f"A autorização da conta {email} falhou: {detail}"
14
44
 
15
45
 
16
46
  def _text(value: Any) -> str:
@@ -35,7 +65,7 @@ def _client_config(account: dict[str, Any]) -> dict[str, Any]:
35
65
  "client_secret": _text(account.get("client_secret")),
36
66
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
37
67
  "token_uri": "https://oauth2.googleapis.com/token",
38
- "redirect_uris": ["http://localhost"],
68
+ "redirect_uris": [loopback_redirect_uri()],
39
69
  }
40
70
  }
41
71
 
@@ -84,8 +114,8 @@ def authorize_account(account: dict[str, Any], storage_root: Path, *, open_brows
84
114
  try:
85
115
  flow = InstalledAppFlow.from_client_config(_client_config(account), BATCH_SCOPES)
86
116
  credentials = flow.run_local_server(
87
- host="localhost",
88
- port=0,
117
+ host=loopback_host(),
118
+ port=loopback_port(),
89
119
  open_browser=open_browser,
90
120
  access_type="offline",
91
121
  prompt="consent",
@@ -95,7 +125,7 @@ def authorize_account(account: dict[str, Any], storage_root: Path, *, open_brows
95
125
  _save_credentials(path, credentials)
96
126
  return IntegrationResult(True, f"Conta Google autorizada para listagem de canais: {email}.", {"status": "authorized", "email": email, "token_path": str(path)})
97
127
  except Exception as exc:
98
- return IntegrationResult(False, f"A autorização da conta {email} falhou: {exc}", {"status": "authorization_failed", "email": email})
128
+ return IntegrationResult(False, _authorization_error_message(email, exc), {"status": "authorization_failed", "email": email, "redirect_uri": loopback_redirect_uri()})
99
129
 
100
130
 
101
131
  def delete_account_token(account: dict[str, Any], storage_root: Path) -> None:
@@ -4,6 +4,7 @@ import json
4
4
  import os
5
5
  import re
6
6
  import secrets
7
+ import shutil
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
 
@@ -78,14 +79,16 @@ def parse_cookie_file(content: bytes, filename: str = "cookies.json") -> dict[st
78
79
  return {key: pairs[key] for key in COOKIE_KEYS}
79
80
 
80
81
 
81
- def parse_credentials_document(content: bytes, filename: str = DIRECT_DOCUMENT_NAME) -> dict[str, Any]:
82
+ def parse_credentials_document(content: bytes, filename: str = DIRECT_DOCUMENT_NAME, *, session_info_override: str = "") -> dict[str, Any]:
82
83
  try:
83
84
  raw = json.loads(content.decode("utf-8-sig", errors="replace"))
84
85
  except (UnicodeDecodeError, json.JSONDecodeError) as exc:
85
86
  raise ValueError(f"O documento de credenciais {filename} deve ser JSON válido.") from exc
86
87
  if not isinstance(raw, dict):
87
88
  raise ValueError(f"O documento de credenciais {filename} deve conter um objecto JSON.")
88
- document = _normalise_document(raw, {"id": raw.get("account_id", ""), "email": raw.get("email", "")})
89
+ document = _normalise_document(raw, {"id": raw.get("account_id", ""), "email": raw.get("email", ""), "sessionInfo": session_info_override})
90
+ if session_info_override.strip():
91
+ document["sessionInfo"] = session_info_override.strip()
89
92
  missing = [key for key in COOKIE_KEYS if not document["cookies"].get(key)]
90
93
  if missing:
91
94
  raise ValueError(f"Faltam cookies obrigatórios no documento {filename}: {', '.join(missing)}.")
@@ -114,6 +117,10 @@ def _channel_keys(channel: dict[str, Any]) -> list[str]:
114
117
  return keys
115
118
 
116
119
 
120
+ def _account_session_info(account: dict[str, Any]) -> str:
121
+ return str(account.get("sessionInfo") or account.get("session_info") or account.get("direct_session_info") or "").strip()
122
+
123
+
117
124
  def _normalise_document(raw: Any, account: dict[str, Any]) -> dict[str, Any]:
118
125
  raw = raw if isinstance(raw, dict) else {}
119
126
  cookies = _normalise_pairs(raw.get("cookies", raw))
@@ -129,7 +136,7 @@ def _normalise_document(raw: Any, account: dict[str, Any]) -> dict[str, Any]:
129
136
  return {
130
137
  "account_id": str(raw.get("account_id") or account.get("id") or "").strip(),
131
138
  "email": str(raw.get("email") or account.get("email") or "").strip(),
132
- "sessionInfo": str(raw.get("sessionInfo") or raw.get("session_info") or raw.get("direct_session_info") or "").strip(),
139
+ "sessionInfo": str(raw.get("sessionInfo") or raw.get("session_info") or raw.get("direct_session_info") or _account_session_info(account)).strip(),
133
140
  "cookies": {key: cookies.get(key, "") for key in COOKIE_KEYS},
134
141
  "INNERTUBE_API_KEY": str(raw.get("INNERTUBE_API_KEY") or raw.get("innertube_api_key") or raw.get("direct_innertube_api_key") or "").strip(),
135
142
  "chunk_size": _safe_chunk_size(raw.get("chunk_size", raw.get("direct_chunk_size", DEFAULT_CHUNK_SIZE))),
@@ -156,7 +163,7 @@ def _legacy_document(storage_root: Path, account: dict[str, Any], settings: dict
156
163
  return _normalise_document({
157
164
  "account_id": account.get("id"),
158
165
  "email": account.get("email"),
159
- "sessionInfo": account.get("direct_session_info") or settings.get("direct_session_info"),
166
+ "sessionInfo": _account_session_info(account) or settings.get("direct_session_info"),
160
167
  "cookies": legacy_cookies,
161
168
  "INNERTUBE_API_KEY": settings.get("direct_innertube_api_key"),
162
169
  "chunk_size": settings.get("direct_chunk_size", DEFAULT_CHUNK_SIZE),
@@ -191,6 +198,30 @@ def save_credentials_document(storage_root: Path, account: dict[str, Any], docum
191
198
  return destination
192
199
 
193
200
 
201
+ def update_credentials_document_session_info(storage_root: Path, account: dict[str, Any], session_info: str) -> Path | None:
202
+ """Update only sessionInfo in an existing credentials document."""
203
+ path = credentials_document_path(storage_root, account)
204
+ if not path.exists():
205
+ return None
206
+ raw = _read_json_document(path) or {}
207
+ document = _normalise_document(raw, {**account, "sessionInfo": session_info})
208
+ document["sessionInfo"] = str(session_info or "").strip()
209
+ return save_credentials_document(storage_root, account, document)
210
+
211
+
212
+ def delete_credentials_document(storage_root: Path, account: dict[str, Any]) -> None:
213
+ """Remove all direct-upload credentials belonging only to one Google account."""
214
+ if not str(account.get("id") or "").strip():
215
+ return
216
+ directory = account_directory(storage_root, account)
217
+ try:
218
+ shutil.rmtree(directory)
219
+ except FileNotFoundError:
220
+ pass
221
+ except OSError:
222
+ pass
223
+
224
+
194
225
  def _read_json_document(path: Path) -> dict[str, Any] | None:
195
226
  try:
196
227
  raw = json.loads(path.read_text(encoding="utf-8"))
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import Any, Callable
9
9
 
10
10
  from integrations.platforms import IntegrationResult
11
+ from integrations.youtube_batch import loopback_host, loopback_port, loopback_redirect_uri
11
12
 
12
13
 
13
14
  # Mantemos os mesmos escopos usados pelo youtube-automation-agent.
@@ -111,7 +112,7 @@ class _GoogleYouTubeBase:
111
112
  "client_secret": self.client_secret,
112
113
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
113
114
  "token_uri": "https://oauth2.googleapis.com/token",
114
- "redirect_uris": ["http://localhost"],
115
+ "redirect_uris": [loopback_redirect_uri()],
115
116
  }
116
117
  }
117
118
 
@@ -167,8 +168,8 @@ class _GoogleYouTubeBase:
167
168
  try:
168
169
  flow = InstalledAppFlow.from_client_config(self._client_config(), self.scopes)
169
170
  credentials = flow.run_local_server(
170
- host="localhost",
171
- port=0,
171
+ host=loopback_host(),
172
+ port=loopback_port(),
172
173
  open_browser=open_browser,
173
174
  access_type="offline",
174
175
  prompt="consent",
@@ -177,7 +178,17 @@ class _GoogleYouTubeBase:
177
178
  self._save_credentials(credentials, nested=self.__class__.__name__ == "YouTubeAutomationAgentUploader")
178
179
  return IntegrationResult(True, "Conta YouTube autorizada com sucesso.", {"status": "authorized", "token_path": str(self.token_path)})
179
180
  except Exception as exc:
180
- return IntegrationResult(False, f"A autorização Google falhou: {exc}", {"status": "authorization_failed"})
181
+ detail = str(exc)
182
+ if "redirect_uri_mismatch" in detail.lower():
183
+ message = (
184
+ "A autorização Google foi rejeitada (redirect_uri_mismatch). "
185
+ f"Use um cliente OAuth do tipo Desktop app ou adicione exactamente {loopback_redirect_uri()} "
186
+ "em Google Cloud > APIs e serviços > Credenciais > URIs de redireccionamento autorizados. "
187
+ "Não use uma URI sem a porta, com localhost diferente ou sem a barra final."
188
+ )
189
+ else:
190
+ message = f"A autorização Google falhou: {detail}"
191
+ return IntegrationResult(False, message, {"status": "authorization_failed", "redirect_uri": loopback_redirect_uri()})
181
192
 
182
193
  def status(self) -> IntegrationResult:
183
194
  if not self.configured:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.47",
3
+ "version": "0.2.48",
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",