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