
Здравствуйте, коллеги!
Хочу поделиться опытом проектирования и реализации 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)
losander
14.06.2025 21:05Тоже была задача скачивать mp3 с YouTube, мне больше понравился cobalt так как не нужно использовать ffmpeg для переформатирования. Не знаю конечно как с разными форматами видео, но на mp3 он мне выдавал сразу ссылку, а скачивать можно асинхронно.
JRTJITEADER
звучит интересно, как раз то, что надо мне для охватов