@danhachuel/thunderbolt 0.2.46 → 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.
- package/MANUAL-INSTALACAO.md +10 -10
- package/README.md +14 -8
- package/app/main.py +49 -19
- package/hermes_ui/blueprints.py +3 -2
- package/integrations/youtube_batch.py +34 -4
- package/integrations/youtube_direct_credentials.py +35 -4
- package/integrations/youtube_upload.py +15 -4
- package/package.json +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -384,7 +384,7 @@ A API key não é gravada em logs, não é incluída no GitHub e não é enviada
|
|
|
384
384
|
Após iniciar a aplicação, valide o seguinte percurso:
|
|
385
385
|
|
|
386
386
|
1. **Início:** confirme que a UI abre, mostra o estado local, as métricas e os cards das filas do Pipeline, sem botões de acções rápidas.
|
|
387
|
-
2. **Blueprints:** coloque um JSON em `~/.thunderbolt/storage/blueprints/importados/` (Windows: `%LOCALAPPDATA%\\THUNDERBOLT\storage\blueprints\importados\`) ou use o carregador da interface.
|
|
387
|
+
2. **Blueprints:** coloque um JSON em `~/.thunderbolt/storage/blueprints/importados/` (Windows: `%LOCALAPPDATA%\\THUNDERBOLT\storage\blueprints\importados\`) ou use o carregador da interface. No formulário **Criar blueprint a partir de link**, preencha também **Nome do Blueprint**; o campo é obrigatório e o valor fica no `name` do JSON e no nome do ficheiro criado.
|
|
388
388
|
3. **Brandings:** abra a subaba **Brandings** e confirme a listagem dos ficheiros JSON.
|
|
389
389
|
4. **Canais:** em **Importar do YouTube**, use o método **Página pública — sem API Key** com um URL `/channel/UC...`, um handle ou uma subpágina `/videos`; o parser resolve o ID, consulta a página pública e tenta o RSS quando necessário. Confirme que o resultado abre o formulário de revisão sem Data API Key. Se o canal não existir ou não fornecer metadados, confirme a mensagem clara e que o formulário de uma pesquisa anterior desaparece. A Data API é opcional e fica separada; em **Cadastro manual**, preencha os dados sem qualquer consulta externa.
|
|
390
390
|
5. **Niche Finder Kaggle:** abra o menu expansível **Niche Finder**, seleccione **Niche Finder Kaggle**, defina os parâmetros dentro do conteúdo principal e confirme que não há preparação automática. Antes do clique, não deve existir download de dataset nem análise. Clique em **Analisar Nichos** para iniciar a preparação dos dados e a análise; depois altere país, engagement, datas e tags e clique novamente para aplicar os novos parâmetros. Em seguida, abra a alternativa independente **Niche Finder Apify**, configure as palavras-chave e filtros na própria aba e confirme que nada é executado antes de clicar em **Pesquisar no Apify**.
|
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
137
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
138
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
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:
|
|
@@ -159,7 +165,7 @@ O menu expansível **Niche Finder** contém duas alternativas independentes. **N
|
|
|
159
165
|
|
|
160
166
|
A interface apresenta dentro da aba os parâmetros da busca: número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, categoria de engagement, intervalo de datas e tags. A página permanece sem resultados até o primeiro clique em **Analisar Nichos**; depois, se os parâmetros forem alterados, mostra os resultados anteriores e pede novo clique para aplicar os filtros actuais. O núcleo aplica normalização, filtros, `log1p`, `StandardScaler`, K-Means e FP-Growth. Os resultados aparecem em DataFrames para clusters, itemsets frequentes, regras de associação e dados analisados, acompanhados por uma visualização Plotly nativa e pesquisa de palavras nos clusters.
|
|
161
167
|
|
|
162
|
-
**Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em Configurações Técnicas. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
168
|
+
**Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em Configurações Técnicas. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.47 install`; o instalador detecta e reutiliza componentes já válidos.
|
|
163
169
|
|
|
164
170
|
## Editor Python baseado no PYEdit
|
|
165
171
|
|
|
@@ -202,7 +208,7 @@ Para usar outro local, defina `THUNDERBOLT_STORAGE_DIR`. O estado é escrito de
|
|
|
202
208
|
|
|
203
209
|
Coloque ficheiros `.json` em `storage/blueprints/canais`, `storage/blueprints/nichos` ou `storage/blueprints/importados`. A aba **Blueprints** relê a pasta e mostra o conteúdo estruturado. A instalação já inclui 13 Blueprints seed, copiados apenas quando o ficheiro ainda não existe; actualizações não substituem Blueprints locais. Também é possível importar através do carregador da própria interface.
|
|
204
210
|
|
|
205
|
-
A mesma aba possui a subaba **Brandings**. No formulário **Criar blueprint a partir de link**, cole um link de canal,
|
|
211
|
+
A mesma aba possui a subaba **Brandings**. No formulário **Criar blueprint a partir de link**, cole um link de canal, informe o **Nome do Blueprint**, o nicho e o idioma e escolha entre **Apenas Blueprint** ou **Blueprint + Branding completo**. O nome é guardado no campo `name` do JSON e também é usado no nome do ficheiro. O primeiro modo grava o blueprint forense local; o segundo grava também um ficheiro de Branding com identidade do canal, handle, descrição, hashtags, keywords, prompts de imagem de perfil e banner, direcção visual de thumbnails, assets e checklist de revisão.
|
|
206
212
|
|
|
207
213
|
O fluxo foi modelado a partir do blueprint de clonagem com Branding anexado, incluindo a distinção entre entrada de canal/vídeo, normalização do link, metadados de nicho/idioma, perfil do canal, estratégia de conteúdo, pesquisa, identidade visual e brand pack. Placeholders de serviços externos são tratados como configuração local; chaves presentes em workflows importados não devem ser commitadas.
|
|
208
214
|
|
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
|
|
@@ -254,21 +254,25 @@ def render_blueprints():
|
|
|
254
254
|
st.subheader("Criar blueprint a partir de link")
|
|
255
255
|
with st.form("create_blueprint_from_link"):
|
|
256
256
|
source_url = st.text_input("Link do canal ou vídeo YouTube", placeholder="https://www.youtube.com/@canal ou https://youtu.be/video")
|
|
257
|
+
blueprint_name = st.text_input("Nome do Blueprint", placeholder="Ex.: Filosofia sombria — Canal X")
|
|
257
258
|
channel_name = st.text_input("Nome do canal, se conhecido")
|
|
258
259
|
niche = st.text_input("Nicho alvo", placeholder="Ex.: filosofia, história, finanças pessoais")
|
|
259
260
|
language = st.selectbox("Idioma do blueprint", ["Português (pt-BR)", "English", "Español"])
|
|
260
261
|
creation_type = st.radio("O que deseja criar?", ["Apenas Blueprint", "Blueprint + Branding completo"], horizontal=True)
|
|
261
262
|
create_submitted = st.form_submit_button("Criar a partir do link", type="primary")
|
|
262
263
|
if create_submitted:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
if not blueprint_name.strip():
|
|
265
|
+
st.error("Informe o nome do Blueprint antes de criar.")
|
|
266
|
+
else:
|
|
267
|
+
try:
|
|
268
|
+
blueprint, branding = create_blueprint_from_link(source_url, niche, language, creation_type == "Blueprint + Branding completo", channel_name, blueprint_name)
|
|
269
|
+
blueprint_path, branding_path = save_generated_blueprint(blueprint, branding)
|
|
270
|
+
st.success(f"Blueprint criado: {blueprint_path.name}")
|
|
271
|
+
if branding_path:
|
|
272
|
+
st.success(f"Branding completo criado: {branding_path.name}")
|
|
273
|
+
st.rerun()
|
|
274
|
+
except ValueError as exc:
|
|
275
|
+
st.error(str(exc))
|
|
272
276
|
st.divider()
|
|
273
277
|
st.subheader("Importar blueprint JSON")
|
|
274
278
|
uploaded = st.file_uploader("Subir novo blueprint JSON", type=["json"], key="blueprint_upload")
|
|
@@ -1462,6 +1466,13 @@ def render_settings():
|
|
|
1462
1466
|
account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
|
|
1463
1467
|
with account_cols[3]:
|
|
1464
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
|
+
)
|
|
1465
1476
|
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", use_container_width=True)
|
|
1466
1477
|
direct_status = direct_account_status(STORAGE, batch_account)
|
|
1467
1478
|
st.markdown("**Credenciais do Upload directo desta conta Google**")
|
|
@@ -1494,14 +1505,30 @@ def render_settings():
|
|
|
1494
1505
|
if result.ok:
|
|
1495
1506
|
st.rerun()
|
|
1496
1507
|
with status_cols[2]:
|
|
1497
|
-
if st.button("
|
|
1508
|
+
if st.button("Apagar conta", icon=":material/delete:", key=f"batch_remove_settings_{account_id}", use_container_width=True):
|
|
1498
1509
|
delete_youtube_batch_token(batch_account, STORAGE)
|
|
1510
|
+
delete_credentials_document(STORAGE, batch_account)
|
|
1499
1511
|
remaining_accounts = [account for account in batch_accounts if str(account.get("id")) != account_id]
|
|
1500
1512
|
settings["youtube_batch_accounts"] = remaining_accounts
|
|
1501
1513
|
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
1502
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)
|
|
1503
1523
|
write_json("settings.json", settings)
|
|
1504
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()
|
|
1505
1532
|
if save_account:
|
|
1506
1533
|
if "@" not in account_email.strip():
|
|
1507
1534
|
st.error("Informe um e-mail Google válido.")
|
|
@@ -1513,7 +1540,8 @@ def render_settings():
|
|
|
1513
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())))
|
|
1514
1541
|
if credentials_changed:
|
|
1515
1542
|
delete_youtube_batch_token(existing, STORAGE)
|
|
1516
|
-
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())
|
|
1517
1545
|
settings["youtube_batch_accounts"] = batch_accounts
|
|
1518
1546
|
write_json("settings.json", settings)
|
|
1519
1547
|
st.success("Conta Google/YouTube guardada.")
|
|
@@ -1524,7 +1552,7 @@ def render_settings():
|
|
|
1524
1552
|
st.error("Seleccione o documento JSON completo desta conta Google antes de guardar.")
|
|
1525
1553
|
else:
|
|
1526
1554
|
try:
|
|
1527
|
-
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())
|
|
1528
1556
|
document["account_id"] = account_id
|
|
1529
1557
|
document["email"] = str(batch_account.get("email", ""))
|
|
1530
1558
|
save_credentials_document(STORAGE, batch_account, document)
|
|
@@ -1544,8 +1572,9 @@ def render_settings():
|
|
|
1544
1572
|
new_account_client_id = st.text_input("OAuth Client ID", key="new_batch_account_client_id")
|
|
1545
1573
|
with add_cols[3]:
|
|
1546
1574
|
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
1547
|
-
|
|
1548
|
-
st.
|
|
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.")
|
|
1549
1578
|
add_account = st.form_submit_button("Adicionar conta Google/YouTube com documento de Upload directo", use_container_width=True)
|
|
1550
1579
|
if add_account:
|
|
1551
1580
|
document_error = ""
|
|
@@ -1555,14 +1584,14 @@ def render_settings():
|
|
|
1555
1584
|
st.error("Informe o Client ID e o Client Secret da nova conta.")
|
|
1556
1585
|
elif new_account_document is not None:
|
|
1557
1586
|
try:
|
|
1558
|
-
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())
|
|
1559
1588
|
except ValueError as exc:
|
|
1560
1589
|
document_error = str(exc)
|
|
1561
1590
|
st.error(document_error)
|
|
1562
1591
|
if "@" in new_account_email.strip() and new_account_client_id.strip() and new_account_client_secret.strip() and not document_error:
|
|
1563
|
-
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()}
|
|
1564
1593
|
if new_account_document is not None:
|
|
1565
|
-
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())
|
|
1566
1595
|
document["account_id"] = new_account["id"]
|
|
1567
1596
|
document["email"] = new_account["email"]
|
|
1568
1597
|
save_credentials_document(STORAGE, new_account, document)
|
|
@@ -1582,6 +1611,7 @@ def render_settings():
|
|
|
1582
1611
|
moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
|
|
1583
1612
|
st.markdown("**YouTube — OAuth 2.0 e consulta pública**")
|
|
1584
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()}`.")
|
|
1585
1615
|
youtube_cols = st.columns(2)
|
|
1586
1616
|
with youtube_cols[0]:
|
|
1587
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.")
|
package/hermes_ui/blueprints.py
CHANGED
|
@@ -42,14 +42,15 @@ def parse_youtube_link(url: str) -> dict[str, str]:
|
|
|
42
42
|
return {"original_url": value, "video_id": video_id, "channel_ref": channel_ref, "input_type": input_type}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def create_blueprint_from_link(url: str, niche: str, language: str, include_branding: bool, channel_name: str = "") -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
45
|
+
def create_blueprint_from_link(url: str, niche: str, language: str, include_branding: bool, channel_name: str = "", blueprint_name: str = "") -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
46
46
|
parsed = parse_youtube_link(url)
|
|
47
47
|
base_name = channel_name.strip() or parsed["channel_ref"] or "Canal analisado"
|
|
48
|
+
display_name = blueprint_name.strip() or f"Blueprint — {base_name}"
|
|
48
49
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
49
50
|
blueprint_id = f"bp_{uuid.uuid4().hex[:10]}"
|
|
50
51
|
blueprint = {
|
|
51
52
|
"id": blueprint_id,
|
|
52
|
-
"name":
|
|
53
|
+
"name": display_name,
|
|
53
54
|
"metadata": {
|
|
54
55
|
"task_type": "forensic_content_blueprint",
|
|
55
56
|
"source_url": parsed["original_url"],
|
|
@@ -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": [
|
|
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=
|
|
88
|
-
port=
|
|
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,
|
|
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
|
|
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
|
|
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": [
|
|
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=
|
|
171
|
-
port=
|
|
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
|
-
|
|
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