Автоматизация контента: как я сделал умного Tg-бота для публикации видео из VK (очередь, дедупликация, NutsDB, yt-dlp)

от автора

Маскот проекта, стала уже традицией

Маскот проекта, стала уже традицией

Здравствуйте, коллеги!

Хочу поделиться опытом проектирования и реализации production-ready Telegram-бота, который автоматически собирает и публикует свежий видеоконтент из паблика ВКонтакте — и делает это без дублей, с гарантией доставки и мемными подписями на базе OpenAI. В статье я покажу архитектуру, приведу примеры кода и расскажу о фишках, таких как очередь ссылок на видео (NutsDB), проверка на уникальность (deduplication), скачивание через yt-dlp и интеграция с OpenAI для генерации описаний.

Код проекта — github.com/digkill/posterAndGrabberBot

TL;DR

  • Go

  • Получаем посты VK, ищем видео, кладём URL в очередь (NutsDB) — только если такого видео не было ранее

  • Отдельная горутина скачивает видео с помощью yt-dlp

  • После скачивания ссылка помечается как обработанная (deduplication)

  • Изображения/видео публикуются в Telegram с подписью от OpenAI

Архитектура решения

Рассмотрим общую схему:

VK API --> Fetcher (ищет видео) --> NutsDB (pending queue)    |                                      |    |------------------------------------> goroutine (yt-dlp downloader)    |                                          |    |                                 (media directory)    |                                          |    |--> Poster (OpenAI captions) ------------> Telegram API
  • Fetcher: Парсит новые посты ВК, находит видео, проверяет — было ли скачано.

  • NutsDB: Лёгкая embedded-база для очереди URL и хранения processed-маркеров.

  • Видео-воркер: По очереди скачивает новые видео yt-dlp, исключая дубли.

  • Poster: Постит фото/видео в Telegram-канал с подписью через OpenAI.

Конфиг и запуск

Проект максимально простой для интеграции:

telegram_bot_token     = "ваш_токен_бота" telegram_channel_id    = "ваш_ид_канала_для_постов" vk_token               = "ваш_VK_API_токен" fetch_interval         = "10m" notification_interval  = "1m" openai_key             = "sk-..." openai_model           = "gpt-4o" openai_prompt          = "Make a meme caption for the image" images_directory       = "./media"

Установка зависимостей:

go mod tidy

Запуск:

go run ./cmd/posterAndGrabberBot/main.go

Модульная архитектура

1. Fetcher: сбор новых постов

Fetcher — воркер, который опрашивает VK API, анализирует новые посты, извлекает url видео и кладёт их в очередь только если это уникальный URL.

Основной фрагмент:

func (f *Fetcher) Fetch(ctx context.Context) error {     ...     for _, attach := range post.Attachments {         if attach.Type == "video" && attach.Video != nil {             videoUrl := fmt.Sprintf("https://vk.com/video%d_%d", attach.Video.OwnerID, attach.Video.ID)             // Дедупликация: кладём только если не скачано             if !nutsdb.IsVideoURLProcessed(videoUrl) {                 nutsdb.SaveVideoLink(videoUrl)             }         }     }     ... } 

2. NutsDB: очередь и проверка уникальности

NutsDB — быстрая embedded key-value база.
Мы храним:

  • pending (list): url-ы для скачивания

  • processed (set по url): хэши обработанных url

Проверка и пометка:

import (     "crypto/md5"     "encoding/hex" )  func urlKey(url string) []byte {     h := md5.Sum([]byte(url))     return []byte("processed_" + hex.EncodeToString(h[:])) }  func (n *NutsDB) IsVideoURLProcessed(url string) bool {     found := false     n.db.View(func(tx *nutsdb.Tx) error {         ds := nutsdb.DataStructureBPTree         bucket := "videos"         key := urlKey(url)         if tx.Has(bucket, key) == nil {             found = true         }         return nil     })     return found }  func (n *NutsDB) MarkVideoURLProcessed(url string) error {     return n.db.Update(func(tx *nutsdb.Tx) error {         ds := nutsdb.DataStructureBPTree         bucket := "videos"         key := urlKey(url)         return tx.Put(bucket, key, []byte{1})     }) } 

3. Горутина-скачиватель через yt-dlp

В отдельной горутине мы периодически достаём URL из pending, скачиваем видео и помечаем url как обработанный.

func StartVideoDownloader(ctx context.Context, n *NutsDB) {     ticker := time.NewTicker(1 * time.Minute)     defer ticker.Stop()     for {         select {         case <-ctx.Done():             return         case <-ticker.C:             links, _ := n.GetAllPendingVideoLinks()             for _, url := range links {                 if err := DownloadVKVideo(url); err == nil {                     n.MarkVideoURLProcessed(url)                     n.RemoveVideoLink(url)                 }             }         }     } }  // yt-dlp shell exec: func DownloadVKVideo(videoUrl string) error {     cmd := exec.Command("yt-dlp", "-P", "./media", videoUrl)     var outBuf, errBuf bytes.Buffer     cmd.Stdout = &outBuf     cmd.Stderr = &errBuf     err := cmd.Run()     if err != nil {         return fmt.Errorf("yt-dlp error: %w\nSTDOUT:\n%s\nSTDERR:\n%s", err, outBuf.String(), errBuf.String())     }     return nil } 

4. Poster: публикация в Telegram с AI-подписью

Poster — воркер, который берёт рандомный файл из папки, генерирует подпись с помощью OpenAI и публикует в канал:

func (p *Poster) processAndSendImage(imgPath string) error {     ...     imgBase64, _ := helpers.EncodeImageToBase64(data, ext)     caption, _ := p.openai.SetCaption("картинка мем", imgBase64)      photoMsg := tgbotapi.NewPhoto(p.channelID, tgbotapi.FileReader{Name: file.Name(), Reader: file})     photoMsg.Caption = caption      _, err := p.bot.Send(photoMsg)     return err } 

Видео также публикуются — при необходимости генерируется thumbnail через ffmpeg.

Полезные нюансы и best practices

  • Очередность: Горутиной-скачивателем нельзя запускать параллельно несколько процессов yt-dlp с одним файлом — держите очередь строгой.

  • База: Никогда не закрывайте NutsDB до завершения всех воркеров!

  • Дедупликация: Использование хэша URL позволяет обрабатывать любые (даже очень длинные) ссылки быстро и с O(1) lookup.

  • yt-dlp: Не забудьте установить yt-dlp и ffmpeg на сервер/контейнер.

  • Error handling: Логируйте не только ошибки yt-dlp, но и все проблемы с файловой системой (например, нехватка места).

  • Производительность: NutsDB отлично тянет сотни тысяч записей, но если видео становится очень много — периодически чистите старые processed ключи (например, по дате).

Итоги и выводы

Данная архитектура прекрасно масштабируется на разные типы контента и любые соцсети.
Вместо VK можно добавить любой источник (YouTube, TikTok, Reddit) — просто реализуйте новый Source и интегрируйте с той же очередью.
Дедупликация и очередь на NutsDB делают пайплайн «огнеупорным»: ни одно видео не будет скачано дважды, бот не падает даже при сбоях сети или рестарте контейнера.

Проект: github.com/digkill/posterAndGrabberBot

Вопросы и обсуждение

Если у вас остались вопросы по реализации, архитектуре или вы хотите добавить свои идеи — пишите в комментариях!
PR и звездочки на GitHub всегда приветствуются.

Спасибо за внимание и увлекательной работы с Go Lang!

P.S. Текст с исходниками может различаться, так как я буду дорабатывать и рефакторить проект


ссылка на оригинал статьи https://habr.com/ru/articles/918186/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *