PTENES
Publicador de YouTube · v2.0.0

Un daemon, una base de datos, N canales

Publica clips en YouTube en cualquier cantidad de canales con una cola de trabajos, horarios por canal, reintentos y avisos por Telegram. Sin 35 procesos ni 17 copias del código.

Ilustración de yt-pub-livesx2: un hub central distribuye videos a varios canales
Qué es

La versión 2 del pipeline de lives de INEMA

O yt-pub-livesx corta transmisiones en vivo en clips y los vuelve a publicar en otro canal. Funcionó tan bien que terminó convertido en 17 instancias copiadas, 35 procesos y 18 puertos. La v2 mantiene los scripts que hacen el trabajo y solo cambia la orquestación.

🧩 Un proceso para todos los canales

O hub.py lee canais/*.yaml cada 30 s, programa un job por horario y los ejecuta en orden. Basta con editar el YAML: no hace falta reiniciar.

🔐 Las credenciales se quedan donde están

Cada canal apunta config_dir a la carpeta que ya tiene el OAuth. No se copia ni se migra nada. Los 17 canales de la v1 se incorporan sin volver a autenticarse.

🧾 Cola con estado, no un reloj en una cadena de texto

Trabajos idempotentes por clave, claim atómico en SQLite, reintento ante fallas transitorias y recuperación si el proceso muere durante una carga.

v1 (yt-pub-livesx)v2 (yt-pub-livesx2)
procesos352
puertos TCP181
bases de datos SQLite171
copias del código191
líneas de orquestación11.363965
pruebas automatizadas015
Cómo funciona

Los productores ponen trabajos en la cola y el hub publica

Cualquier cosa que genere un MP4 con título es un productor: la CLI, la carpeta entrada/<canal>/, un workflow de n8n, el cortador de lives. El hub solo se encarga del cuándo y del estado.

MP4 + título→ clips (cola)→ job a la hora del canal→ token mediante config_dir→ carga resumible→ publicado→ Telegram
1

Entrada

./hub publicar, o copiar el archivo a entrada/<canal>/ con un .json opcional al lado (título, descripción, etiquetas, portada).

2

Programación

Para cada canal activo y cada horario del YAML, un trabajo pub:<canal>:<dia HH:MM>. Reiniciar el hub no duplica ni pierde publicaciones.

3

Publicación

O runner es el único que publica. Si falla la red, vuelve a la cola; si se rechaza el OAuth, el error queda visible en el panel y en la CLI.

Requisitos previos

Python, dos bibliotecas y credenciales de YouTube

Sin Redis, Docker ni framework. Probado en Ubuntu con Python 3.12.

Dependencias

cryptography (mismo formato de credenciales que la v1) y PyYAML.

# en la carpeta del proyecto
pip install -r requirements.txt

OAuth de cada canal

Un proyecto de Google Cloud por canal, con .env e credentials.enc. Quien viene de v1 ya lo tiene; quien empieza desde cero usa el script.

GWS_CONFIG_DIR=/caminho/config scripts/yt-auth

Telegram (opcional)

Un bot dedicado para avisar sobre publicaciones. Un único token en la raíz del proyecto.

cp .env.example .env
# TELEGRAM_NOTIFY_BOT_TOKEN=...
Guía de uso · paso a paso

Del clon a la primera publicación

Todos los comandos de abajo existen en el repo y se ejecutaron. Ninguno sube contenido de verdad hasta que lo pidas con --agora.

1

Clonar e instalar

El proyecto usa solo Python y la stdlib, más dos 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

Describir un canal en YAML

Copia el ejemplo y apunta config_dir a la carpeta que tiene el OAuth. Si tienes instancias de la v1, pasa al paso 3, que genera los YAML automáticamente.

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

Importar el historial de la v1 (solo lectura)

Lee el lives.db de cada yt-pub-livesN en modo read-only, crea canais/livesN.yaml con ativo: false y trae los publicados. Puedes volver a ejecutarlo cuando quieras.

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

Revisar canales y credenciales

Un comando muestra qué canal tiene un OAuth válido. Así aparecieron 4 canales de la v1 que fallaban en silencio.

./hub status                # cola, publicados, errores y el último por canal
./hub canais --token        # actualiza el token de cada canal: OK o motivo del fallo
5

Publicar un video

Directamente desde la CLI, en el momento o en la cola. El --dry-run descifra, actualiza el token y prepara los metadatos, pero se detiene antes de subir.

./hub publicar video.mp4 --canal lives8 --titulo "Titulo" --dry-run   # valida todo
./hub publicar video.mp4 --canal lives8 --titulo "Titulo" --agora     # inicia ahora
./hub publicar video.mp4 --canal lives8 --titulo "Titulo"             # cola, sale en el próximo horario
6

O soltarlo en la carpeta de entrada

El mismo efecto sin CLI, para n8n, scripts o alguien que arrastra archivos. Un .json con el mismo nombre, trae el título, la descripción, las etiquetas y la portada.

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

Iniciar el daemon y el panel

Dos servicios de usuario en systemd. Con todos los canales inactivos, el hub solo observa; activa un canal a la vez cambiando ativo: true en el YAML y deteniendo el scheduler v1 correspondiente.

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
# panel: http://localhost:8200
8

Supervisar y corregir

Cola y errores por CLI o desde el panel único. Los clips con error vuelven a la cola con un comando.

./hub fila            # clips pendientes y con error
./hub jobs 20         # últimos jobs ejecutados
./hub retry 12467     # el clip con error vuelve a la cola
python3 tests/test_jobs.py   # 15 pruebas, sin red
Ejemplos

Salida real de los comandos

Capturas de texto de la primera ejecución, en 2026-09-06, frente a las 17 instancias 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 canales OK, 5 con OAuth rechazado o vencido:
# 4 de ellos publicaban "error" a cada hora sin que nadie lo supiera

./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 (generado)

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 cuando se detenga yt-scheduler8
Hoja de ruta

Fase 1 lista, fase 2 diseñada

Límite: el núcleo se mantiene por debajo de 1.500 líneas. Lo que supere ese límite pasa a ser un script llamado por un trabajo.

Hecho
Núcleo de publicaciónDaemon, cola de jobs, CLI, panel único, publisher de YouTube con dry-run, carpeta de entrada, Telegram, importador de v1, 15 pruebas.
Fase 2
Corte de lives y miniatura como jobsLos scripts yt-clip e yt-thumbnail de la v1 ya están copiadas; pasan a ser tipos de job cortar_live e thumb con LIVES_DIR por canal.
Fase 2
Alerta de error en TelegramUn chat de administración recibe los avisos de OAuth rechazado y cargas fallidas. Era lo que faltaba en la v1 para no descubrir por casualidad que 4 canales estaban parados.
Fase 2
TikTok e Instagram mediante un agregadorPublisher con la misma suscripción de YouTube, apuntando a Upload-Post o Blotato. Investigación y comparación en docs/PUBLICADORES.md.
Migración
Canal por canal, sin big bangDetener el yt-schedulerN, cambiar ativo: true, observar. Las carpetas v1 quedan como bóveda de credenciales y de lives en disco.