PTENES
YouTube Publisher Β· v2.0.0

One daemon, one database, N channels

Publishes clips to YouTube across any number of channels, with a job queue, per-channel schedules, retries, and Telegram notifications. No 35 processes, no 17 copies of the code.

Illustration of yt-pub-livesx2: a central hub distributing videos to multiple channels
What it is

INEMA's live pipeline version 2

O yt-pub-livesx cuts livestreams into clips and reposts them to another channel. It worked so well that it grew into 17 copied instances, 35 processes, and 18 ports. V2 keeps the scripts that do the work and changes only the orchestration.

🧩 One process for all channels

O hub.py reads canais/*.yaml every 30 seconds, schedules a job for each time slot and runs them in order. Just edit the YAML: no restart is needed.

πŸ” Credentials stay where they are

Each channel points config_dir to the folder that already has OAuth. Nothing is copied, nothing is migrated. The 17 v1 channels are included without reauthentication.

🧾 Queue with state, not a time string

Idempotent jobs keyed by ID, atomic claims in SQLite, retries for transient failures, and recovery if the process dies during an upload.

v1 (yt-pub-livesx)v2 (yt-pub-livesx2)
processes352
TCP ports181
SQLite databases171
copies of the code191
orchestration lines11.363965
automated tests015
How it works

Producers add items to the queue; the hub publishes them

Anything that generates an MP4 with a title is a producer: the CLI, the folder entrada/<canal>/, an n8n workflow, the live stream cutter. The hub only handles the when and the state.

MP4 + title→ clips (queue)→ job at the channel's scheduled time→ token via config_dir→ resumable upload→ published→ Telegram
1

Input

./hub publicar, or copy the file to entrada/<canal>/ with one .json optional alongside it (title, description, tags, thumbnail).

2

Scheduling

For each active channel and each time slot in the YAML, a job pub:<canal>:<dia HH:MM>. Restarting the hub won't duplicate or lose a post.

3

Publishing

O runner is the only one that publishes. Network failures return to the queue; rejected OAuth credentials become an error visible in the dashboard and CLI.

Prerequisites

Python, two libraries, and YouTube credentials

No Redis, Docker, or framework. Tested on Ubuntu with Python 3.12.

Dependencies

cryptography (same credential format as v1) and PyYAML.

# in the project folder
pip install -r requirements.txt

OAuth for each channel

One Google Cloud project per channel, with .env e credentials.enc. Anyone coming from v1 already has it; anyone starting from scratch uses the script.

GWS_CONFIG_DIR=/caminho/config scripts/yt-auth

Telegram (optional)

A dedicated bot for publication notifications. One token, in the project root.

cp .env.example .env
# TELEGRAM_NOTIFY_BOT_TOKEN=...
User guide Β· step by step

From clone to first post

All the commands below are in the repo and have been run. None uploads anything for real until you ask with --agora.

1

Clone and install

The project uses only Python and the standard library, plus two libraries.

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

Describe a channel in YAML

Copy the example and point config_dir to the folder with OAuth. If you have v1 instances, skip to step 3, which generates the YAML files automatically.

cp canais/_exemplo.yaml canais/meucanal.yaml
# destination_name, config_dir, schedules: ["08:10","16:10"], privacy, telegram_chat_ids, active
3

Import v1 history (read-only)

Reads the lives.db of each yt-pub-livesN in read-only mode, creates canais/livesN.yaml with ativo: false and fetches the published videos. You can run it again whenever you want.

scripts/importar-historico --todos  # 17 instances, 12,466 posts imported
4

Check channels and credentials

One command shows which channels have valid OAuth. That’s how 4 v1 channels failing silently came to light.

./hub status                # queue, published, errors, and latest per channel
./hub canais --token        # refreshes each channel's token: OK or reason for failure
5

Publish a video

Directly through the CLI, immediately or in the queue. The --dry-run decrypts, refreshes the token, and builds the metadata, but stops before upload.

./hub publicar video.mp4 --canal lives8 --titulo "Titulo" --dry-run   # validates everything
./hub publicar video.mp4 --canal lives8 --titulo "Titulo" --agora     # start now
./hub publicar video.mp4 --canal lives8 --titulo "Titulo"             # queue, runs at the next scheduled time
6

Or drop it into the input folder

Same functionality without the CLI, for n8n, scripts, or someone dragging and dropping files. A .json with the same name brings over the title, description, tags, and thumbnail.

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

Start the daemon and dashboard

Two systemd user services. With all channels inactive, the hub only monitors; activate one channel at a time by changing ativo: true in the YAML and stopping the corresponding v1 scheduler.

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

Monitor and fix

Queue and errors through the CLI or the unified dashboard. A clip with an error goes back in the queue with one command.

./hub fila            # pending and errored clips
./hub jobs 20         # latest jobs run
./hub retry 12467     # clip with an error goes back in the queue
python3 tests/test_jobs.py   # 15 tests, no network
Examples

Actual command output

Text captures from the first run, on 2026-09-06, compared with the 17 v1 instances.

./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 channels --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 channels OK, 5 with OAuth denied or expired:
# 4 of them were publishing "error" on every scheduled run without anyone knowing

./hub publish --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 (generated)

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 when yt-scheduler8 stops
Roadmap

Phase 1 ready, phase 2 designed

Limit: the core stays under 1,500 lines. Anything beyond that becomes a script called by a job.

Done
Publishing coreDaemon, job queue, CLI, unified dashboard, YouTube publisher with dry run, input folder, Telegram, v1 importer, 15 tests.
Phase 2
Live stream cutting and thumbnails as jobsThe scripts yt-clip e yt-thumbnail from v1 have already been copied; they become job types cortar_live e thumb with LIVES_DIR per channel.
Phase 2
Telegram error alertAn admin chat receives notifications about rejected OAuth and failed uploads. That was what v1 lacked, leading to the accidental discovery of 4 stalled channels.
Phase 2
TikTok and Instagram through an aggregatorA publisher with the same YouTube API signature, pointed at Upload-Post or Blotato. Research and comparison at docs/PUBLICADORES.md.
Migration
Channel by channel, without a big bangStop the yt-schedulerN, change ativo: true, monitor it. The v1 folders remain as a vault for credentials and live streams on disk.