@danhachuel/thunderbolt 0.2.51 → 0.2.53
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 +15 -15
- package/README.md +19 -17
- package/app/main.py +106 -90
- package/integrations/youtube_direct_credentials.py +55 -5
- 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.53
|
|
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.53 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.53 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.
|
|
@@ -345,7 +345,7 @@ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
|
|
345
345
|
|
|
346
346
|
## 8. Configuração inicial da UI
|
|
347
347
|
|
|
348
|
-
Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Automação**, **Edição**, **Models AI** e **Configurações**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **Models AI** contém **Personagens** e **Redes Sociais**, nessa ordem; ambas estão reservadas para desenvolvimento futuro e não executam operações. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. Abra **Configurações > Configurações Técnicas** e reveja. A página está organizada em três subabas sem numeração visível, na ordem **Contas Google/YouTube — canais em lote**, **API Keys** e **Teste de vozes**. A subaba de contas contém os registos Google, sessionInfo e documentos de Upload directo; **API Keys** reúne execução local, OAuth, LLM, TTS, materiais e restantes credenciais; **Teste de vozes** contém apenas o preview de áudio. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas** e **Upload**. O menu **Configurações** contém **Canais**, **Blueprints**, **MCP** e **Configurações Técnicas**. Para YouTube, preencha primeiro o par **OAuth Client ID + OAuth Client Secret** se pretende autorizar uploads. A **YouTube Data API Key** é uma credencial Google Cloud diferente e fica numa área opcional, apenas para o método oficial de métricas; Client ID + Client Secret não formam uma API Key.
|
|
348
|
+
Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Automação**, **Edição**, **Models AI** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **Models AI** contém **Personagens** e **Redes Sociais**, nessa ordem; ambas estão reservadas para desenvolvimento futuro e não executam operações. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. Abra **Configurações > Configurações Técnicas** e reveja. A página está organizada em três subabas sem numeração visível, na ordem **Contas Google/YouTube — canais em lote**, **API Keys** e **Teste de vozes**. A subaba de contas contém os registos Google, sessionInfo e documentos de Upload directo; **API Keys** reúne execução local, OAuth, LLM, TTS, materiais e restantes credenciais; **Teste de vozes** contém apenas o preview de áudio. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas** e **Upload**. O menu **Configurações** contém **Canais Youtube**, **Blueprints Youtube**, **MCP** e **Configurações Técnicas**. Para YouTube, preencha primeiro o par **OAuth Client ID + OAuth Client Secret** se pretende autorizar uploads. A **YouTube Data API Key** é uma credencial Google Cloud diferente e fica numa área opcional, apenas para o método oficial de métricas; Client ID + Client Secret não formam uma API Key.
|
|
349
349
|
|
|
350
350
|
| Configuração | Finalidade |
|
|
351
351
|
|---|---|
|
|
@@ -396,17 +396,17 @@ Após iniciar a aplicação, valide o seguinte percurso:
|
|
|
396
396
|
11. **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.
|
|
397
397
|
12. **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.
|
|
398
398
|
13. **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`.
|
|
399
|
-
14. **Canais:** abra **Configurações > Canais**, clique em **Editar** num cartão, altere o nicho, Blueprint/Prompts do Canal, Narrador/Voz padrão e horário, guarde e confirme que o cartão é actualizado sem duplicar o canal. Clique em **Actualizar últimos 10 vídeos**, valide a vista Lista, mude para Kanban e abra **Editar vídeo** para testar título, estado, data, URL e notas.
|
|
400
|
-
15. **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
|
+
14. **Canais Youtube:** abra **Configurações > Canais Youtube**, clique em **Editar** num cartão, altere o nicho, Blueprint/Prompts do Canal, Narrador/Voz padrão e horário, guarde e confirme que o cartão é actualizado sem duplicar o canal. Clique em **Actualizar últimos 10 vídeos**, valide a vista Lista, mude para Kanban e abra **Editar vídeo** para testar título, estado, data, URL e notas.
|
|
400
|
+
15. **Automação Youtube:** 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 Youtube** 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.
|
|
401
401
|
16. **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.
|
|
402
|
-
17. **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.
|
|
402
|
+
17. **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 Youtube > 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.
|
|
403
403
|
18. **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.
|
|
404
404
|
19. **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.
|
|
405
405
|
20. **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.
|
|
406
406
|
|
|
407
407
|
## Gestão de Canais, edição e vídeos recentes
|
|
408
408
|
|
|
409
|
-
Na página **Configurações > Canais**, cada canal cadastrado aparece num cartão com o botão **Editar**. O editor permite alterar o nome, URL, handle, idioma, estilo wide, **Canais de Referência / Nicho**, **Prompts do Canal** (Blueprint padrão), **Narrador** (Voz padrão), conta Google do Upload directo, descrição e Automação ON/horário. Guardar alterações actualiza o mesmo registo local; não é necessário apagar e criar o canal novamente.
|
|
409
|
+
Na página **Configurações > Canais Youtube**, cada canal cadastrado aparece num cartão com o botão **Editar**. O editor permite alterar o nome, URL, handle, idioma, estilo wide, **Canais de Referência / Nicho**, **Prompts do Canal** (Blueprint padrão), **Narrador** (Voz padrão), conta Google do Upload directo, descrição e Automação ON/horário. Guardar alterações actualiza o mesmo registo local; não é necessário apagar e criar o canal novamente.
|
|
410
410
|
|
|
411
411
|
O cartão mostra o nicho ou Canais de Referência imediatamente abaixo do nome. Os três blocos de gestão usam os rótulos **Prompts do Canal**, **Canais de Referência** e **Narrador**, com botões que abrem o editor do próprio canal.
|
|
412
412
|
|
|
@@ -414,7 +414,7 @@ A secção **Últimos 10 vídeos publicados** fica abaixo das configurações do
|
|
|
414
414
|
|
|
415
415
|
## Canais em lote por conta Google/YouTube
|
|
416
416
|
|
|
417
|
-
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`.
|
|
417
|
+
A subaba **Canais em lote gmail**, dentro de **Canais Youtube**, 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`.
|
|
418
418
|
|
|
419
419
|
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.
|
|
420
420
|
|
|
@@ -428,7 +428,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
|
|
|
428
428
|
|
|
429
429
|
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.
|
|
430
430
|
|
|
431
|
-
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.
|
|
431
|
+
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.53 install`; o instalador detecta e reutiliza o que já estiver válido.
|
|
432
432
|
|
|
433
433
|
### Niche Finder Apify
|
|
434
434
|
|
|
@@ -460,19 +460,19 @@ No modo **Lote geral**, todos os canais cadastrados são incluídos automaticame
|
|
|
460
460
|
|
|
461
461
|
O modo `Pexels/Pixabay` representa materiais de stock. Ao seleccionar `full_ia`, o selector `Estilo IA` apresenta os 12 estilos disponíveis. Ao seleccionar `Apenas Música`, a UI obriga a escolher uma música existente, carregar um ficheiro ou solicitar uma música a um endpoint Suno configurado. O vídeo recebe `background_mode=none`, sem fundo Pexels/Pixabay ou IA.
|
|
462
462
|
|
|
463
|
-
A aba **Automação** lista os canais e vídeos, permite escolher e guardar **Blueprint padrão** e **Voz padrão** por canal, guarda **Automação ON** e valida horários diários no formato `HH:MM`. Os valores ficam sincronizados com o editor existente no cartão da aba **Canais** e são copiados para novas tarefas. O launcher inicia o worker local, que consulta o relógio do computador, gera um briefing, título e pacote de thumbnail específicos com o provider LLM configurado, cria o lote agendado na fila e evita duplicar o mesmo canal no mesmo dia. Se a geração não puder ser executada, o erro fica registado e o placeholder antigo não é usado.
|
|
463
|
+
A aba **Automação Youtube**, dentro do menu expansível **Automação**, lista os canais e vídeos, permite escolher e guardar **Blueprint padrão** e **Voz padrão** por canal, guarda **Automação ON** e valida horários diários no formato `HH:MM`. Os valores ficam sincronizados com o editor existente no cartão da aba **Canais Youtube** e são copiados para novas tarefas. O launcher inicia o worker local, que consulta o relógio do computador, gera um briefing, título e pacote de thumbnail específicos com o provider LLM configurado, cria o lote agendado na fila e evita duplicar o mesmo canal no mesmo dia. Se a geração não puder ser executada, o erro fica registado e o placeholder antigo não é usado.
|
|
464
464
|
|
|
465
465
|
A área **Teste de vozes**, dentro de **Configurações > Configurações Técnicas**, é isolada da pipeline. O preview pode ser reproduzido e descarregado de `storage/voice_previews/`. Caminhos vazios, directórios, ficheiros vazios e previews sem permissões de leitura são ignorados com uma mensagem clara, sem traceback. Se uma instalação antiga mostrar que `edge-tts` está em falta, execute `npx.cmd --yes @danhachuel/thunderbolt install`; o detector reinstala apenas a dependência ausente.
|
|
466
466
|
|
|
467
467
|
## Upload directo
|
|
468
468
|
|
|
469
|
-
Em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada
|
|
469
|
+
Em **Configurações > Configurações Técnicas > Contas Google/YouTube — canais em lote**, cada conta aparece como um expander identificado por nome e e-mail. Dentro dele existe o uploader **Subir documento de cookies/credenciais** 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`.
|
|
470
470
|
|
|
471
|
-
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.
|
|
471
|
+
Em **Canais Youtube > 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.
|
|
472
472
|
|
|
473
|
-
A subaba **Upload directo** adapta o [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api). Cada conta Google tem um único `credentials.json`, com cookies, sessionInfo, INNERTUBE_API_KEY, chunk_size e `delegated_session_ids` por canal. O
|
|
473
|
+
A subaba **Upload directo** adapta o [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api). Cada conta Google tem um único `credentials.json`, criado automaticamente ao adicionar a conta, com cookies, sessionInfo, INNERTUBE_API_KEY, chunk_size e `delegated_session_ids` por canal. O uploader aceita um documento JSON completo ou parcial; o merge actualiza somente os valores presentes e preserva os restantes. O documento é guardado fora de `storage/state/`, com permissões locais restritas.
|
|
474
474
|
|
|
475
|
-
Associe cada canal à conta Google correcta em **Canais**; não introduza o ID delegado na UI. No momento do upload, o Thunderbolt lê o documento da conta, encontra o `DELEGATED_SESSION_ID` pela chave do canal e bloqueia a operação se faltar conta, documento válido, cookies, sessionInfo, INNERTUBE_API_KEY, ID delegado ou vídeo elegível. O tamanho de chunk é normalizado para múltiplos de 262144 bytes. O método é uma integração não oficial de sessão do YouTube; não extraia cookies automaticamente, não os coloque no repositório e não os partilhe.
|
|
475
|
+
Associe cada canal à conta Google correcta em **Canais Youtube**; não introduza o ID delegado na UI. A associação do canal é permitida mesmo que o documento esteja incompleto. No momento do upload, o Thunderbolt lê o documento da conta, encontra o `DELEGATED_SESSION_ID` pela chave do canal e bloqueia apenas a operação se faltar conta, documento válido, cookies, sessionInfo, INNERTUBE_API_KEY, ID delegado ou vídeo elegível. O tamanho de chunk é normalizado para múltiplos de 262144 bytes. O método é uma integração não oficial de sessão do YouTube; não extraia cookies automaticamente, não os coloque no repositório e não os partilhe.
|
|
476
476
|
|
|
477
477
|
## MCP e integrações externas
|
|
478
478
|
|
package/README.md
CHANGED
|
@@ -12,39 +12,41 @@ A primeira versão implementa a camada UI independente com:
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| Início | Resumo de canais, tarefas, backlog, execução e falhas, com as filas do Pipeline inline |
|
|
14
14
|
| Pipeline | Menu expansível com Criação de Vídeos, Criação de Músicas e Upload |
|
|
15
|
-
| Blueprints | Leitura da pasta `storage/blueprints/`, upload/validação de JSON e criação a partir de link YouTube |
|
|
15
|
+
| Blueprints Youtube | Leitura da pasta `storage/blueprints/`, upload/validação de JSON e criação a partir de link YouTube |
|
|
16
16
|
| Brandings | Subaba própria dentro de Blueprints, upload/listagem de Brandings e criação conjunta com Blueprint |
|
|
17
|
-
| Canais | Subabas de importação pública sem API Key, Canais em lote gmail por conta Google/YouTube via OAuth e cadastro manual independente; cartões com botão Editar, nicho/referências visível, Prompts do Canal/Blueprint, Narrador/voz e gestão dos últimos 10 vídeos |
|
|
17
|
+
| Canais Youtube | Subabas de importação pública sem API Key, Canais em lote gmail por conta Google/YouTube via OAuth e cadastro manual independente; cartões com botão Editar, nicho/referências visível, Prompts do Canal/Blueprint, Narrador/voz e gestão dos últimos 10 vídeos |
|
|
18
18
|
| Criação de Vídeos / Criação de Músicas | Subabas Criar vídeo e Vídeos; lotes; 51 rótulos de idioma; Pexels/Pixabay, full IA com Estilo IA e Apenas Música com agente musical; a segunda página reutiliza o mesmo fluxo com título próprio |
|
|
19
|
-
| Automação |
|
|
19
|
+
| Automação | Menu expansível com a subaba **Automação Youtube**, onde ficam os vídeos e canais, selectores editáveis de Blueprint/voz padrão, Automação ON, horário diário HH:MM e worker local baseado no relógio do computador |
|
|
20
20
|
| Models AI | Menu expansível com Personagens e Redes Sociais reservados para desenvolvimento futuro |
|
|
21
21
|
| Niche Finder | Menu expansível com duas alternativas independentes: Niche Finder Kaggle e Niche Finder Apify, com parâmetros, execução e resultados separados |
|
|
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, Client Secret e sessionInfo por conta,
|
|
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, cartões expansíveis por nome/e-mail, documento credentials.json criado automaticamente e eliminação individual, 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
|
|
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 conta aparece como um cartão expansível identificado por **nome — e-mail**. Dentro do cartão existe o uploader **Subir documento de cookies/credenciais** e o único campo técnico permitido na UI, **sessionInfo token desta conta Google**. Cookies, `INNERTUBE_API_KEY`, `chunk_size` e `delegated_session_ids` continuam exclusivamente no documento.
|
|
31
31
|
|
|
32
|
-
O
|
|
32
|
+
O botão **Adicionar outra conta Gmail** fica abaixo de uma divisória, fora dos cartões existentes. Ao criar uma conta, o Thunderbolt cria imediatamente `credentials.json` com placeholders vazios. Um upload completo ou parcial é incorporado por merge: os cookies enviados actualizam apenas os cookies fornecidos e valores como `sessionInfo`, `INNERTUBE_API_KEY`, `chunk_size` e `delegated_session_ids` existentes não são apagados. O alerta **Documento incompleto: SID, SSID, HSID, APISID, SAPISID, sessionInfo, INNERTUBE_API_KEY** permanece visível por conta quando necessário.
|
|
33
|
+
|
|
34
|
+
O documento é 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; essa associação continua permitida mesmo quando o documento está incompleto. O uploader procura o ID do canal dentro do mapa `delegated_session_ids` do documento e usa-o como `pageId`/`onBehalfOfUser`; só a operação de Upload directo é bloqueada quando faltarem os dados técnicos. Não existem campos separados de cookies, INNERTUBE_API_KEY, chunk_size ou DELEGATED_SESSION_ID na UI.
|
|
33
35
|
|
|
34
36
|
Os dados são segredos de sessão. Os valores não aparecem em tabelas ou logs, o ficheiro é escrito com permissões locais restritas e não é incluído no Git. O Thunderbolt não extrai cookies automaticamente do browser e não envia as credenciais para o GitHub. O método é não oficial e pode deixar de funcionar se o YouTube alterar os endpoints internos.
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
Os adaptadores do MoneyPrinterTurbo e de publicação nas plataformas são ligados pelas configurações locais e pelos pontos de integração em `integrations/`. A UI não inventa dados quando um serviço externo ou credencial não está disponível.
|
|
38
40
|
|
|
39
|
-
## Navegação da UI 0.2.
|
|
41
|
+
## Navegação da UI 0.2.53
|
|
40
42
|
|
|
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.
|
|
43
|
+
A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Automação**, **Edição**, **Models AI** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes** e **Editor Python**, nessa ordem. **Models AI** é expansível e contém **Personagens** e **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 Youtube**, **Blueprints Youtube**, **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
44
|
|
|
43
|
-
Dentro de **Configurações Técnicas**, a UI está organizada em três subabas, sem numeração visível: **Contas Google/YouTube — canais em lote**, **API Keys** e **Teste de vozes**. A primeira contém contas Google, sessionInfo e documentos de Upload directo; **API Keys** reúne execução local, OAuth, LLM, TTS, materiais e restantes credenciais; **Teste de vozes** contém exclusivamente o preview de áudio.
|
|
45
|
+
Dentro de **Configurações Técnicas**, a UI está organizada em três subabas, sem numeração visível: **Contas Google/YouTube — canais em lote**, **API Keys** e **Teste de vozes**. A primeira contém cartões expansíveis de contas Google, sessionInfo e documentos de Upload directo; o formulário **Adicionar outra conta Gmail** fica fora dos cartões. **API Keys** reúne execução local, OAuth, LLM, TTS, materiais e restantes credenciais; **Teste de vozes** contém exclusivamente o preview de áudio.
|
|
44
46
|
|
|
45
|
-
## Canais — edição por cartão e vídeos recentes
|
|
47
|
+
## Canais Youtube — edição por cartão e vídeos recentes
|
|
46
48
|
|
|
47
|
-
A página **Canais** mantém o cadastro e a importação existentes, mas cada cartão agora tem o botão **Editar**. O editor permite alterar nome, URL, handle, idioma, estilo wide, **Canais de Referência / Nicho**, **Prompts do Canal** (Blueprint padrão), **Narrador** (voz padrão), conta Google do Upload directo, descrição e Automação ON/horário. O nicho aparece imediatamente abaixo do nome do canal no cartão; quando não existe, a UI mostra **SEM NICHO CONFIGURADO**.
|
|
49
|
+
A página **Canais Youtube** mantém o cadastro e a importação existentes, mas cada cartão agora tem o botão **Editar**. O editor permite alterar nome, URL, handle, idioma, estilo wide, **Canais de Referência / Nicho**, **Prompts do Canal** (Blueprint padrão), **Narrador** (voz padrão), conta Google do Upload directo, descrição e Automação ON/horário. O nicho aparece imediatamente abaixo do nome do canal no cartão; quando não existe, a UI mostra **SEM NICHO CONFIGURADO**.
|
|
48
50
|
|
|
49
51
|
Os blocos do cartão usam a nomenclatura solicitada: **Prompts do Canal**, **Canais de Referência** e **Narrador**. Os botões de acção abrem o mesmo editor persistente, sem criar um segundo canal nem perder as associações existentes.
|
|
50
52
|
|
|
@@ -72,7 +74,7 @@ A consulta só é executada quando o utilizador carrega no botão; a aplicação
|
|
|
72
74
|
|
|
73
75
|
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.
|
|
74
76
|
|
|
75
|
-
A área **Contas Google/YouTube — canais em lote** permite manter várias contas. Cada cartão tem
|
|
77
|
+
A área **Contas Google/YouTube — canais em lote** permite manter várias contas em cartões expansíveis com nome e e-mail. Cada cartão tem OAuth Client ID, OAuth Client Secret, sessionInfo e documento de Upload directo; o formulário separado **Adicionar outra conta Gmail** cria o documento padrão automaticamente. Use **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.
|
|
76
78
|
|
|
77
79
|
## Instalação
|
|
78
80
|
|
|
@@ -224,9 +226,9 @@ storage/
|
|
|
224
226
|
|
|
225
227
|
Para usar outro local, defina `THUNDERBOLT_STORAGE_DIR`. O estado é escrito de forma atómica e ficheiros JSON inválidos são preservados com cópia `.corrupt-*` antes de serem recriados.
|
|
226
228
|
|
|
227
|
-
## Blueprints e Brandings
|
|
229
|
+
## Blueprints Youtube e Brandings
|
|
228
230
|
|
|
229
|
-
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.
|
|
231
|
+
Coloque ficheiros `.json` em `storage/blueprints/canais`, `storage/blueprints/nichos` ou `storage/blueprints/importados`. A aba **Blueprints Youtube** 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.
|
|
230
232
|
|
|
231
233
|
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.
|
|
232
234
|
|
|
@@ -240,7 +242,7 @@ A descrição segue a composição do workflow de referência: preview, secção
|
|
|
240
242
|
|
|
241
243
|
## Canais YouTube
|
|
242
244
|
|
|
243
|
-
A aba **Canais** está dividida em dois fluxos independentes. Em **Importar do YouTube**, o método padrão **Página pública — sem API Key** consulta a página pública do canal e preenche nome, ID, handle, URL, descrição e thumbnail quando esses dados estão disponíveis. A opção **YouTube Data API — API Key opcional** pode ser escolhida para métricas oficiais quando uma **YouTube Data API Key própria** estiver configurada em **Configurações** ou em `YOUTUBE_API_KEY`; essa chave é distinta do OAuth Client ID e Client Secret.
|
|
245
|
+
A aba **Canais Youtube** está dividida em dois fluxos independentes. Em **Importar do YouTube**, o método padrão **Página pública — sem API Key** consulta a página pública do canal e preenche nome, ID, handle, URL, descrição e thumbnail quando esses dados estão disponíveis. A opção **YouTube Data API — API Key opcional** pode ser escolhida para métricas oficiais quando uma **YouTube Data API Key própria** estiver configurada em **Configurações** ou em `YOUTUBE_API_KEY`; essa chave é distinta do OAuth Client ID e Client Secret.
|
|
244
246
|
|
|
245
247
|
Em **Cadastro manual**, nenhum pedido ao YouTube é feito e não existe qualquer dependência de API Key. O utilizador pode preencher nome, URL, handle, descrição, métricas, thumbnail, idioma, estilo, Blueprint padrão, voz padrão, `DELEGATED_SESSION_ID` e configuração de Automação. A importação pública nunca grava automaticamente: os dados aparecem num formulário de revisão antes de guardar. A resolução pública aceita URLs `/channel/UC...`, handles e subpáginas; quando existe um ID mas o HTML não traz todos os dados, tenta o feed RSS público. Se o YouTube responder que o canal não existe ou não fornecer metadados, a UI mostra uma mensagem clara e não mantém o formulário de uma pesquisa anterior. Cada cartão de canal tem **Activo** e, logo abaixo, **Apagar canal**, com confirmação; tarefas e artefactos não são apagados.
|
|
246
248
|
|
|
@@ -254,11 +256,11 @@ Use **Autorizar fallback OAuth** apenas se precisar de uma autorização separad
|
|
|
254
256
|
|
|
255
257
|
A subaba **Upload directo** adapta o [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api). Ela usa cookies, `sessionInfo`, `INNERTUBE_API_KEY` e `DELEGATED_SESSION_ID` fornecidos manualmente pelo utilizador, cria o vídeo através do endpoint interno e envia o ficheiro em chunks de 256 KiB. Este caminho é experimental, não extrai cookies automaticamente e fica separado do agente YouTube principal.
|
|
256
258
|
|
|
257
|
-
## Pipeline: Criação de Vídeos, Criação de Músicas e Automação
|
|
259
|
+
## Pipeline: Criação de Vídeos, Criação de Músicas e Automação Youtube
|
|
258
260
|
|
|
259
261
|
Em **Criação de Vídeos**, o estilo visual `Pexels/Pixabay` é o modo de materiais, `full_ia` abre o selector **Estilo IA** com os 12 estilos solicitados e **Apenas Música** exige um áudio local, um upload musical ou um pedido ao endpoint Suno configurado. **Criação de Músicas** reutiliza o mesmo conteúdo e formulário com título próprio. Neste último modo, a task fica com `background_mode=none`: não são gerados fundos Pexels/Pixabay nem fundos IA.
|
|
260
262
|
|
|
261
|
-
O agente musical guarda os ficheiros em `storage/music/`, aceita formatos de áudio comuns e pode descarregar uma URL de áudio devolvida por um endpoint Suno compatível. Em **Canais**, abra **Definir Blueprint e voz padrão** no cartão do canal para guardar os defaults; em **Automação**, os mesmos dois selectores aparecem no cartão e são sincronizados. Esses valores são usados automaticamente em novas tarefas criadas para o canal. A aba **Automação** também guarda `Automação ON` e um horário diário `HH:MM` por canal e lista os vídeos cadastrados; por definição, esta entrega não executa workers de fundo.
|
|
263
|
+
O agente musical guarda os ficheiros em `storage/music/`, aceita formatos de áudio comuns e pode descarregar uma URL de áudio devolvida por um endpoint Suno compatível. Em **Canais Youtube**, abra **Definir Blueprint e voz padrão** no cartão do canal para guardar os defaults; em **Automação Youtube**, os mesmos dois selectores aparecem no cartão e são sincronizados. Esses valores são usados automaticamente em novas tarefas criadas para o canal. A aba **Automação Youtube** também guarda `Automação ON` e um horário diário `HH:MM` por canal e lista os vídeos cadastrados; por definição, esta entrega não executa workers de fundo.
|
|
262
264
|
|
|
263
265
|
## Teste de vozes
|
|
264
266
|
|
package/app/main.py
CHANGED
|
@@ -37,7 +37,7 @@ from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesiz
|
|
|
37
37
|
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_topic_for_channel
|
|
38
38
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
39
39
|
from integrations.youtube_direct_upload import YouTubeDirectUploader
|
|
40
|
-
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_direct_credentials import delete_credentials_document, direct_account_status, document_status, ensure_credentials_document, merge_credentials_document, parse_credentials_document, save_credentials_document, update_credentials_document_session_info
|
|
41
41
|
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
|
|
42
42
|
from integrations.local_runtime import MoneyPrinterRuntime
|
|
43
43
|
from integrations.moneyprinter_config import sync_moneyprinter_config
|
|
@@ -490,7 +490,7 @@ def render_dashboard():
|
|
|
490
490
|
|
|
491
491
|
|
|
492
492
|
def render_blueprints():
|
|
493
|
-
st.title("Blueprints")
|
|
493
|
+
st.title("Blueprints Youtube")
|
|
494
494
|
st.caption(f"Biblioteca local lida directamente de `{BLUEPRINTS}`")
|
|
495
495
|
blueprint_tab, branding_tab = st.tabs(["Blueprints", "Brandings"])
|
|
496
496
|
with blueprint_tab:
|
|
@@ -588,7 +588,7 @@ def render_blueprints():
|
|
|
588
588
|
|
|
589
589
|
|
|
590
590
|
def render_channels():
|
|
591
|
-
st.title("Canais")
|
|
591
|
+
st.title("Canais Youtube")
|
|
592
592
|
st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
|
|
593
593
|
settings = read_json("settings.json", {})
|
|
594
594
|
youtube = YouTubeAdapter(settings=settings)
|
|
@@ -1651,7 +1651,7 @@ def render_videos():
|
|
|
1651
1651
|
|
|
1652
1652
|
|
|
1653
1653
|
def render_automation():
|
|
1654
|
-
st.title("Automação")
|
|
1654
|
+
st.title("Automação Youtube")
|
|
1655
1655
|
st.caption("Agendamento diário da geração por canal. O worker verifica o relógio local do computador e coloca os lotes agendados na fila.")
|
|
1656
1656
|
worker_status = load_worker_status()
|
|
1657
1657
|
local_now = datetime.now().astimezone()
|
|
@@ -1914,53 +1914,60 @@ def render_settings():
|
|
|
1914
1914
|
|
|
1915
1915
|
with google_accounts_tab:
|
|
1916
1916
|
st.subheader("Contas Google/YouTube — canais em lote")
|
|
1917
|
-
st.caption("
|
|
1917
|
+
st.caption("Abra uma conta para ver e editar os detalhes. O e-mail identifica a conta Google; esta área não lê a caixa Gmail.")
|
|
1918
1918
|
batch_accounts = [account for account in settings.get("youtube_batch_accounts", []) if isinstance(account, dict) and account.get("id")]
|
|
1919
|
-
|
|
1920
|
-
|
|
1919
|
+
channel_state = read_json("channels.json", [])
|
|
1920
|
+
youtube_accounts_missing_document: list[str] = []
|
|
1921
|
+
|
|
1921
1922
|
for batch_account in batch_accounts:
|
|
1922
1923
|
account_id = str(batch_account["id"])
|
|
1923
|
-
|
|
1924
|
+
account_email_snapshot = str(batch_account.get("email") or "sem e-mail")
|
|
1925
|
+
account_label_snapshot = str(batch_account.get("label") or "Canais YouTube")
|
|
1926
|
+
ensure_credentials_document(STORAGE, batch_account, settings, channel_state)
|
|
1927
|
+
direct_status = direct_account_status(STORAGE, batch_account)
|
|
1928
|
+
missing_document_parts = list(direct_status.get("missing_cookies", []))
|
|
1929
|
+
if not direct_status.get("has_session_info"):
|
|
1930
|
+
missing_document_parts.append("sessionInfo")
|
|
1931
|
+
if not direct_status.get("has_innertube_api_key"):
|
|
1932
|
+
missing_document_parts.append("INNERTUBE_API_KEY")
|
|
1933
|
+
if missing_document_parts:
|
|
1934
|
+
youtube_accounts_missing_document.append(account_email_snapshot)
|
|
1935
|
+
|
|
1936
|
+
with st.expander(f"{account_label_snapshot} — {account_email_snapshot}", expanded=False):
|
|
1924
1937
|
with st.form(f"batch_account_form_{account_id}"):
|
|
1925
|
-
account_cols = st.columns(
|
|
1938
|
+
account_cols = st.columns(2)
|
|
1926
1939
|
with account_cols[0]:
|
|
1927
|
-
account_label = st.text_input("Nome da conta", value=
|
|
1928
|
-
|
|
1929
|
-
account_email = st.text_input("E-mail/Gmail da conta", value=str(batch_account.get("email", "")), key=f"batch_email_{account_id}")
|
|
1930
|
-
with account_cols[2]:
|
|
1940
|
+
account_label = st.text_input("Nome da conta", value=account_label_snapshot, key=f"batch_label_{account_id}")
|
|
1941
|
+
account_email = st.text_input("E-mail/Gmail da conta", value=account_email_snapshot if account_email_snapshot != "sem e-mail" else "", key=f"batch_email_{account_id}")
|
|
1931
1942
|
account_client_id = st.text_input("OAuth Client ID", value=str(batch_account.get("client_id", "")), key=f"batch_client_id_{account_id}")
|
|
1932
|
-
with account_cols[
|
|
1943
|
+
with account_cols[1]:
|
|
1933
1944
|
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}")
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1945
|
+
account_session_info = st.text_input(
|
|
1946
|
+
"sessionInfo token desta conta Google",
|
|
1947
|
+
value=str(batch_account.get("sessionInfo") or batch_account.get("session_info") or batch_account.get("direct_session_info", "")),
|
|
1948
|
+
type="password",
|
|
1949
|
+
key=f"batch_session_info_{account_id}",
|
|
1950
|
+
help="Token sessionInfo usado pelo Upload directo. É guardado por conta e sincronizado no credentials.json; os cookies e restantes valores continuam exclusivamente no documento.",
|
|
1951
|
+
)
|
|
1941
1952
|
save_account = st.form_submit_button("Guardar dados da conta Google", type="primary", use_container_width=True)
|
|
1942
|
-
|
|
1943
|
-
st.markdown("**
|
|
1944
|
-
st.caption("
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
else:
|
|
1959
|
-
st.warning("Sem documento de credenciais")
|
|
1960
|
-
st.caption("Documento guardado em: storage/youtube_direct_accounts/<id-da-conta>/credentials.json")
|
|
1961
|
-
direct_account_save_buttons[account_id] = st.button("Guardar documento de Upload directo desta conta", type="secondary", use_container_width=True, key=f"save_direct_account_{account_id}")
|
|
1953
|
+
|
|
1954
|
+
st.markdown("**Documento de cookies/credenciais desta conta Google**")
|
|
1955
|
+
st.caption("O documento padrão é criado automaticamente. Suba um JSON completo ou apenas o documento de cookies; os valores preenchidos são incorporados e mantidos em credentials.json.")
|
|
1956
|
+
document_upload = st.file_uploader(
|
|
1957
|
+
"Subir documento de cookies/credenciais",
|
|
1958
|
+
type=["json"],
|
|
1959
|
+
key=f"direct_credentials_document_{account_id}",
|
|
1960
|
+
help="Aceita o JSON do YouTube-Video-Upload-Frontend-Api. Um documento parcial de cookies também é incorporado sem apagar os restantes campos.",
|
|
1961
|
+
)
|
|
1962
|
+
if missing_document_parts:
|
|
1963
|
+
st.warning(f"Documento incompleto: {', '.join(missing_document_parts)}")
|
|
1964
|
+
else:
|
|
1965
|
+
st.success("Documento completo para a conta Google")
|
|
1966
|
+
st.caption(f"Documento guardado em: {direct_status['document_file']}")
|
|
1967
|
+
document_save = st.button("Guardar documento nesta conta", key=f"save_direct_account_{account_id}", use_container_width=True)
|
|
1968
|
+
|
|
1962
1969
|
account_status = youtube_batch_account_status(batch_account, STORAGE)
|
|
1963
|
-
status_cols = st.columns(
|
|
1970
|
+
status_cols = st.columns(3)
|
|
1964
1971
|
with status_cols[0]:
|
|
1965
1972
|
(st.success if account_status.ok else st.warning)(account_status.message)
|
|
1966
1973
|
with status_cols[1]:
|
|
@@ -1977,23 +1984,13 @@ def render_settings():
|
|
|
1977
1984
|
settings["youtube_batch_accounts"] = remaining_accounts
|
|
1978
1985
|
if settings.get("youtube_batch_selected_account_id") == account_id:
|
|
1979
1986
|
settings["youtube_batch_selected_account_id"] = str(remaining_accounts[0].get("id")) if remaining_accounts else ""
|
|
1980
|
-
|
|
1981
|
-
channels_changed = False
|
|
1982
|
-
for channel in channels:
|
|
1987
|
+
for channel in channel_state:
|
|
1983
1988
|
if str(channel.get("google_account_id") or "") == account_id:
|
|
1984
1989
|
channel.update({"google_account_id": "", "google_account_email": ""})
|
|
1985
|
-
|
|
1986
|
-
if channels_changed:
|
|
1987
|
-
write_json("channels.json", channels)
|
|
1990
|
+
write_json("channels.json", channel_state)
|
|
1988
1991
|
write_json("settings.json", settings)
|
|
1989
1992
|
st.rerun()
|
|
1990
|
-
|
|
1991
|
-
st.session_state["new_batch_account_label"] = account_label
|
|
1992
|
-
st.session_state["new_batch_account_email"] = account_email
|
|
1993
|
-
st.session_state["new_batch_account_client_id"] = account_client_id
|
|
1994
|
-
st.session_state["new_batch_account_client_secret"] = account_client_secret
|
|
1995
|
-
st.session_state["new_batch_account_session_info"] = account_session_info
|
|
1996
|
-
st.rerun()
|
|
1993
|
+
|
|
1997
1994
|
if save_account:
|
|
1998
1995
|
if "@" not in account_email.strip():
|
|
1999
1996
|
st.error("Informe um e-mail Google válido.")
|
|
@@ -2007,40 +2004,47 @@ def render_settings():
|
|
|
2007
2004
|
delete_youtube_batch_token(existing, STORAGE)
|
|
2008
2005
|
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()})
|
|
2009
2006
|
update_credentials_document_session_info(STORAGE, existing, account_session_info.strip())
|
|
2007
|
+
ensure_credentials_document(STORAGE, existing, settings, channel_state)
|
|
2010
2008
|
settings["youtube_batch_accounts"] = batch_accounts
|
|
2011
2009
|
write_json("settings.json", settings)
|
|
2012
2010
|
st.success("Conta Google/YouTube guardada.")
|
|
2013
2011
|
st.rerun()
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
if
|
|
2017
|
-
st.error("Seleccione
|
|
2012
|
+
|
|
2013
|
+
if document_save:
|
|
2014
|
+
if document_upload is None:
|
|
2015
|
+
st.error("Seleccione um documento JSON de cookies/credenciais antes de guardar.")
|
|
2018
2016
|
else:
|
|
2019
2017
|
try:
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2018
|
+
merge_credentials_document(
|
|
2019
|
+
STORAGE,
|
|
2020
|
+
batch_account,
|
|
2021
|
+
document_upload.getvalue(),
|
|
2022
|
+
document_upload.name,
|
|
2023
|
+
session_info_override=str(batch_account.get("sessionInfo") or ""),
|
|
2024
|
+
channels=channel_state,
|
|
2025
|
+
)
|
|
2026
|
+
st.success("Documento incorporado e guardado nesta conta Google.")
|
|
2025
2027
|
st.rerun()
|
|
2026
2028
|
except ValueError as exc:
|
|
2027
2029
|
st.error(str(exc))
|
|
2030
|
+
|
|
2031
|
+
if youtube_accounts_missing_document:
|
|
2032
|
+
st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
|
|
2033
|
+
|
|
2034
|
+
st.divider()
|
|
2035
|
+
st.markdown("### Adicionar outra conta Gmail")
|
|
2036
|
+
st.caption("Este formulário fica fora dos cartões das contas existentes. A associação de canais não depende da completude deste documento; ela apenas ficará pendente para Upload directo até os campos serem preenchidos.")
|
|
2028
2037
|
with st.form("add_batch_account_form"):
|
|
2029
|
-
st.
|
|
2030
|
-
st.caption("Estes campos pertencem a este Gmail. O ficheiro será guardado numa pasta exclusiva da conta e nunca será uma configuração global.")
|
|
2031
|
-
add_cols = st.columns(4)
|
|
2038
|
+
add_cols = st.columns(2)
|
|
2032
2039
|
with add_cols[0]:
|
|
2033
2040
|
new_account_label = st.text_input("Nome da nova conta", value="Canais YouTube", key="new_batch_account_label")
|
|
2034
|
-
with add_cols[1]:
|
|
2035
2041
|
new_account_email = st.text_input("E-mail/Gmail", key="new_batch_account_email")
|
|
2036
|
-
with add_cols[2]:
|
|
2037
2042
|
new_account_client_id = st.text_input("OAuth Client ID", key="new_batch_account_client_id")
|
|
2038
|
-
with add_cols[
|
|
2043
|
+
with add_cols[1]:
|
|
2039
2044
|
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
st.
|
|
2043
|
-
add_account = st.form_submit_button("Adicionar conta Google/YouTube com documento de Upload directo", use_container_width=True)
|
|
2045
|
+
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.")
|
|
2046
|
+
new_account_document = st.file_uploader("Documento de cookies/credenciais opcional", type=["json"], key="new_batch_account_credentials_document", help="Pode subir agora um JSON completo ou apenas o documento de cookies. Se não subir, será criado um credentials.json padrão vazio.")
|
|
2047
|
+
add_account = st.form_submit_button("Adicionar conta Google/YouTube", type="primary", use_container_width=True)
|
|
2044
2048
|
if add_account:
|
|
2045
2049
|
document_error = ""
|
|
2046
2050
|
if "@" not in new_account_email.strip():
|
|
@@ -2049,25 +2053,27 @@ def render_settings():
|
|
|
2049
2053
|
st.error("Informe o Client ID e o Client Secret da nova conta.")
|
|
2050
2054
|
elif new_account_document is not None:
|
|
2051
2055
|
try:
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2056
|
+
raw_document = new_account_document.getvalue()
|
|
2057
|
+
json.loads(raw_document.decode("utf-8-sig", errors="replace"))
|
|
2058
|
+
except (UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc:
|
|
2059
|
+
document_error = f"O documento {new_account_document.name} deve ser JSON válido."
|
|
2055
2060
|
st.error(document_error)
|
|
2056
2061
|
if "@" in new_account_email.strip() and new_account_client_id.strip() and new_account_client_secret.strip() and not document_error:
|
|
2057
2062
|
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()}
|
|
2058
|
-
if new_account_document is not None:
|
|
2059
|
-
document = parse_credentials_document(new_account_document.getvalue(), new_account_document.name, session_info_override=new_account_session_info.strip())
|
|
2060
|
-
document["account_id"] = new_account["id"]
|
|
2061
|
-
document["email"] = new_account["email"]
|
|
2062
|
-
save_credentials_document(STORAGE, new_account, document)
|
|
2063
2063
|
batch_accounts.append(new_account)
|
|
2064
2064
|
settings["youtube_batch_accounts"] = batch_accounts
|
|
2065
2065
|
settings["youtube_batch_selected_account_id"] = new_account["id"]
|
|
2066
2066
|
write_json("settings.json", settings)
|
|
2067
|
+
ensure_credentials_document(STORAGE, new_account, settings, channel_state)
|
|
2067
2068
|
if new_account_document is not None:
|
|
2068
|
-
|
|
2069
|
+
try:
|
|
2070
|
+
merge_credentials_document(STORAGE, new_account, new_account_document.getvalue(), new_account_document.name, session_info_override=new_account_session_info.strip(), channels=channel_state)
|
|
2071
|
+
except ValueError as exc:
|
|
2072
|
+
st.error(str(exc))
|
|
2073
|
+
st.stop()
|
|
2074
|
+
st.success(f"Conta {new_account['email']} adicionada e documento incorporado.")
|
|
2069
2075
|
else:
|
|
2070
|
-
st.warning(f"Conta {new_account['email']} adicionada.
|
|
2076
|
+
st.warning(f"Conta {new_account['email']} adicionada. Foi criado um credentials.json padrão; suba o documento de cookies nesta conta quando estiver pronto.")
|
|
2071
2077
|
st.rerun()
|
|
2072
2078
|
|
|
2073
2079
|
with api_keys_tab:
|
|
@@ -2595,8 +2601,8 @@ def main():
|
|
|
2595
2601
|
("Redes Sociais", ":material/share:", "Redes Sociais"),
|
|
2596
2602
|
]
|
|
2597
2603
|
settings_items = [
|
|
2598
|
-
("Canais", ":material/ondemand_video:", "Canais"),
|
|
2599
|
-
("Blueprints", ":material/library_books:", "Blueprints"),
|
|
2604
|
+
("Canais Youtube", ":material/ondemand_video:", "Canais Youtube"),
|
|
2605
|
+
("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
|
|
2600
2606
|
("MCP", ":material/hub:", "MCP"),
|
|
2601
2607
|
("Configurações Técnicas", ":material/settings:", "Configurações Técnicas"),
|
|
2602
2608
|
]
|
|
@@ -2604,6 +2610,9 @@ def main():
|
|
|
2604
2610
|
("Niche Finder Kaggle", ":material/search:", "Niche Finder Kaggle"),
|
|
2605
2611
|
("Niche Finder Apify", ":material/api:", "Niche Finder Apify"),
|
|
2606
2612
|
]
|
|
2613
|
+
automation_items = [
|
|
2614
|
+
("Automação Youtube", ":material/schedule:", "Automação Youtube"),
|
|
2615
|
+
]
|
|
2607
2616
|
top_pages = [
|
|
2608
2617
|
("Início", ":material/home:", "Início"),
|
|
2609
2618
|
("Niche Finder", ":material/search:", "Niche Finder"),
|
|
@@ -2619,10 +2628,13 @@ def main():
|
|
|
2619
2628
|
"Vídeos": "Criação de Vídeos",
|
|
2620
2629
|
"Limpador de metadado": "Limpador de Metadados",
|
|
2621
2630
|
"Niche Finder": "Niche Finder Kaggle",
|
|
2631
|
+
"Automação": "Automação Youtube",
|
|
2632
|
+
"Canais": "Canais Youtube",
|
|
2633
|
+
"Blueprints": "Blueprints Youtube",
|
|
2622
2634
|
"Configurações Técnicas": "Configurações Técnicas",
|
|
2623
2635
|
}
|
|
2624
2636
|
current_page = aliases.get(st.session_state.get("page", "Início"), st.session_state.get("page", "Início"))
|
|
2625
|
-
if current_page not in {item[0] for item in top_pages + pipeline_items + edition_items + models_ai_items + niche_finder_items + settings_items}:
|
|
2637
|
+
if current_page not in {item[0] for item in top_pages + pipeline_items + automation_items + edition_items + models_ai_items + niche_finder_items + settings_items}:
|
|
2626
2638
|
current_page = "Início"
|
|
2627
2639
|
st.session_state["page"] = current_page
|
|
2628
2640
|
|
|
@@ -2642,6 +2654,10 @@ def main():
|
|
|
2642
2654
|
with st.expander("Pipeline", expanded=current_page in {item[0] for item in pipeline_items}, icon=":material/account_tree:"):
|
|
2643
2655
|
for child_target, child_icon, child_label in pipeline_items:
|
|
2644
2656
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
2657
|
+
elif target == "Automação":
|
|
2658
|
+
with st.expander("Automação", expanded=current_page in {item[0] for item in automation_items}, icon=":material/schedule:"):
|
|
2659
|
+
for child_target, child_icon, child_label in automation_items:
|
|
2660
|
+
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
2645
2661
|
elif target == "Edição":
|
|
2646
2662
|
with st.expander("Edição", expanded=current_page in {item[0] for item in edition_items}, icon=":material/edit:"):
|
|
2647
2663
|
for child_target, child_icon, child_label in edition_items:
|
|
@@ -2665,7 +2681,7 @@ def main():
|
|
|
2665
2681
|
"Início": render_dashboard,
|
|
2666
2682
|
"Criação de Vídeos": render_new_video,
|
|
2667
2683
|
"Criação de Músicas": render_music_creation,
|
|
2668
|
-
"Automação": render_automation,
|
|
2684
|
+
"Automação Youtube": render_automation,
|
|
2669
2685
|
"Niche Finder Kaggle": render_niche_finder,
|
|
2670
2686
|
"Niche Finder Apify": render_niche_finder_apify,
|
|
2671
2687
|
"Edição": lambda: render_edit_placeholder("Edição", "Seleccione uma das abas de edição no menu expansível."),
|
|
@@ -2676,8 +2692,8 @@ def main():
|
|
|
2676
2692
|
"Personagens": lambda: render_edit_placeholder("Personagens", "Área reservada para a futura funcionalidade de personagens."),
|
|
2677
2693
|
"Redes Sociais": lambda: render_edit_placeholder("Redes Sociais", "Área reservada para a futura funcionalidade de redes sociais."),
|
|
2678
2694
|
"Upload": render_upload,
|
|
2679
|
-
"Canais": render_channels,
|
|
2680
|
-
"Blueprints": render_blueprints,
|
|
2695
|
+
"Canais Youtube": render_channels,
|
|
2696
|
+
"Blueprints Youtube": render_blueprints,
|
|
2681
2697
|
"MCP": render_mcp,
|
|
2682
2698
|
"Configurações Técnicas": render_settings,
|
|
2683
2699
|
}
|
|
@@ -32,6 +32,13 @@ def cookie_file_path(storage_root: Path, account: dict[str, Any]) -> Path:
|
|
|
32
32
|
return account_directory(storage_root, account) / "cookies.json"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _placeholder_to_empty(value: Any) -> str:
|
|
36
|
+
text = str(value or "").strip()
|
|
37
|
+
if text in {"...", "…", "<preencher>", "<valor>"}:
|
|
38
|
+
return ""
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
|
|
35
42
|
def _normalise_pairs(value: Any) -> dict[str, str]:
|
|
36
43
|
pairs: dict[str, str] = {}
|
|
37
44
|
if isinstance(value, dict):
|
|
@@ -40,13 +47,13 @@ def _normalise_pairs(value: Any) -> dict[str, str]:
|
|
|
40
47
|
for key, item in value.items():
|
|
41
48
|
key_text = str(key).strip()
|
|
42
49
|
if key_text in COOKIE_KEYS:
|
|
43
|
-
pairs[key_text] =
|
|
50
|
+
pairs[key_text] = _placeholder_to_empty(item)
|
|
44
51
|
return pairs
|
|
45
52
|
if isinstance(value, list):
|
|
46
53
|
for item in value:
|
|
47
54
|
if isinstance(item, dict):
|
|
48
55
|
name = str(item.get("name") or item.get("key") or "").strip()
|
|
49
|
-
content =
|
|
56
|
+
content = _placeholder_to_empty(item.get("value"))
|
|
50
57
|
if name in COOKIE_KEYS:
|
|
51
58
|
pairs[name] = content
|
|
52
59
|
return pairs
|
|
@@ -130,15 +137,15 @@ def _normalise_document(raw: Any, account: dict[str, Any]) -> dict[str, Any]:
|
|
|
130
137
|
for key, value in delegated_raw.items():
|
|
131
138
|
if isinstance(value, dict):
|
|
132
139
|
value = value.get("DELEGATED_SESSION_ID") or value.get("delegated_session_id") or ""
|
|
133
|
-
value =
|
|
140
|
+
value = _placeholder_to_empty(value)
|
|
134
141
|
if value:
|
|
135
142
|
delegated[str(key)] = value
|
|
136
143
|
return {
|
|
137
144
|
"account_id": str(raw.get("account_id") or account.get("id") or "").strip(),
|
|
138
145
|
"email": str(raw.get("email") or account.get("email") or "").strip(),
|
|
139
|
-
"sessionInfo":
|
|
146
|
+
"sessionInfo": _placeholder_to_empty(raw.get("sessionInfo") or raw.get("session_info") or raw.get("direct_session_info") or _account_session_info(account)),
|
|
140
147
|
"cookies": {key: cookies.get(key, "") for key in COOKIE_KEYS},
|
|
141
|
-
"INNERTUBE_API_KEY":
|
|
148
|
+
"INNERTUBE_API_KEY": _placeholder_to_empty(raw.get("INNERTUBE_API_KEY") or raw.get("innertube_api_key") or raw.get("direct_innertube_api_key") or ""),
|
|
142
149
|
"chunk_size": _safe_chunk_size(raw.get("chunk_size", raw.get("direct_chunk_size", DEFAULT_CHUNK_SIZE))),
|
|
143
150
|
"delegated_session_ids": delegated,
|
|
144
151
|
}
|
|
@@ -257,6 +264,49 @@ def ensure_credentials_document(storage_root: Path, account: dict[str, Any], set
|
|
|
257
264
|
return credentials_document_path(storage_root, account)
|
|
258
265
|
|
|
259
266
|
|
|
267
|
+
def merge_credentials_document(
|
|
268
|
+
storage_root: Path,
|
|
269
|
+
account: dict[str, Any],
|
|
270
|
+
content: bytes,
|
|
271
|
+
filename: str = DIRECT_DOCUMENT_NAME,
|
|
272
|
+
*,
|
|
273
|
+
session_info_override: str = "",
|
|
274
|
+
channels: list[dict[str, Any]] | None = None,
|
|
275
|
+
) -> Path:
|
|
276
|
+
"""Merge a complete or partial credentials JSON into the account document.
|
|
277
|
+
|
|
278
|
+
The upload may contain only cookies or the full Frontend API document. Existing
|
|
279
|
+
non-empty values are preserved, so uploading cookies never discards sessionInfo,
|
|
280
|
+
INNERTUBE_API_KEY, chunk_size or delegated channel IDs.
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
raw = json.loads(content.decode("utf-8-sig", errors="replace"))
|
|
284
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
285
|
+
raise ValueError(f"O documento de credenciais {filename} deve ser JSON válido.") from exc
|
|
286
|
+
if not isinstance(raw, dict):
|
|
287
|
+
raise ValueError(f"O documento de credenciais {filename} deve conter um objecto JSON.")
|
|
288
|
+
|
|
289
|
+
current = load_credentials_document(storage_root, account, channels=channels, create=True)
|
|
290
|
+
incoming = _normalise_document(raw, {**account, "sessionInfo": session_info_override})
|
|
291
|
+
merged = _normalise_document(current, account)
|
|
292
|
+
for key in COOKIE_KEYS:
|
|
293
|
+
if incoming["cookies"].get(key):
|
|
294
|
+
merged["cookies"][key] = incoming["cookies"][key]
|
|
295
|
+
incoming_session = _placeholder_to_empty(session_info_override) or incoming.get("sessionInfo", "")
|
|
296
|
+
if incoming_session:
|
|
297
|
+
merged["sessionInfo"] = incoming_session
|
|
298
|
+
if incoming.get("INNERTUBE_API_KEY"):
|
|
299
|
+
merged["INNERTUBE_API_KEY"] = incoming["INNERTUBE_API_KEY"]
|
|
300
|
+
if "chunk_size" in raw:
|
|
301
|
+
merged["chunk_size"] = incoming["chunk_size"]
|
|
302
|
+
for key, value in incoming.get("delegated_session_ids", {}).items():
|
|
303
|
+
if value:
|
|
304
|
+
merged["delegated_session_ids"][str(key)] = value
|
|
305
|
+
merged["account_id"] = str(account.get("id") or merged.get("account_id") or "")
|
|
306
|
+
merged["email"] = str(account.get("email") or merged.get("email") or "")
|
|
307
|
+
return save_credentials_document(storage_root, account, merged)
|
|
308
|
+
|
|
309
|
+
|
|
260
310
|
def delegated_session_id(document: dict[str, Any], channel: dict[str, Any]) -> str:
|
|
261
311
|
mapping = document.get("delegated_session_ids", {}) if isinstance(document, dict) else {}
|
|
262
312
|
if not isinstance(mapping, dict):
|
package/package.json
CHANGED