A friendly Telegram media downloader for Instagram, TikTok, YouTube, SoundCloud, and Spotify.
DuckLoader turns supported links into downloadable media inside Telegram. It combines a simple user experience with per-user settings, a full owner-only admin panel, sponsor-channel monetization, advertising requests, download protection, error logging, and configurable Duck reactions.
- TikTok
- YouTube
- SoundCloud
- Spotify
Instagram, TikTok, YouTube, and SoundCloud use yt-dlp for extraction. Instagram photo posts, photo carousels, photo/video stories and highlight share links (instagram.com/s/...) are also supported.
Spotify is handled through its Web API for track metadata. The matching audio is searched on YouTube Music first (it carries the official label-supplied recordings), then YouTube, then SoundCloud. Every candidate is scored on title, artists and duration; covers, remixes, live/sped-up/instrumental versions and same-titled songs by other artists are rejected, and the downloaded file's real length is verified before it is sent. Spotify's protected streams are never touched. Protected (DRM / Go+ preview-only) SoundCloud tracks use the same matcher to find the song elsewhere.
Users can choose their default quality in /settings:
- Best
- 720p
- Audio only
YouTube also has a per-video quality picker that inspects available formats and presents suitable resolutions. The bot can automatically reduce quality when a result would otherwise exceed the configured Telegram upload limit.
Each user can configure:
- 🇮🇷 Persian or 🇺🇸 English interface
- Best, 720p, or audio-only download quality
The owner can configure multiple Telegram stickers or GIF/Animation reactions for:
- Start
- Downloading
- Failed
- Complete
DuckLoader can require users to join configured sponsor channels before downloading.
When enabled:
- A supported link is received.
- Sponsor membership is checked.
- Users who are not members see a join button for each remaining sponsor channel.
- ✅ Check Membership & Continue rechecks membership without asking the user to resend the link.
- The keyboard is updated so only channels the user still needs to join remain visible.
- Once all memberships are confirmed, the original download resumes automatically.
The owner can temporarily disable the entire membership requirement or exempt individual users.
For a sponsor channel to be enforceable, the bot should be an administrator and Telegram must allow the bot to inspect membership. If Telegram refuses a membership check, that channel is skipped and the user is allowed to continue.
DuckLoader has two advertising systems.
The owner can configure one global post-download advertisement and enable or disable it independently.
Users can submit advertising requests with /adrequest or through the optional advertisement button.
The form starts in Persian and can be switched to English from inside the form. Advertisers choose between:
- 📢 Sponsor Channel — users must join the advertiser's channel before using the downloader, when the sponsor gate is enabled.
- 📣 Post-Download Ad — the advertiser's promotion is shown after successful downloads.
Requests are sent to the owner immediately for review and remain available in the admin panel. The owner can approve, reject, or contact the advertiser.
An approved post-download ad runs as a campaign with impression and click tracking (stored in ad_stats.json):
- Quota — the number in the request's duration field (e.g.
1000,۱۰۰۰ نمایش,2k) is the impression quota. When it is reached the campaign stops and the owner gets the final report. - Frequency rules — no ad if the user saw any ad in the last 15 minutes; the same campaign at most 3 times per user, at least 12 hours apart; never again after the user clicks it. Users who have seen a campaign least, and campaigns furthest behind their quota, go first.
- Banner — the owner sends or forwards a photo / video / GIF post with caption (or a text post) and it is sent to users exactly like that, formatting included. Without a banner a short "📣 Ad" message with the button is sent.
- Button or caption link — per campaign, the owner chooses a "join the channel" button under the banner (clicks are counted: the first tap is recorded and the button turns into the channel link) or no button, with the channel written in the caption (clicks can't be counted). The button link and text can be changed from the panel.
- Approval — an approved post-download ad starts paused, so the banner can be set and previewed before pressing
▶️ . - Global ad — the owner's own ad (
/setad, or panel → Ads → 🖼 Global ad) is a campaign too, with a banner, the same frequency rules and a report, but no quota; paid campaigns are always shown first. - Clicks — the ad button is counted on the first tap and then turns into the channel link.
- Report — impressions vs. quota, unique viewers, clicks, unique clickers, CTR and a daily breakdown. From the admin panel the owner can pause/resume, raise the quota, and send the report to the advertiser.
The owner can:
- Ban and unban users
- Exempt users from the sponsor membership requirement
- Remove exemptions
- Broadcast messages to known users
The admin panel includes:
- Total known users
- Download counts by platform
- Recent download errors
- Error details including time, platform, user, link, and message
- Sponsor-channel tools
/checksponsor is owner-only and can be used to inspect whether the bot can check membership in a sponsor channel.
| Command | Description |
|---|---|
/start |
Start or initialize DuckLoader |
/help |
Show the bot's main information |
/settings |
Choose language and download quality |
/whoami |
Show your Telegram user ID |
/adrequest |
Submit an advertising request |
/feedback |
Suggest a feature or report a problem (also the 💬 button under the chat) |
Feedback goes straight to the owner with the user's name, ID, language, start-link source and — for problem reports — that user's latest errors from the error log. Screenshots, videos and voice notes are forwarded as sent. The owner's copy has a 💬 Reply to user button; the reply is delivered to the user by the bot. Everything is also kept in feedback.json. Users can send at most 5 messages per hour.
For normal downloading, users simply send a supported URL.
These commands work only for the user configured as OWNER_ID.
| Command | Description |
|---|---|
/lock <platform> |
Disable a platform |
/unlock <platform> |
Enable a platform |
/toggle <setting> |
Toggle a bot-wide setting |
/stats |
Show bot statistics |
/broadcast <message> |
Broadcast a message to known users |
/ban <user_id> |
Ban a user |
/unban <user_id> |
Unban a user |
/setad <message> |
Set the global ad as a text banner (picture banners: panel → Ads → 🖼 Global ad) |
/checksponsor <channel> |
Check bot access to a sponsor channel |
/addsponsor <channel> <name> |
Add a sponsor channel |
/removesponsor <channel> |
Remove a sponsor channel |
/sponsors |
List sponsor channels |
/senddl <user_id> <link> [<link> ...] |
Download link(s) and deliver them to a user in the normal format (e.g. to make up for failed requests) |
/setcache |
Connect the private cache channel (then forward any message from that channel to the bot). Every delivered file is kept there; inline mode needs it |
The owner can also access these functions through the button-based admin panel from /settings.
The current admin panel provides:
Enable or disable downloading for each supported platform.
- Auto quality fallback
- Global sponsor advertisement
- Advertisement-request button
- Sponsor-channel membership requirement
- Set the global advertisement
- Add sponsor channels
- Remove sponsor channels
- View sponsor channels
Review pending requests and approve, reject, or contact advertisers. 📊 Post-download ad reports lists every campaign with its progress and opens its report.
- Ban
- Unban
- Broadcast
- Exempt from sponsor membership
- Remove sponsor exemption
View user totals, platform download counts, and recorded errors.
Inspect recent errors with platform, user ID, source link, time, and the recorded error message.
Configure and clear reactions for start, downloading, failure, and completion. Multiple reactions can be stored for every stage.
The owner can still use the normal user language and quality settings.
A built-in reference page lists the available owner commands.
- Python 3.12+ recommended
- FFmpeg
- Deno or another supported JavaScript runtime for full YouTube challenge solving
- A Telegram bot token from @BotFather
- A Telegram user ID for
OWNER_ID - Spotify API credentials if Spotify support is needed
Install Python dependencies:
python -m pip install -r requirements.txtInstall FFmpeg on Debian/Ubuntu:
sudo apt update
sudo apt install -y ffmpegInstall Deno:
curl -fsSL https://deno.land/install.sh | shytmusicapi (in requirements.txt) is what makes Spotify matching accurate; the startup log warns if it is missing.
Create .env in the project root:
BOT_TOKEN=your_telegram_bot_token
OWNER_ID=your_telegram_user_id
SPOTIFY_CLIENT_ID=your_spotify_client_id
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
# Optional: explicit FFmpeg directory when it is not visible on PATH
FFMPEG_LOCATION=/usr/bin
# Optional: comma-separated proxy URLs for lightweight probing/search calls
ROTATING_PROXIES=socks5://127.0.0.1:9050,socks5://127.0.0.1:9051| Variable | Required | Purpose |
|---|---|---|
BOT_TOKEN |
Yes | Telegram Bot API token |
OWNER_ID |
Yes | Telegram user ID of the owner |
SPOTIFY_CLIENT_ID |
Spotify | Spotify application client ID |
SPOTIFY_CLIENT_SECRET |
Spotify | Spotify application client secret |
FFMPEG_LOCATION |
No | Explicit FFmpeg directory |
ROTATING_PROXIES |
No | Proxy URLs for lightweight probing/search |
MAX_UPLOAD_MB |
No | Upload limit override |
YOUTUBE_COOKIE_FILE |
No | YouTube cookie-file path |
INSTAGRAM_COOKIE_FILE |
No | Instagram cookie-file path |
YTDLP_COOKIES_BROWSER |
No | Browser source for YouTube cookies |
INSTAGRAM_COOKIES_BROWSER |
No | Browser source for Instagram cookies |
INSTAGRAM_COOKIES_PROFILE |
No | Browser profile (name or path) for INSTAGRAM_COOKIES_BROWSER |
TIKTOK_COOKIE_FILE / TIKTOK_COOKIES_BROWSER |
No | Logged-in TikTok cookies (age-restricted videos) |
YTDLP_PLAYER_CLIENT |
No | Override YouTube player clients |
BOT_WORKER_THREADS |
No | Telegram handler threads (default 96) |
MAX_CONCURRENT_DOWNLOADS |
No | Downloads running at once, bot-wide (default 4) |
INSTAGRAM_MAX_CONCURRENT |
No | Instagram downloads running at once (default 3) |
MAX_QUEUE_WAITING |
No | Requests allowed to wait in the queue (default 40) |
MEDIA_CACHE_TTL_HOURS |
No | How long an already-delivered link is re-sent from the cache instead of downloaded again (default 720 = 30 days; the caption's likes/views are from the first download) |
SPOTIFY_MAX_TRACKS |
No | Max tracks fetched from one album/playlist link (default 50) |
MAX_PLAYLIST_ENTRIES |
No | Max items from one link, e.g. a highlight or SoundCloud set (default 30) |
- A link that was already downloaded (same media, same quality, by anyone, in private, in a group or inline; Spotify single tracks too) is re-sent instantly by its Telegram
file_idforMEDIA_CACHE_TTL_HOURS. The index is kept on disk (media_cache.db, survives restarts) and every delivered result is copied into the cache channel, so the files stay on Telegram even if the user deletes the chat. When many people send the same link at the same moment, only the first one downloads it; the rest wait and get the uploaded copy. - Sending the same link again while it is still downloading is ignored with a short notice.
- Queued users see their position in the queue.
Add the bot to a group and it downloads every supported link members send — quietly: no progress messages, duck stickers, sponsor gate or ads. A 👀 reaction on the link means "working on it", the file is sent as a reply to the link, and 🤷 means it failed. One download runs at a time per group, and Spotify albums/playlists are redirected to the private chat.
YouTube shows the quality picker in groups too, but only the person who sent the link can use it. If the bot is a group admin, the picker is sent as an ephemeral message (Bot API 10.2+) that only that person sees; otherwise it's a normal message that ignores everyone else's taps.
With privacy mode on, a bot only sees plain links in groups where it is an admin — the bot says so when it's added. Set /setprivacy → Disable in @BotFather to make it work everywhere.
In any chat, type @DuckDownloader_Bot <link>:
- Media that was already downloaded is offered instantly from the cache.
- Otherwise a "⏳" placeholder is sent; the bot downloads the file, uploads it to the private cache channel (an inline message can only be turned into media that's already on Telegram) and replaces the placeholder with the file.
- YouTube: the sent message becomes a quality picker right in that chat — only the sender's taps count — and then turns into the video.
- Users who haven't joined the sponsor channels get a button that takes them to the bot first.
- Inline posts carry the full caption (username, likes / comments / views and the post text) with a 🙈 Hide caption button that only the sender can use. Users who never want captions in inline mode can turn them off in
/settings(📝 Captions in inline mode). - Nobody has to remember the username: files downloaded in the private chat have a 📤 Send to your friends button (opens the chat picker with
@DuckDownloader_Bot <link>already typed), and every inline file has a 🦆 Download another link button that types@DuckDownloader_Botinto the current chat. Telegram also lists recently used inline bots as soon as you type@.
Setup: in @BotFather enable /setinline and set /setinlinefeedback to 100%; create a private channel, make the bot an admin, then send /setcache to the bot and forward any message from that channel (or set CACHE_CHAT_ID in .env).
A video sent or forwarded to the bot in a private chat (also round video notes and videos sent as files) gets a 🎧 Audio file button, and 🎙 Voice message for clips up to 10 minutes. The audio file is an .m4a that keeps the original sound track untouched when it is AAC/MP3 (almost always), so even a large video takes only seconds; the cover is a frame of the video. It uses the same download queue, progress bar and rate limit as downloads. The owner can turn it off in the admin panel (General settings → 🎧 Video to audio) or with /toggle video_to_audio.
Telegram lets bots receive files of at most 20 MB unless the local Bot API server runs in --local mode. tools/enable_local_bot_api.sh switches the Docker container to local mode: it copies its data to /home/mahshot/telegram-bot-api (set LOCAL_BOT_API_DIR if you use another folder), re-creates the container with the same settings plus TELEGRAM_LOCAL=1, keeps the old one for rollback, and installs a cron job that deletes received videos after an hour. Inputs are capped at VIDEO_TO_AUDIO_MAX_MB (default 1024).
Give every ad campaign its own start link, e.g. https://t.me/DuckDownloader_Bot?start=insta_ali. New users who arrive through it are counted per source, and /stats (or 📊 in the admin panel) shows the last 7 days of new users, active users and downloads — the numbers advertisers ask for.
INSTAGRAM_COOKIE_FILE defaults to instagram_cookies.txt in the bot folder when it exists. If both a browser and a cookie file are configured, both are tried in turn. Cookie files are only ever read — the bot works on a temporary copy, so an Instagram hiccup can no longer wipe the saved session out of the file.
When Instagram cookies stop working (expired session, logged-out responses), the owner receives a Telegram alert (at most once every 6 hours).
Authenticated cookies can be used when a platform requires them, especially for some Instagram and YouTube content.
Typical files are:
cookies.txt
instagram_cookies.txt
Treat cookie files as credentials. Keep them on the deployment machine and never commit them to Git.
Run the bot directly with:
python bot.pyAt startup the bot checks for FFmpeg and an external JavaScript runtime and logs what the running process can actually see.
A production VPS can run DuckLoader through systemd.
Example:
[Unit]
Description=DuckLoader Telegram Bot
After=network.target
[Service]
Type=simple
User=mahshot
WorkingDirectory=/home/mahshot/DuckLoaderBot
ExecStart=/home/mahshot/venv/bin/python /home/mahshot/DuckLoaderBot/bot.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl restart duckloader
sudo systemctl status duckloaderView live logs with:
sudo journalctl -u duckloader -fDuckLoader/
├── bot.py
├── bot_features.py
├── admin.py
├── ads.py
├── platforms.py
├── store.py
├── requirements.txt
└── .gitignore
Application entry point. Loads environment variables, configures the Telegram API, performs startup dependency checks, registers features, and starts polling.
Contains the main Telegram user experience: commands, settings, URL handling, sponsor checks, ad requests, download callbacks, progress messages, uploads, and Duck reactions.
Contains owner-only commands and the button-based management panel.
Handles sponsor-channel normalization, sponsor-channel storage, membership checks, and the global advertisement message.
Handles platform detection, yt-dlp extraction/downloads, YouTube quality probing, authentication settings, Spotify matching, media classification, file-size handling, and cleanup.
Stores persistent state in small JSON files: users, settings, feature flags, bans, statistics, errors, advertising requests, Duck reactions, and sponsor exemptions.
| Platform / setting | Default |
|---|---|
| ✅ Enabled | |
| TikTok | ✅ Enabled |
| YouTube | ❌ Disabled |
| SoundCloud | ✅ Enabled |
| Spotify | ✅ Enabled |
| Auto quality fallback | ❌ Disabled |
| Advertisement button | ✅ Enabled |
| Sponsor membership requirement | ✅ Enabled |
The owner can change runtime settings from the admin panel.
To enforce sponsor-channel membership:
- Add the channel with the admin panel or
/addsponsor. - Make DuckLoader an administrator in the channel.
- Run
/checksponsor @ChannelUsernamefrom the owner account. - Confirm that Telegram allows the bot to check membership.
- Leave Sponsor Channel Membership Requirement enabled.
Example:
/addsponsor @MyChannel My Channel
Then:
/checksponsor @MyChannel
If Telegram refuses a membership check, that channel is skipped and the user can continue. This is intentional behavior.
/adrequest
↓
Channel
↓
Display name
↓
Advertisement type
↓
Duration / displays
↓
Notes
↓
Confirmation
↓
Submit
The request form starts in Persian and can be switched to English.
The owner receives a newly submitted request immediately and can approve, reject, or contact the advertiser.
Approved Sponsor Channel requests become active sponsor channels. Approved Post-Download requests are eligible to appear after successful downloads.
The repository should contain source code and documentation, not secrets or runtime state.
Keep these out of Git:
.env
cookies.txt
instagram_cookies.txt
*.pem
*.key
*.json
*.backup
.cache/
downloads/
venv/
Runtime JSON files should live on the deployment machine and be backed up separately from the Git repository.
If credentials or cookies are ever accidentally committed to a public repository, treat them as exposed and rotate/revoke them as appropriate.
DuckLoader depends on external services and extractors. Platforms can change their web pages, authentication, anti-bot protection, and APIs without notice.
When a platform stops working:
- Update
yt-dlpand related dependencies. - Check the startup logs for dependency problems.
- Check the relevant yt-dlp extractor status.
- Check authentication/cookie validity when applicable.
TikTok, Instagram, YouTube, and other services may require different fixes when their upstream behavior changes.
Use a deliberate Git workflow and review staged changes before committing:
git status
git add bot.py bot_features.py admin.py ads.py platforms.py store.py requirements.txt .gitignore README.md
git diff --cached
git commit -m "Describe the change"
git pushAvoid committing generated downloads, runtime JSON, cookies, local environments, or backup copies.
No license is currently declared in this repository. If you plan to distribute DuckLoader as open source, add an explicit license before presenting it as an open-source project.
You bring the link. DuckLoader brings the media. 🦆