
Здравствуйте, коллеги!
Хочу поделиться опытом проектирования и реализации 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 делают пайплайн "огнеупорным": ни одно видео не будет скачано дважды, бот не падает даже при сбоях сети или рестарте контейнера.
Вопросы и обсуждение
Если у вас остались вопросы по реализации, архитектуре или вы хотите добавить свои идеи — пишите в комментариях!
PR и звездочки на GitHub всегда приветствуются.
Спасибо за внимание и увлекательной работы с Go Lang!
P.S. Текст с исходниками может различаться, так как я буду дорабатывать и рефакторить проект
Комментарии (3)
 - losander14.06.2025 21:05- Тоже была задача скачивать mp3 с YouTube, мне больше понравился cobalt так как не нужно использовать ffmpeg для переформатирования. Не знаю конечно как с разными форматами видео, но на mp3 он мне выдавал сразу ссылку, а скачивать можно асинхронно. 
 
           
 
JRTJITEADER
звучит интересно, как раз то, что надо мне для охватов