# Бот для скачивания рилс и TikTok в Telegram на Python

> Как сделать Telegram-бота на Python и aiogram 3, который скачивает рилс Instagram и видео TikTok без водяного знака по ссылке. Полный код и запуск на сервере.

- URL: https://storiesfly.com/ru/blog/telegram-bot-download-instagram-reels-python
- Language: Русский (ru)
- English version: https://storiesfly.com/blog/telegram-bot-download-instagram-reels-python
- Published: 2026-10-21
- Topic: Telegram
- Publisher: StoriesFly (https://storiesfly.com)

## Как сделать Telegram-бота для скачивания рилс Instagram и видео TikTok?

Нужен бот на Python и aiogram 3. Он находит ссылку Instagram или TikTok в сообщении, запрашивает у метода download API файлы, скачивает их и загружает в чат. Скрипт ниже занимает около 120 строк и тратит один запрос API на одну ссылку. StoriesFly — наш API, поэтому используем его; аккаунт Instagram или TikTok, логин и cookies не нужны.

Бот работает с публичными рилс, постами и ссылками IGTV, а также с видео TikTok (ссылки `www`, `m`, а также короткие `vm` и `vt`). Видео TikTok приходят без водяного знака. Одно видео или фото бот присылает как есть, карусель — альбомом до 10 элементов.

## Что понадобится

- Python 3.10 или новее (в коде используется запись `bytes | None`).
- Токен Telegram-бота.
- Ключ StoriesFly API.

**Токен бота.** Откройте в Telegram [@BotFather](https://t.me/BotFather), отправьте `/newbot`, выберите имя и username, заканчивающийся на `bot`. BotFather пришлёт токен вида `123456:ABC...`. Не публикуйте его.

**Ключ API.** Войдите через Telegram на [/developers/dashboard](/developers/dashboard) и создайте ключ вида `sf_live_...`. Новый аккаунт получает 100 бесплатных запросов (один раз на Telegram-аккаунт), этого хватит, чтобы проверить бота. Документация на английском: [/developers/docs](/developers/docs).

**Установка и запуск.**

```bash
pip install "aiogram>=3,<4" aiohttp
export BOT_TOKEN="123456:ABC..."
export STORIESFLY_KEY="sf_live_..."
python reels_bot.py
```

Откройте бота в Telegram, нажмите `/start` и отправьте ссылку, например `https://www.instagram.com/reel/XXXXXXXXXXX/` или `https://www.tiktok.com/@user/video/123...`.

## Как устроен бот

### Поиск ссылки

```python
LINK_RE = re.compile(
    r"https?://(?:www\.)?instagram\.com/(?:p|reels?|tv)/[\w-]+"
    r"|https?://(?:www\.|m\.|vm\.|vt\.)?tiktok\.com/[^\s]+"
)
```

Первая половина шаблона принимает `/p/` (посты), `/reel/` и `/reels/` (рилс) и `/tv/` (IGTV). Она обрывается на коде поста, поэтому ссылка с трекинг-параметрами вроде `?igsh=...` тоже подходит, а параметры отбрасываются. Вторая половина принимает ссылки TikTok на хостах `www`, `m`, `vm` и `vt` и берёт всё до ближайшего пробела. Обработчик использует `F.text.regexp(LINK_RE)`: сработает любое сообщение со ссылкой, даже если вокруг есть текст. Остальные сообщения попадают в последний обработчик, который просит прислать ссылку.

### Запрос /v1/download

```python
async with session.get(
    f"{API}/download", params={"url": url}, headers={"X-Api-Key": KEY}
) as r:
```

[`GET /v1/download`](/developers/docs/download) принимает одну ссылку `url` и работает для обеих платформ. Метод отдаёт только файлы, без метрик. Поля ответа:

| Поле | Что значит |
|---|---|
| `platform` | `instagram` или `tiktok` |
| `id` | id поста или видео |
| `url` | ссылка на пост |
| `author` | username автора |
| `caption` | только для TikTok; для Instagram `null` |
| `media_type` | `video`, `photo` или `carousel` |
| `thumbnail_url` | картинка-превью |
| `media` | список файлов: `type` (`video` или `image`), `url`, `quality` (`hd`, `sd` или `null`), `thumbnail_url` |
| `audio_url` | только для TikTok |

Ответ выглядит так (значения условные, сокращено):

```json
{
  "platform": "tiktok",
  "id": "7300000000000000000",
  "url": "https://www.tiktok.com/@user/video/7300000000000000000",
  "author": "sample_user",
  "caption": "Sample caption",
  "media_type": "video",
  "thumbnail_url": "https://...",
  "media": [
    { "type": "video", "url": "https://...", "quality": "hd", "thumbnail_url": "https://..." }
  ],
  "audio_url": "https://..."
}
```

Для видео TikTok первый элемент `media` — лучшее качество без водяного знака. Поэтому, когда `media_type` равен `video`, бот берёт только первый элемент:

```python
files = post["media"]
if post["media_type"] == "video":
    files = files[:1]  # the first entry is the best quality
```

Подпись читается как `(post.get("caption") or "")[:1000] or None`, поэтому пост Instagram, где она `null`, уходит без подписи.

Ошибки приходят в JSON: `{"error": "...", "code": "not_found"}`. `fetch_download` при любом статусе, кроме 200, выбрасывает `LookupError(code)`, а обработчик превращает код в сообщение.

### Почему бот скачивает файл и загружает его сам

Ссылки на медиа в ответе через какое-то время перестают работать, поэтому их нужно использовать сразу. Проще всего передать URL в `answer_video`, и Telegram сам скачает файл. Но это работает только для небольших файлов: лимит на файлы, которые бот отправляет по ссылке, ниже лимита 50 МБ на файлы, которые бот загружает (в документации Bot API для ссылок указаны 5 МБ для фото и 20 МБ для остальных файлов). Видео часто больше.

Поэтому бот скачивает байты через `fetch_file` и отправляет их как `BufferedInputFile`. Сначала проверяется `Content-Length`: если файл больше 50 МБ, бот не загружает его, а присылает ссылку.

Ограничение кода: если сервер не передал `Content-Length`, проверка размера пройдёт и файл целиком прочитается в память. Рилс и видео TikTok обычно сильно меньше лимита, для небольшого бота этого достаточно.

### Карусели как альбомы

```python
for i, m in enumerate(files[:10]):  # an album holds up to 10
```

В альбом Telegram помещается до 10 элементов, поэтому список обрезается до 10. Каждый элемент становится `InputMediaVideo` или `InputMediaPhoto` в зависимости от `m["type"]`. Подпись добавляется только к первому элементу: Telegram показывает подпись альбома по первому. Подпись обрезается до 1000 символов, это меньше лимита Telegram для подписи к медиа. Если остался один элемент, бот отправляет его через `answer_video` или `answer_photo`, а не альбомом.

### Ошибки в понятных сообщениях

```python
ERRORS = {
    "not_found": "This post does not exist or was deleted.",
    "private_account": "The account is private, so its posts are not available.",
    "bad_request": "That does not look like an Instagram or TikTok post link.",
    ...
    "upstream_unavailable": "Could not get this one right now. Try again in a minute.",
}
```

Код ошибки API сопоставляется с фразой для пользователя. Для неизвестного кода бот отвечает «Something went wrong, try later.» Чаще всего встречаются такие коды:

| Код | HTTP | Что значит | Списание |
|---|---|---|---|
| `not_found` | 404 | пост не существует или удалён | да |
| `private_account` | 403 | аккаунт закрыт; приватного контента API не отдаёт | да |
| `bad_request` | 400 | это не ссылка на пост Instagram или TikTok | нет |
| `rate_limited` | 429 | слишком много запросов; ждите `Retry-After` | нет |
| `credits_exhausted` | 402 | баланс закончился | нет |
| `upstream_unavailable` | 503 | сбой на нашей стороне; бот отвечает «попробуйте через минуту» | возврат |

На 429 бот не повторяет запрос, а просит пользователя попробовать позже. Если ботом пользуются многие сразу, добавьте небольшую очередь или паузу.

Тексты сообщений в коде на английском; при желании переведите их в словаре `ERRORS`.

## Полный скрипт

Сохраните как `reels_bot.py`.

```python
"""Telegram bot: send it an Instagram reel/post or a TikTok link, get the video back.

Usage:
    pip install "aiogram>=3,<4" aiohttp
    BOT_TOKEN=123:abc STORIESFLY_KEY=sf_live_... python reels_bot.py
"""
import asyncio
import os
import re

import aiohttp
from aiogram import Bot, Dispatcher, F
from aiogram.filters import CommandStart
from aiogram.types import (
    BufferedInputFile,
    InputMediaPhoto,
    InputMediaVideo,
    Message,
)

API = "https://storiesfly.com/api/v1"
KEY = os.environ["STORIESFLY_KEY"]
BOT_TOKEN = os.environ["BOT_TOKEN"]

# A bot may upload files up to 50 MB; bigger videos are sent as a link.
MAX_UPLOAD = 50 * 1024 * 1024
LINK_RE = re.compile(
    r"https?://(?:www\.)?instagram\.com/(?:p|reels?|tv)/[\w-]+"
    r"|https?://(?:www\.|m\.|vm\.|vt\.)?tiktok\.com/[^\s]+"
)

ERRORS = {
    "not_found": "This post does not exist or was deleted.",
    "private_account": "The account is private, so its posts are not available.",
    "bad_request": "That does not look like an Instagram or TikTok post link.",
    "rate_limited": "Too many requests at once. Try again in a few seconds.",
    "credits_exhausted": "The bot is out of API requests for now.",
    "upstream_unavailable": "Could not get this one right now. Try again in a minute.",
}

dp = Dispatcher()


async def fetch_download(session: aiohttp.ClientSession, url: str) -> dict:
    async with session.get(
        f"{API}/download", params={"url": url}, headers={"X-Api-Key": KEY}
    ) as r:
        data = await r.json()
        if r.status != 200:
            raise LookupError(data.get("code", "error"))
        return data


async def fetch_file(session: aiohttp.ClientSession, url: str) -> bytes | None:
    async with session.get(url) as r:
        r.raise_for_status()
        size = int(r.headers.get("Content-Length", 0))
        if size > MAX_UPLOAD:
            return None
        return await r.read()


@dp.message(CommandStart())
async def start(message: Message) -> None:
    await message.answer("Send me a link to an Instagram reel or post, or a TikTok video.")


@dp.message(F.text.regexp(LINK_RE))
async def on_link(message: Message, session: aiohttp.ClientSession) -> None:
    link = LINK_RE.search(message.text).group(0)
    await message.bot.send_chat_action(message.chat.id, "upload_video")

    try:
        post = await fetch_download(session, link)
    except LookupError as e:
        await message.answer(ERRORS.get(str(e), "Something went wrong, try later."))
        return

    files = post["media"]
    if post["media_type"] == "video":
        files = files[:1]  # the first entry is the best quality
    caption = (post.get("caption") or "")[:1000] or None

    items = []
    for i, m in enumerate(files[:10]):  # an album holds up to 10
        data = await fetch_file(session, m["url"])
        if data is None:  # over 50 MB: give the link instead
            await message.answer(f"The video is too big to upload, here is the link:\n{m['url']}")
            continue
        is_video = m["type"] == "video"
        name = f"{post['id'] or 'post'}_{i}.{'mp4' if is_video else 'jpg'}"
        file = BufferedInputFile(data, filename=name)
        cap = caption if not items else None
        items.append(
            InputMediaVideo(media=file, caption=cap)
            if is_video
            else InputMediaPhoto(media=file, caption=cap)
        )

    if len(items) == 1:
        one = items[0]
        if isinstance(one, InputMediaVideo):
            await message.answer_video(one.media, caption=one.caption)
        else:
            await message.answer_photo(one.media, caption=one.caption)
    elif items:
        await message.answer_media_group(items)


@dp.message()
async def other(message: Message) -> None:
    await message.answer("I need a link like https://www.instagram.com/reel/... or https://www.tiktok.com/@user/video/...")


async def main() -> None:
    bot = Bot(BOT_TOKEN)
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=120)) as session:
        await dp.start_polling(bot, session=session)


if __name__ == "__main__":
    asyncio.run(main())
```

## Как запустить на сервере

В режиме polling публичный адрес не нужен, подойдёт любой небольшой VPS. Создайте пользователя и virtualenv, положите скрипт в `/opt/reels-bot`, а секреты храните в env-файле с правами `chmod 600`.

```ini
# /etc/systemd/system/reels-bot.service
[Unit]
Description=Reels and TikTok Telegram bot
After=network-online.target

[Service]
WorkingDirectory=/opt/reels-bot
EnvironmentFile=/opt/reels-bot/.env
ExecStart=/opt/reels-bot/venv/bin/python reels_bot.py
Restart=always
RestartSec=5
User=reelsbot

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now reels-bot
journalctl -u reels-bot -f
```

В `.env` две строки: `BOT_TOKEN=...` и `STORIESFLY_KEY=...`, без кавычек. Если нужен Docker, возьмите образ `python:3.12-slim`, установите оба пакета через `pip`, передайте те же переменные через `--env-file` и добавьте `--restart unless-stopped`.

## Сколько это стоит

Бот делает один запрос API на одну ссылку. Скачивание самого файла запросом API не считается. У `download` своя фиксированная цена за скачивание, ниже обычного запроса данных, причём скачивание TikTok стоит дешевле, чем Instagram. Пока у вас есть бесплатные запросы, одно скачивание тратит один из 100. Ответы «не найдено» и «закрытый аккаунт» списываются как данные, поэтому ссылка на удалённый пост тоже стоит одного скачивания. Актуальные цены смотрите на [/developers/pricing](/developers/pricing).

Если нужны просмотры, лайки и другие цифры, вызывайте `/v1/post` для Instagram или `/v1/tiktok/video` для TikTok. Это обычные запросы данных.

Лимит частоты — 1 запрос в секунду до первого пополнения. В каждом ответе есть заголовки `X-Balance-Remaining` и `X-RateLimit-Remaining`: логируйте их, чтобы видеть остаток.

## Как добавить сторис

Для сторис подход тот же. Вызовите [`GET /v1/user/stories?username=...`](/developers/docs/user-stories). В ответе `stories[]` с полями `media_type` (`video` или `photo`), `media_url`, `taken_at` и `video_duration`. Добавьте команду `/stories username`, скачайте каждый `media_url` так же и отправьте альбомом. Подробный разбор — в статье [Как скачать сторис Instagram на Python](/blog/download-instagram-stories-python). Для закрытых аккаунтов вернётся `private_account`.

## Важно

Это работает только с публичными данными. Соблюдайте правила Instagram и TikTok и местное законодательство и отправляйте людям только тот контент, который они вправе сохранять: свой или с разрешения автора.

## Частые вопросы

### Может ли бот скачивать видео TikTok без водяного знака?

Да. Для видео TikTok первый элемент `media` — лучшее качество без водяного знака, его бот и отправляет.

### Может ли бот скачивать рилс из закрытых аккаунтов?

Нет. API возвращает `private_account` без контента, а бот сообщает, что аккаунт закрыт.

### Почему бот загружает видео сам, а не отправляет ссылку?

Telegram ограничивает файлы, которые бот отправляет по ссылке, меньшим размером, чем загружаемые (50 МБ). Видео часто превышает лимит для ссылок. К тому же так в чат не попадёт просроченная ссылка.

### Нужен ли аккаунт Instagram или TikTok, или cookies?

Нет. Бот обращается только к StoriesFly API с заголовком `X-Api-Key`. Ни логина, ни cookies, ни аккаунта Instagram, Facebook или TikTok не требуется.

### Можно ли использовать вебхуки вместо polling?

Да, aiogram поддерживает вебхуки. В примере используется polling, потому что ему не нужны домен и HTTPS-сертификат.

### Сколько запросов тратится на одну ссылку?

Одно скачивание. Вызов `/v1/download` возвращает все элементы карусели в одном ответе. Загрузка файлов по ссылкам из ответа в запросы не входит.
