Part 2: I Broke Out of YouTube My Bot Now Hits Instagram & Twitter/X Too (3 Platforms, 3 Completely Different Protection Models)
š Part 1 is here ā read it first: How I Built a Telegram Bot That Downloads YouTube Music & Videos (Up to 2 GB) š¤ Try the live bot: (https://t.me/songsuperzxbot) š¦ Source code: github.com/chandan0000001/music-video_tele
š Part 1 is here ā read it first:
How I Built a Telegram Bot That Downloads YouTube Music & Videos (Up to 2 GB)š¤ Try the live bot: (https://t.me/songsuperzxbot)
š¦ Source code: github.com/chandan0000001/music-video_telegrambot
Someone sent me a message after Part 1 went live.
"Can it do Instagram Reels?"
Short answer at the time: no.
Slightly longer answer after a few weeks of work: yes, and also Twitter/X, and also Instagram Stories, profile pictures, highlights, and posts. But getting there was a completely different problem from YouTube ā and I mean completely different. Three platforms. Three protection models. Three sets of rules about what you can fetch without authentication, what you can't, and what will quietly rate-limit you into oblivion if you're not careful.
This is the story of how I expanded the bot, what I learned about how Instagram and Twitter/X actually protect their content, and the architectural changes that come with supporting multiple platforms in one bot.
What the Bot Already Does (Quick Recap)
Before I get into the new stuff ā if you haven't read Part 1, the short version:
The bot is a production-grade Telegram bot built with Python 3.11, aiogram 3, FFmpeg, and SQLite WAL mode. It searches YouTube, lets users pick from the top 5 results, and downloads audio in MP3/M4A/WAV up to 320kbps or video in MP4 up to 1080p. The clever parts are:
- yt-dlp's Signature Cipher bypass ā reverse-engineers YouTube's JS player to reconstruct the real stream URL
- Smart 2 GB delivery ā files under 48 MB go via HTTP Bot API instantly; larger files use native MTProto
- Intel VAAPI hardware acceleration ā FFmpeg transcodes using the GPU render node, ~10x faster than CPU
- Full admin panel ā stats, ban/unblock, broadcast, CSV export, live queue view
- 30 automated tests ā URL validation, FFmpeg pipeline, rate limiting, cancellation, database
That foundation is what Part 2 is built on. Now let's talk about why adding Instagram and Twitter/X was harder than just "swap yt-dlp's input URL."
Why Three Platforms is an Architecture Problem, Not Just a Feature
When I built the YouTube handler, it was a one-platform world. The whole flow assumed: user sends query or YouTube URL ā validate ā download ā deliver.
The moment you add Instagram and Twitter/X, that assumption breaks. You now need:
- A URL router ā detect which platform a URL belongs to before doing anything else
- Platform-specific authentication ā Instagram requires a session cookie; YouTube needs cookies for age-restricted content; Twitter/X can sometimes do without
- Platform-specific content types ā YouTube has audio or video. Instagram has reels, stories, highlights, posts, and profile pictures. Twitter/X has videos, images, and posts with mixed media
- Platform-specific rate limiting ā YouTube is relatively forgiving. Instagram will silently downgrade your requests or temporarily block your IP if you hammer it. Twitter/X has changed its protection model more aggressively than any other platform in recent years
Here's the new routing layer that runs before any download starts:
import re
from enum import Enum
class Platform(Enum):
YOUTUBE = "youtube"
INSTAGRAM = "instagram"
TWITTER = "twitter"
UNKNOWN = "unknown"
PLATFORM_PATTERNS = {
Platform.YOUTUBE: re.compile(
r'^https?://(www\.)?(youtube\.com/watch\?v=|youtu\.be/|youtube\.com/shorts/)[A-Za-z0-9_\-]{11}'
),
Platform.INSTAGRAM: re.compile(
r'^https?://(www\.)?instagram\.com/(p|reel|stories|tv|s)/[A-Za-z0-9_\-]+'
),
Platform.TWITTER: re.compile(
r'^https?://(www\.)?(twitter\.com|x\.com)/\w+/status/\d+'
),
}
def detect_platform(url: str) -> Platform:
# SSRF guard ā always first
if re.match(r'https?://(localhost|127\.|10\.|192\.168\.|169\.254\.)', url):
return Platform.UNKNOWN
for platform, pattern in PLATFORM_PATTERNS.items():
if pattern.match(url):
return platform
return Platform.UNKNOWN
The SSRF check runs before platform detection. An attacker sending http://192.168.1.1/admin disguised as any platform URL gets rejected at this layer, not inside any platform-specific handler.
Platform 1: YouTube (The One We Already Solved)
Covered in full in Part 1. The short summary of how YouTube protects streams:
- Signature Cipher ā stream URLs are scrambled via a JavaScript function that yt-dlp reverse-engineers from YouTube's player JS
- n-parameter throttle ā download speed is capped at ~50 KB/s unless you decrypt an "n" parameter from the same JS bundle
-
po_token ā a newer proof-of-origin token embedded in browser sessions, requiring a valid
cookies.txtto bypass
yt-dlp handles all three. Keep it updated and refresh cookies.txt every few weeks.
Platform 2: Instagram ā Sessions, Stories, and Aggressive Rate Limiting
Instagram's protection model is fundamentally different from YouTube's. YouTube obfuscates stream URLs but serves them publicly. Instagram ties almost everything to authenticated sessions.
What's public vs what needs auth
No authentication needed:
- Public post images and videos
- Public Reels
- Profile pictures (always public ā served directly from Instagram's CDN)
Requires a valid Instagram session:
- Stories (expire in 24 hours; private to logged-in users)
- Highlights (archived stories; account-level access)
- Content from private accounts
How Instagram serves content
When you watch a Reel in your browser, the actual video file is served from Instagram's CDN ā URLs that look like:
https://scontent-iad3-1.cdninstagram.com/v/t50.2886-16/...?_nc_ht=scontent...&_nc_cat=...
These CDN URLs are time-limited and session-tied. They're embedded inside a JSON blob in the page's JavaScript bundle or fetched via an internal GraphQL endpoint. yt-dlp's Instagram extractor reverse-engineers these endpoints to pull the direct CDN URL.
For authenticated content (stories, highlights), the extractor needs a valid sessionid cookie ā the same session token your browser uses when you're logged into Instagram.
Configuring Instagram in the bot
def get_instagram_opts(content_type: str, output_path: str) -> dict:
base_opts = {
'outtmpl': output_path,
'quiet': True,
'cookiefile': 'instagram_cookies.txt', # must contain valid sessionid
'http_headers': {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
},
}
if content_type in ('reel', 'video', 'post'):
base_opts['format'] = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]'
elif content_type == 'story':
base_opts['format'] = 'best' # stories are single-stream
elif content_type == 'profile_picture':
base_opts['format'] = 'best'
return base_opts
The rate limiting trap
Instagram rate-limits by IP and by session. If you download too many items in quick succession using the same session cookie, Instagram starts returning 401 Unauthorized errors ā even for public content ā or silently serves lower-quality versions.
My fix: per-platform cooldown timers. Instagram downloads enforce a 15-second minimum gap between requests from the same session. This is longer than the YouTube cooldown (10 seconds) specifically because Instagram's rate limiting is less forgiving and harder to detect when it's happening.
PLATFORM_COOLDOWNS = {
Platform.YOUTUBE: 10, # seconds between requests
Platform.INSTAGRAM: 15, # Instagram rate-limits harder
Platform.TWITTER: 8,
}
Profile pictures ā the easy one
Profile pictures are the simplest Instagram download. They're always public, always served directly from the CDN, and don't require authentication:
https://instagram.com/{username}/?__a=1
The JSON response contains graphql.user.profile_pic_url_hd ā a direct image URL. No cipher, no session required, just a clean HTTP GET.
Platform 3: Twitter/X ā Bearer Tokens and a Platform That Keeps Moving
Twitter's 2023 pivot to X ā and the dramatic changes to API access that came with it ā made this the most technically interesting platform to support.
How Twitter/X protects video content
Twitter/X video is delivered as HLS streams (HTTP Live Streaming). When you watch a video in a tweet, the player fetches an M3U8 playlist that lists the actual video segments:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=2176000,RESOLUTION=1280x720
https://video.twimg.com/ext_tw_video/.../720x1280/...m3u8
#EXT-X-STREAM-INF:BANDWIDTH=832000,RESOLUTION=640x360
https://video.twimg.com/ext_tw_video/.../360x640/...m3u8
The M3U8 playlist URL itself is embedded in the tweet's card data, which is fetched from an internal API endpoint:
https://api.twitter.com/1.1/statuses/show.json?id=<tweet_id>
This endpoint requires a Bearer token ā an authentication token that Twitter's own web client sends with every request. yt-dlp extracts this bearer token from Twitter/X's JavaScript bundle (the same approach it uses for YouTube's cipher ā find the token in the JS, use it for requests).
What happened in 2023
When Elon Musk took over Twitter and rebranded it to X, free API tier access was almost entirely removed. Third-party apps that relied on Twitter's public API were killed. Nitter (the privacy-friendly Twitter frontend) started failing. Many download tools broke.
yt-dlp adapted by switching to the "guest API" ā an unauthenticated path that Twitter/X still exposes for public content, using a guest token that gets issued when you visit the site without being logged in. This guest token approach still works for public tweets.
Handling tweets with multiple images
A tweet can have 1-4 images. When a user sends a tweet URL with multiple images, the bot downloads all of them and sends them as a Telegram media group:
async def handle_twitter_images(url: str, message: Message):
with yt_dlp.YoutubeDL({'quiet': True, 'skip_download': True}) as ydl:
info = ydl.extract_info(url, download=False)
if 'entries' in info:
# Multiple images ā send as album
media_group = []
for entry in info['entries']:
img_url = entry.get('url')
if img_url:
media_group.append(InputMediaPhoto(media=img_url))
if media_group:
await message.answer_media_group(media=media_group)
else:
# Single image or video
...
Telegram supports sending up to 10 items in a media group, which covers Twitter/X's 4-image maximum comfortably.
The Content-Type Detection System
Instagram URLs carry information about content type in the path:
| URL pattern | Content type |
|---|---|
instagram.com/p/{id}/ |
Post (image or video) |
instagram.com/reel/{id}/ |
Reel (short video) |
instagram.com/stories/{user}/{id}/ |
Story |
instagram.com/s/{highlight_id}/ |
Highlight |
instagram.com/{username}/ |
Profile (ā profile picture) |
instagram.com/tv/{id}/ |
IGTV (long video) |
def detect_instagram_content_type(url: str) -> str:
if '/reel/' in url: return 'reel'
if '/stories/' in url: return 'story'
if '/p/' in url: return 'post'
if '/s/' in url: return 'highlight'
if '/tv/' in url: return 'video'
# If it's just instagram.com/{username}/ ā profile picture
if re.match(r'https?://(www\.)?instagram\.com/[^/]+/?$', url):
return 'profile_picture'
return 'unknown'
The bot surfaces a different keyboard to the user depending on content type. A Reel shows "Download Video" and "Download Audio". A Story just shows "Download". A profile URL shows "Download Profile Picture". No unnecessary choices for things that have only one format.
Updated Supported Platforms
Here's where the bot stands now:
| Platform | Content Types | Auth Required |
|---|---|---|
| YouTube | Music search, Audio (MP3/M4A/WAV), Video (144pā1080p) | cookies.txt (age-restricted only) |
| Reel, Video, Post, Story, Highlight, Profile Picture | sessionid cookie (stories/highlights) | |
| Twitter / X | Video, Post images (up to 4), GIF | None (public tweets) |
All three platforms route through the same pipeline:
URL ā platform detection ā SSRF guard ā content type detection ā platform-specific yt-dlp config ā FFmpeg ā smart delivery (HTTP or MTProto) ā cleanup.
What I Learned Building This
Instagram is the hardest platform to stay compatible with. YouTube breaks when they rotate the cipher ā that's a known, predictable event that yt-dlp patches. Instagram breaks when they change their internal GraphQL schema, which happens without announcement and with no public documentation to reference. Two of my three "bot is down" incidents since adding Instagram support were Instagram schema changes, not YouTube.
Twitter/X's guest API is fragile. It works today. X has shown zero interest in maintaining compatibility with third-party tools, and the guest API could be deprecated at any point. I handle this by catching yt-dlp errors and returning a clear message to users ā "this video may not be downloadable" ā instead of silently failing.
Per-platform rate limiting is non-negotiable. A single rate limiter across all platforms was my first approach. It was wrong. Instagram needs longer cooldowns than YouTube. Twitter/X is somewhere in the middle. Platform-specific cooldown values aren't premature optimization ā they're the difference between a bot that works reliably and one that gets session-banned after 20 minutes.
yt-dlp is genuinely impressive. One library, three completely different platform protection models, maintained by a community that ships patches sometimes within hours. The right posture when building on top of it: keep it updated, wrap all extractor calls in robust error handling, and never assume a config that works today will work in two weeks.
Try It
The bot handles all three platforms now. Drop a YouTube link, an Instagram Reel, or a Twitter/X video URL and it figures out the rest.
Try it: @songsuperzxbot
Source code: github.com/chandan0000001/music-video_telegrambot
Part 1 (YouTube deep-dive): dev.to/chandan0000001/...
Questions about Instagram's session handling, Twitter/X's HLS parsing, or anything else ā drop them in the comments.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes ā full credit and traffic to the original publisher.