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

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

Хочу поделиться опытом проектирования и реализации 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. Текст с исходниками может различаться, так как я буду дорабатывать и рефакторить проект

Комментарии (3)


  1. JRTJITEADER
    14.06.2025 21:05

    звучит интересно, как раз то, что надо мне для охватов


  1. losander
    14.06.2025 21:05

    Тоже была задача скачивать mp3 с YouTube, мне больше понравился cobalt так как не нужно использовать ffmpeg для переформатирования. Не знаю конечно как с разными форматами видео, но на mp3 он мне выдавал сразу ссылку, а скачивать можно асинхронно.


  1. Comresult
    14.06.2025 21:05

    На каком языке написан код?