tg-media-bot: кидаешь ссылку — получаешь файл
Self-hosted Telegram-бот, который качает медиа с 1000+ сайтов через yt-dlp и присылает файл обратно — MP3 с тегами, видео для inline-просмотра, загрузки до 2 ГБ, живой прогресс-бар. Плюс военные истории: три бага в коде, который выглядел готовым, загрузка, которую нельзя измерить, и cookies.txt, ломающий тот самый сайт, которому должен был помогать.
tg-mpv-bot играет ссылки на моём телевизоре. Но не реже мне нужно не посмотреть что-то на большом экране, а получить сам файл: трек на телефон, клип в офлайн, сет с SoundCloud как MP3 с тегами. Так что у него появился брат. Он крутится на том же старом ThinkPad из статьи про домашний сервер в гостиной.
tg-media-bot — небольшой self-hosted Telegram-бот: кидаешь ему ссылку (с любого из 1000+ сайтов, которые поддерживает yt-dlp) — и он скачивает медиа и присылает файл обратно в чат. Чистая утилита. Без AI, без аккаунтов, без трекинга; крутится на твоей машине и устанавливает только исходящие соединения.
Этот пост — о том, что он умеет, и (что полезнее) о том, что ломалось по дороге.
Что он умеет
- Кидаешь ссылку (до трёх за сообщение) — и он качает в текущем режиме и присылает файл. YouTube, SoundCloud, TikTok, Instagram, Reddit, Twitch, Vimeo, X — всё, до чего дотягивается yt-dlp.
/audioотдаёт нормальный MP3 с тегами — вшитая обложка, превью в плеере, название/исполнитель/длительность. SoundCloud всегда аудио, без команды./video— режим по умолчанию; не-MP4 источники перекодируются в стримящийся H.264/AAC, чтобы Telegram играл их inline./formats <url>показывает кнопки выбора качества — Best / 1080p / 720p / 480p / Audio.- Загрузки до 2 ГБ через встроенный локальный Telegram Bot API server (вместо стандартных 50 МБ), с живым прогресс-баром во время скачивания.
- Мгновенные повторы: просишь ссылку, которую уже кто-то качал, — и она прилетает за секунду, прямо из кэша Telegram.
- Доступ по allowlist (работает и в личке, и в группах, которые активировал
разрешённый пользователь), пер-юзер рейт-лимит, реально работающий глобальный
лимит параллельных загрузок, настоящий
/cancelи автоочистка временных файлов.
Внутри — Python + aiogram поверх yt-dlp, юнит-тесты на
pytest в CI, мультиарх Docker-образ и пакет в AUR.
Военные истории
1. Три бага в коде, который выглядел готовым
Скелет существовал ещё до того, как я взялся всерьёз, и выглядел законченным — типизированный, задокументированный, с тестами. Три вещи тихо врали:
- Глобальный лимит параллельности не делал ничего. Был семафор под
MAX_PARALLEL_DOWNLOADS… который никто никогда не захватывал. Реальный путь запускал каждую загрузку сразу. Лимит был украшением. /cancelничего не отменял. Он переставлял статус задачи вcancelled— а процесс yt-dlp как ни в чём не бывало продолжал качать и всё равно загружал результат. Отмена была ярлыком, а не действием.DOWNLOAD_TIMEOUTнигде не применялся. Считывался из env, задокументирован в README, не подключён ни к чему. Зависшая загрузка висела вечно.
Ни один из них не падает с ошибкой. Ни один не валит тест, который не написан.
Это самый опасный тип багов: код, который читается как правильный. Фиксы —
реально захватывать слот в раннере, держать ссылку на asyncio-задачу, чтобы
отмена могла .cancel() её и убить процесс, обернуть подпроцесс в wait_for —
оказались крошечными. Чтобы их найти, надо было не верить, что фича с ручкой в
конфиге и докстрингом действительно существует.
2. Загрузка, которую нельзя измерить
Прогресс-бар просили, и со скачиванием всё просто: запускаешь yt-dlp с
--newline --progress-template, читаешь stdout построчно, троттлишь правки,
чтобы Telegram не зарейтлимитил, рисуешь █░-бар. Готово.
А вот загрузка — стена. У Bot API нет колбэка прогресса загрузки — а с локальным Bot API server ещё хуже: бот отдаёт файл серверу на той же машине (быстро), а тот уже грузит в дата-центры Telegram полностью вне зоны видимости. Считать байты неоткуда. (Мой tg-mpv-bot умеет показывать % загрузки только потому, что говорит на сыром MTProto через другую библиотеку; Bot API этого просто не даёт.)
Так что я перестал притворяться. Статус теперь идёт честными фазами: 📥 Скачивание (настоящий бар) → 🔄 Обработка (этап склейки/перекодировки, который иначе залипает на запутывающих 100%) → 📤 Загрузка с хартбитом — прошедшее время + размер. Не фальшивый процент, а сигнал «живой, работаю», что как раз и нужно знать пользователю.
3. cookies.txt, ломающий сайт, которому должен был помогать
Чтобы качать закрытый контент (Instagram, видео с возрастным ограничением), бот
умеет использовать cookies.txt. Я положил его — и YouTube тут же начал падать:
ERROR: [youtube] …: Requested format is not availableТот же ролик прекрасно качался без куки. Воспроизведение всё прояснило: с куками yt-dlp аутентифицируется как залогиненная сессия, и YouTube отдаёт ей ответ «tv downgraded» player с набором форматов, который мой селектор не мог удовлетворить, — иногда вообще без пригодных форматов. Куки, предназначенные для Instagram, травили YouTube.
Выборочно подсунуть куки одному хосту из одного файла нельзя, поэтому фикс — это фолбэк по образцу гео-ретрая: если загрузка падает с ошибкой формата/экстракции и куки были задействованы — повторить один раз без них, сохранив исходную ошибку, если и ретрай не удался. Куки по-прежнему помогают сайтам, которым они нужны, и больше не топят те, которым вредят. (Та же семья уроков, что и в tg-mpv-bot, где залогиненные куки YouTube добавляли 45 секунд залипания. Куки — это заряженное ружьё, направленное на YouTube.)
4. file_id — повторная загрузка, которая достаётся бесплатно
Telegram выдаёт каждому загруженному файлу ID, и переотправка по file_id
мгновенна и бесплатна — без перекачивания и перезагрузки. Поэтому бот кэширует
(url, format) → file_id. Просишь ссылку, которую уже кто-то качал, — он вообще
пропускает очередь и присылает файл за секунду. На редкий протухший ID он тихо
выкидывает запись и качает заново. Это самое большое ускорение в повседневной
жизни — и почти без кода: просто берёшь то, что платформа уже сама даёт.
5. 64 байта callback_data
Выбор качества — единственный кусок настоящего UI в боте: жмёшь кнопку —
получаешь это разрешение. Inline-кнопки Telegram несут полезную нагрузку
callback_data, ограниченную 64 байтами. URL туда не влезает. Поэтому ссылка
складывается на сервере под коротким токеном, а кнопка несёт только
q:<token>:<choice>. Мелкое ограничение — но именно оно определяет весь дизайн
взаимодействия.
От скрипта к пакету
То, что делает это проектом, а не сниппетом: реально работающая очередь,
настоящий /cancel, понятные сообщения об ошибках, переводящие сырые сбои
yt-dlp в действие («возрастное ограничение → задай COOKIES_FILE»),
лендинг, CI с pytest на двух версиях
Python, мультиарх Docker-образ на ghcr и
пакет в AUR. В Docker он ещё и
обновляет yt-dlp при каждом старте — так что когда YouTube что-то меняет, весь
фикс — это рестарт.
Попробовать
git clone https://github.com/antlis/tg-media-bot && cd tg-media-botcp .env.example .env # BOT_TOKEN от @BotFather, твой ID в ALLOWED_USERSdocker compose up -d # тянет готовый образ + локальный Bot API serverИли paru -S tg-media-bot на Arch.
README описывает каждую
переменную окружения и путь через голый Python. Issues и PR приветствуются.