Просто песня: бот, который приносит YouTube прямо в Telegram
Ещё один пет-проект: кидаешь боту ссылку на YouTube — получаешь обратно видео файлом. Внутри yt-dlp, ffmpeg и serverless-контейнер, который спит, пока никто ничего не качает.
Иногда мне нужно видео с YouTube именно файлом. Ссылка не годится: её нельзя вставить в презентацию, отправить туда, где нет интернета, или просто оставить себе. А ещё ролики с YouTube пропадают: автор удалил, канал забанили, правообладатель пожаловался. Если видео чем-то дорого, надёжнее держать копию у себя.
Раньше я для этого шёл на очередной сайт-качалку с тремя попапами и кнопкой «Download», которая на самом деле реклама. Надоело, и я сделал бота: кидаешь ему ссылку — получаешь видео файлом прямо в Telegram.
Зовут его @youtwoybot, в Telegram он представляется как «Просто песня».
Что он умеет
Отправляешь ему ссылку на YouTube — обычное видео, шортс, запись трансляции или короткую ссылку youtu.be. Бот отвечает «Скачиваю видео…», через некоторое время статус меняется на «Отправляю…», и в чат прилетает видео с названием, обложкой и правильной длительностью. Его можно смотреть прямо в Telegram, со стримингом, не дожидаясь полной загрузки.
Ограничение одно, и оно не моё: Bot API не даёт ботам отправлять файлы больше 50 МБ. Поэтому бот сам подбирает качество так, чтобы влезть в лимит, а если ролик слишком длинный и не влезает даже в минимальном качестве — честно пишет об этом, а не падает молча.
Стек
Бот написан на Python 3.12, и весь список зависимостей — четыре строчки:
aiogram— фреймворк для Telegram. Работает через вебхук, не через поллинг: бот живёт в облаке и не может позволить себе висеть постоянным процессом.FastAPI+uvicorn— принимают этот вебхук. Всё приложение — это два эндпоинта:GET /для проверки здоровья иPOST /webhookдля апдейтов от Telegram.yt-dlp— скачивает видео с YouTube.ffmpeg— склеивает видео с аудио и нарезает обложки. Он не в requirements, ставится в Docker-образ через apt, но по факту это половина бота.
Хостится всё это в Yandex Cloud на Serverless Container: контейнер просыпается, когда Telegram присылает сообщение, и засыпает обратно. Когда никто ничего не качает, он масштабируется в ноль и не стоит ничего. Та же логика, что и с моим ботом для учёта расходов: пет-проект работает минуты в день, платить за постоянно включённый сервер под него жалко.
Инфраструктура описана в Terraform, токен бота и секрет вебхука лежат в Lockbox и попадают в контейнер только при деплое. В самом репозитории секретов нет.
Главная засада: чёрный экран
Самое интересное в этом боте — не скачивание, а один неочевидный костыль.
Если попросить yt-dlp «дай лучшее качество в mp4», он часто принесёт видео в кодеке AV1 или VP9. Файл валидный, ffmpeg его понимает, компьютер играет. А вот Telegram на части устройств показывает чёрный экран со звуком. Видео вроде есть, а смотреть нечего.
Лечится это тем, что бот принудительно просит у YouTube кодек H.264 — его декодируют все клиенты Telegram, даже старые телефоны. Селектор форматов у yt-dlp выглядит так:
_FORMAT = (
f"bestvideo[vcodec^=avc1][height<=720][filesize<{_TARGET_BYTES}]+bestaudio[ext=m4a]"
f"/best[vcodec^=avc1][ext=mp4][filesize<{_TARGET_BYTES}]"
f"/best[ext=mp4][height<=480]"
f"/best[height<=480]/best"
)
Читается сверху вниз: сначала пробуем H.264 до 720p с аудио в m4a, не вышло — готовый mp4 в H.264, дальше просто mp4 до 480p, и в самом конце — хоть что-нибудь. Каждая следующая строчка хуже предыдущей, но лучше, чем ответ «не получилось».
Отдельная деталь: целевой размер здесь не 50 МБ, а 48. Два мегабайта — запас на контейнер mp4 и неточность муксера. Без него склеенный файл иногда вылезал за лимит буквально на сотни килобайт, и Telegram его отбрасывал уже после того, как вся работа проделана.
Обложка из середины ролика
Ещё одна мелочь, которую замечаешь только когда она сделана плохо. Если нарезать превью из первого кадра, у половины роликов оно получается чёрным: YouTube-видео любят начинаться с затемнения. В чате такое выглядит как битый файл.
Поэтому бот спрашивает у yt-dlp длительность ролика, перематывает ffmpeg на середину и берёт кадр оттуда. Масштабирует до ширины 320 с чётной высотой — JPEG-энкодеру ffmpeg нечётная высота не нравится. Мелочь, а видео в чате сразу выглядит как видео, а не как чёрный прямоугольник.
Вебхук, который всегда отвечает «ок»
С вебхуками у Telegram есть особенность: если эндпоинт ответил ошибкой, Telegram будет присылать тот же апдейт снова и снова. Уронил обработку одного кривого сообщения — получил бесконечный цикл ретраев.
Поэтому обработка апдейта обёрнута так, что любая ошибка логируется, но наружу всё равно уходит {"ok": true}. Пользователь получит сообщение об ошибке от самого бота, а Telegram — свой успешный статус и повод забыть про этот апдейт.
Сам эндпоинт при этом не публичная дверь для всех желающих. При установке вебхука Telegram получает секрет и присылает его в заголовке с каждым запросом, а бот сверяет его через secrets.compare_digest и отвечает 401 всем остальным. Контейнер технически может дёрнуть кто угодно — URL-то публичный, иначе Telegram до него не достучится, — но без секрета это бессмысленно.
И последнее: yt-dlp и ffmpeg — блокирующие штуки, а бот асинхронный. Скачивание уезжает в отдельный поток через asyncio.to_thread, чтобы событийный цикл не вставал колом, пока качается очередной шортс. Контейнеру разрешено обрабатывать четыре запроса параллельно, так что несколько человек могут пользоваться ботом одновременно и не мешать друг другу.
Деплой без рук
Пайплайн в GitHub Actions собирает Docker-образ, пушит его в реестр и выкатывает новую ревизию контейнера. Дальше смоук-тест: до двадцати попыток достучаться до health-check с интервалом в три секунды. Если контейнер так и не ответил — автоматический откат на предыдущую ревизию. Сломанный деплой откатывается сам, без меня.
Отдельное удовольствие я получил от IAM-ролей Yandex Cloud. Чтобы задеплоить serverless-контейнер, сервисному аккаунту нужна роль functions.editor — да, контейнеры внутри считаются функциями. Чтобы передать контейнеру секрет из Lockbox, недостаточно права читать сам секрет: CLI при деплое проверяет его метаданные, и без lockbox.viewer падает с «Permission denied». А чтобы аккаунт мог запустить контейнер от своего же имени, ему нужно право пользоваться самим собой. Каждую из этих ролей я нашёл не в документации, а по тексту очередной ошибки деплоя.
Итог
По коду это маленький проект: четыре модуля на Python, которые умещаются в пару экранов. Но почти каждое решение в нём — ответ на конкретные грабли: чёрный экран вместо видео, чёрная обложка вместо превью, файл на 50,3 МБ, бесконечные ретраи вебхука. По отдельности каждая проблема — ерунда, но пока их все не соберёшь, бот работает «почти всегда», а это раздражает сильнее, чем «никогда».
Попробовать можно здесь: @youtwoybot. Код целиком лежит на GitHub: