PTENES
Publicador YouTube · v2.0.0

Um daemon, um banco, N canais

Publica clips no YouTube em qualquer número de canais com fila de jobs, horários por canal, retry e aviso no Telegram. Sem 35 processos, sem 17 cópias do código.

Ilustração do yt-pub-livesx2: um hub central distribuindo vídeos para vários canais
O que é

A versão 2 do pipeline de lives do INEMA

O yt-pub-livesx corta lives em clips e republica em outro canal. Funcionou tão bem que virou 17 instâncias copiadas, 35 processos e 18 portas. A v2 mantém os scripts que fazem o trabalho e troca só a orquestração.

🧩 Um processo para todos os canais

O hub.py lê canais/*.yaml a cada 30 s, agenda um job por horário e executa em ordem. Editar o YAML basta: não há restart.

🔐 Credenciais ficam onde estão

Cada canal aponta config_dir para a pasta que já tem o OAuth. Nada é copiado, nada é migrado. Os 17 canais do v1 entram sem reautenticar.

🧾 Fila com estado, não relógio em string

Jobs idempotentes por chave, claim atômico em SQLite, retry para falha transitória e recuperação se o processo morrer no meio de um upload.

v1 (yt-pub-livesx)v2 (yt-pub-livesx2)
processos352
portas TCP181
bancos SQLite171
cópias do código191
linhas de orquestração11.363965
testes automatizados015
Como funciona

Produtores põem na fila, o hub publica

Qualquer coisa que gere um MP4 com título é um produtor: a CLI, a pasta entrada/<canal>/, um workflow n8n, o cortador de lives. O hub só cuida do quando e do estado.

MP4 + título→ clips (fila)→ job no horário do canal→ token via config_dir→ upload resumable→ publicado→ Telegram
1

Entrada

./hub publicar, ou copiar o arquivo para entrada/<canal>/ com um .json opcional ao lado (título, descrição, tags, capa).

2

Agendamento

Para cada canal ativo e cada horário do YAML, um job pub:<canal>:<dia HH:MM>. Reiniciar o hub não dobra nem perde publicação.

3

Publicação

O runner é o único que publica. Falha de rede volta para a fila; OAuth recusado vira erro visível no painel e na CLI.

Pré-requisitos

Python, duas libs e credenciais do YouTube

Nada de Redis, Docker ou framework. Testado em Ubuntu com Python 3.12.

Dependências

cryptography (mesmo formato de credencial do v1) e PyYAML.

# na pasta do projeto
pip install -r requirements.txt

OAuth de cada canal

Um projeto Google Cloud por canal, com .env e credentials.enc. Quem vem do v1 já tem; quem começa do zero usa o script.

GWS_CONFIG_DIR=/caminho/config scripts/yt-auth

Telegram (opcional)

Um bot dedicado para avisar publicações. Token único, na raiz do projeto.

cp .env.example .env
# TELEGRAM_NOTIFY_BOT_TOKEN=...
Guia de uso · passo a passo

Do clone à primeira publicação

Todos os comandos abaixo existem no repo e foram executados. Nenhum faz upload real até você pedir com --agora.

1

Clonar e instalar

O projeto é só Python e stdlib, mais duas bibliotecas.

git clone https://github.com/inematds/yt-pub-livesx2 ~/projetos/yt-pub-livesx2
cd ~/projetos/yt-pub-livesx2 && pip install -r requirements.txt
2

Descrever um canal em YAML

Copie o exemplo e aponte config_dir para a pasta que tem o OAuth. Quem tem instâncias v1 pula para o passo 3, que gera os YAML sozinho.

cp canais/_exemplo.yaml canais/meucanal.yaml
# destino_nome, config_dir, horarios: ["08:10","16:10"], privacy, telegram_chat_ids, ativo
3

Importar o histórico do v1 (somente leitura)

Lê o lives.db de cada yt-pub-livesN em modo read-only, cria canais/livesN.yaml com ativo: false e traz os publicados. Pode rodar de novo quando quiser.

scripts/importar-historico --todos  # 17 instancias, 12.466 publicacoes importadas
4

Conferir canais e credenciais

Um comando mostra qual canal tem OAuth válido. Foi assim que apareceram 4 canais do v1 falhando em silêncio.

./hub status                # fila, publicados, erros e ultimo por canal
./hub canais --token        # refresca o token de cada canal: OK ou motivo da falha
5

Publicar um vídeo

Direto pela CLI, na hora ou na fila. O --dry-run decripta, refresca o token e monta a metadata, mas para antes do upload.

./hub publicar video.mp4 --canal lives8 --titulo "Titulo" --dry-run   # valida tudo
./hub publicar video.mp4 --canal lives8 --titulo "Titulo" --agora     # sobe agora
./hub publicar video.mp4 --canal lives8 --titulo "Titulo"             # fila, sai no proximo horario
6

Ou soltar na pasta de entrada

Mesmo efeito sem CLI, para n8n, scripts ou uma pessoa arrastando arquivos. Um .json com o mesmo nome traz título, descrição, tags e capa.

cp video.mp4 video.json entrada/lives8/
# video.json: {"titulo":"...","descricao":"...","tags":["a","b"],"privacy":"public"}
7

Subir o daemon e o painel

Dois serviços de usuário no systemd. Com todos os canais inativos o hub só observa; ative um canal por vez trocando ativo: true no YAML e parando o scheduler v1 correspondente.

cp systemd/yt-hub.service systemd/yt-hub-dashboard.service ~/.config/systemd/user/
systemctl --user daemon-reload && systemctl --user enable --now yt-hub yt-hub-dashboard
# painel: http://localhost:8200
8

Acompanhar e corrigir

Fila e erros pela CLI ou pelo painel único. Clip com erro volta para a fila com um comando.

./hub fila            # clips pendentes e com erro
./hub jobs 20         # ultimos jobs executados
./hub retry 12467     # clip com erro volta pra fila
python3 tests/test_jobs.py   # 15 testes, sem rede
Exemplos

Saída real dos comandos

Capturas de texto da primeira execução, em 2026-09-06, contra as 17 instâncias v1.

./hub status

canal     destino        fila  public. erros  ultimo
lives1    INEMA TDS         0     2413     0  2026-09-04 17:15
lives6    INEMA Robot       0     1110     0  2026-09-06 17:40
lives8    Inema Vibe        0     1043     0  2026-09-06 16:10
lives11   INEMA Futuros     0      738     0  2026-09-06 21:40
...
17 canais | fila 0 | publicados 12466 | erros 0 | hub v2.0.0

./hub canais --token

lives1   OK creds  horarios=08:15,10:15,...
         token: FALHOU — refresh_token recusado (400)
lives8   OK creds  horarios=08:10,16:10
         token: OK
...
# 12 canais OK, 5 com OAuth recusado ou expirado:
# 4 deles publicavam "erro" a cada horario sem ninguem saber

./hub publicar --dry-run

clip #12467 na fila de lives8
  [lives8] publicando #12467: Teste dry-run hub v2
  [lives8]     token: refresh via .../yt-pub-lives8/config
  [lives8]     metadata: 'Teste dry-run hub v2' privacy=public
  [lives8]     DRY RUN — parando antes do upload
dry-run: ok

canais/lives8.yaml (gerado)

destino_id: UCJxL11aomXJiBZga-JXylgQ
destino_nome: Inema Vibe
config_dir: /home/.../yt-pub-lives8/config
horarios: ["08:10", "16:10"]
privacy: public
telegram_chat_ids: ["7852..."]
ativo: false   # true quando parar o yt-scheduler8
Roadmap

Fase 1 pronta, fase 2 desenhada

Teto: o núcleo fica abaixo de 1.500 linhas. O que passar disso vira script chamado por um job.

Feito
Núcleo de publicaçãoDaemon, fila de jobs, CLI, painel único, publisher YouTube com dry-run, pasta de entrada, Telegram, importador do v1, 15 testes.
Fase 2
Corte de lives e thumbnail como jobsOs scripts yt-clip e yt-thumbnail do v1 já estão copiados; viram tipos de job cortar_live e thumb com LIVES_DIR por canal.
Fase 2
Alerta de erro no TelegramUm chat de admin recebe OAuth recusado e upload com falha. É o que faltou no v1 para não descobrir 4 canais parados por acaso.
Fase 2
TikTok e Instagram por agregadorPublisher com a mesma assinatura do YouTube apontando para Upload-Post ou Blotato. Pesquisa e comparação em docs/PUBLICADORES.md.
Migração
Canal a canal, sem big bangParar o yt-schedulerN, trocar ativo: true, observar. As pastas v1 ficam como cofre de credenciais e de lives em disco.