Skip to main content

Music Downloader Telegram Bot aka scdlbot

Telegram Bot MIT License PyPI Version build Docs

Telegram bot that downloads audio of tracks, sets, albums, videos and playlists from SoundCloud, Bandcamp, YouTube and anything yt-dlp supports, with tags and artwork. Videos from TikTok, Instagram, Twitter and YouTube Shorts are sent as videos (other YouTube videos too, at 480p, with the 🎬 Video button or /video), Instagram and Twitter photos as photos and albums, text-only tweets as text, with the author (Author: Name (@handle), linked to the profile) and the post's text in the caption.

Send /help to @scdlbot to see how to use it, or read the help text. Please report bugs and ideas in issues.

How it works

One Python process with one asyncio loop (python-telegram-bot):

update → handlers.py (parse links, save a job row, enqueue, reply "wait a bit")
           → JobQueue: WORKERS workers
               → yt-dlp subprocess: prints a JSON line for every finished file
               → ffmpeg: re-encode video to h264 if needed, split files bigger than MAX_TG_FILE_SIZE
               → Telegram: send_audio / send_video / albums of up to 10, then delete the file
  • Handlers never wait for downloads, so the bot stays responsive under any load.
  • Downloads run as python -m yt_dlp … processes: each gets killed with all its children on timeout, and the exact command is logged, so you can reproduce any failure in a shell.
  • SQLite (db.py) keeps chat settings, the job queue (jobs resume after a restart) and stats.
  • Audio stays in its original m4a or mp3 when possible (no re-encoding); anything else becomes mp3.
  • Video is h264 in mp4: up to 720p for short videos (TikTok, reels, Shorts), 480p for the rest (YouTube, ~4 MB a minute). Videos without h264 are re-encoded up to 30 minutes long (MAX_REENCODE_DURATION); longer ones are sent as they are.
  • YouTube downloads have their own small worker pool (YOUTUBE_WORKERS): each runs deno (~250 MB) to solve YouTube's JS challenges, so their number is limited without making other sites wait behind them.
Module What it does
config.py all settings, from environment variables
links.py finds links in messages; SITES table of known sites
handlers.py Telegram commands, messages and buttons
jobs.py job queue, workers and the download → send pipeline
ytdlp.py yt-dlp command line, output parsing, error codes, cookies
routes.py routes (proxies, source IPs, cookies): which job goes where, retries, leaving out blocked routes
media.py ffmpeg / ffprobe
sender.py uploads to Telegram, captions, albums
db.py SQLite schema and queries
observe.py Prometheus metrics, systemd notify, owner alerts, Sentry

To support a new site, add a row to links.SITES; for a new command, add a handler in handlers.py and register it in app.py.

Run locally

Requirements: Python 3.14+ (e.g. via pyenv), poetry, mise (installs deno, which yt-dlp needs for YouTube) and ffmpeg.

mise install           # deno
mise run install       # .venv with pyenv's Python and all dependencies
cp .env.sample .env-dev && nano .env-dev   # at least TG_BOT_TOKEN
mise run dev           # run the bot with .env-dev

Other tasks (mise tasks):

mise run fetch https://soundcloud.com/artist/track   # download one link into ./downloads exactly like the bot, without Telegram
mise run fetch --route fin1 https://soundcloud.com/artist/track   # the same through one route (see Routes below)
mise run test
mise run lint
mise run fmt
mise run update        # update dependencies, including yt-dlp's nightly build

Configuration

Environment variables, all documented in .env.sample. Only TG_BOT_TOKEN is required.

Routes

Downloads go out through routes: a proxy or a source IP address of the server, each with its own cookies. They are set in a TOML file (ROUTES_FILE, see deploy/routes.sample.toml), which also says which sites use which routes, e.g. YouTube only through the route with its logged-in account. systemctl reload scdlbot applies changes to the file without a restart. Without the file, PROXIES, SOURCE_IPS and COOKIES_FILE make the routes, like in earlier versions.

  • Each site takes its routes in turn, and one download makes all its requests through one route.
  • If a site blocks a download (bot check, HTTP 403/429) or the route can't connect, it's tried once more through another route.
  • A route that a site keeps blocking is left out for that site for ROUTE_EXCLUSION seconds (2 hours), and the owner is told; if all routes of a site are left out, all of them are used anyway.
  • Downloads that a site gives only to logged-in users (Instagram's age-restricted reels, stories) go once more through the site's login routes ([login] in the routes file): routes with an account's cookies that anonymous downloads never use, so the account only sees the few downloads that need it. Instagram stories, which Instagram never shows anonymously, go straight to the login routes. When the last 8 downloads through a login route all failed that way too, its account is probably logged out: the route is left out for a day (or until a reload) and the owner is told. tools/instagram_cookies.py turns Instagram accounts bought "with cookies" into cookies.txt files.
  • Metrics: scdlbot_route_attempts_total{site,route,result}, scdlbot_route_retries_total, scdlbot_route_up{site,route}, and a route label on scdlbot_jobs_total and scdlbot_download_errors_total.
  • scdlbot fetch --route NAME URL downloads through one route, to check it.

Source links: every sent file's caption starts with an invisible link to where it came from (HIDDEN_SOURCE_LINKS), in the format Telegram asked big downloader bots to use in February 2026 (leak); Telegram then shows those files in link previews of the same URL (leak). Links in captions are cleaned up (NORMALIZE_URLS): yt-dlp's canonical link of the post or track, without share tracking (?si=, ?igsh=, TikTok's u_code…) and with short links expanded.

In @BotFather: set the commands from telegram_settings/commands.txt, and disable privacy mode if the bot should react to plain messages with links in groups (otherwise it only sees commands).

Deploy

Server

What @scdlbot runs on, as a reference (November 2026):

Load ~16,000 downloads a day for ~49,000 monthly users; busiest in the evening (UTC)
Server Hetzner CPX22: 2 vCPU (AMD EPYC), 4 GB RAM, no swap, Ubuntu 22.04, plus a 50 GB volume for DL_DIR
Settings WORKERS=8, YOUTUBE_WORKERS=4, a local Bot API server, systemd limits of 1.8 CPU and 2.3 GB RAM
CPU ~3 CPU-seconds per download: 0.7 cores on average, up to 1.3 in the busiest hour. YouTube (deno) and video re-encodes cost the most
RAM the bot with its downloads: ~0.5 GB usually, up to 2.2 GB at peaks (each YouTube download runs deno, ~250 MB); the Bot API server: up to 0.4 GB
Disk the database: ~100 MB with 90 days of downloads. DL_DIR holds only files being downloaded and sent: usually a few GB, but leave room for WORKERS files of MAX_CONVERT_FILE_SIZE
Traffic every file is downloaded once and uploaded once: ~240 GB a day each way

For a personal bot or a few chats, 1 vCPU and 1 GB of RAM (2 GB with a local Bot API server) should do with WORKERS=2 and YOUTUBE_WORKERS=1.

The server needs:

  • Linux. With systemd, the bot uses its watchdog (see below); anywhere else, use Docker.
  • Python 3.14 or 3.15, e.g. from pyenv (that's what @scdlbot uses) or the deadsnakes PPA on Ubuntu.
  • ffmpeg and ffprobe, and deno, the JS runtime yt-dlp needs for YouTube (or node with YTDLP_ARGS="--js-runtimes node"), in the PATH of the service. To put the latest builds into /usr/local/bin:
# ffmpeg and ffprobe: yt-dlp's builds, with its patches (for ARM servers: linuxarm64 instead of linux64)
curl -fsSL https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz \
  | sudo tar -xJ -C /usr/local/bin --strip-components=2 --wildcards '*/bin/ffmpeg' '*/bin/ffprobe'
# deno (for ARM servers: aarch64 instead of x86_64)
curl -fsSLo /tmp/deno.zip https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip \
  && sudo unzip -o /tmp/deno.zip -d /usr/local/bin && rm /tmp/deno.zip
ffmpeg -version | head -n 1 && ffprobe -version | head -n 1 && deno --version   # check

Your distribution's ffmpeg (sudo apt install ffmpeg) works too. Run the same commands again to update them.

systemd

sudo git clone https://github.com/gpchelkin/scdlbot /opt/scdlbot && cd /opt/scdlbot   # the service file and .env.sample
sudo python3.15 -m venv .venv && sudo .venv/bin/pip install scdlbot   # the latest release from PyPI, or `pip install .` for this checkout
sudo cp .env.sample /etc/default/scdlbot && sudo nano /etc/default/scdlbot
sudo cp deploy/scdlbot.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now scdlbot

The service uses Type=notify with a watchdog: the bot pings it only while its workers make progress, so systemd restarts it if it ever gets stuck. Restarts are safe: unfinished downloads start again after them. Check its resource limits (CPUQuota, MemoryMax) against your server.

For big files (up to 2000 MB) and faster uploads run a local Bot API server on the same machine (deploy/telegram-bot-api.service) and set TG_BOT_API="http://127.0.0.1:8081".

Updates:

sudo /opt/scdlbot/.venv/bin/pip install -U scdlbot && sudo systemctl restart scdlbot
# yt-dlp: sites change all the time, and fixes reach yt-dlp's nightly builds days or weeks before its releases
sudo /opt/scdlbot/.venv/bin/pip install -U --pre "yt-dlp[default,curl-cffi]" && sudo systemctl restart scdlbot

Docker

docker build -t scdlbot .
docker run -d --name scdlbot --restart unless-stopped --env-file .env -v scdlbot-data:/data -p 127.0.0.1:8000:8000 scdlbot

The image has ffmpeg, deno and the yt-dlp nightly build from poetry.lock.

Monitoring and stats

  • Prometheus metrics at http://METRICS_HOST:METRICS_PORT/metrics: scdlbot_jobs_total{site,kind,status}, scdlbot_job_duration_seconds, scdlbot_queue_size, scdlbot_workers_busy, scdlbot_download_errors_total{code}, scdlbot_rejected_total{reason}, scdlbot_active_users{period}, scdlbot_active_users_by_language{lang} and more, plus CPU/memory of the bot's main process.
  • /stats in Telegram (owner only): active users, jobs by status, top sites, app languages. Telegram doesn't tell bots users' countries; the language of their Telegram app is the closest thing.
  • Errors (unexpected exceptions only) go to TG_BOT_OWNER_CHAT_ID, at most one message per minute, and to SENTRY_DSN if set.
  • When a site starts failing (e.g. YouTube logged out, SoundCloud blocked the IP), TG_BOT_OWNER_CHAT_ID gets a message, and another one when it recovers (ALERT_* settings in .env.sample).
  • Logs go to stdout (journalctl -u scdlbot). Use LOGLEVEL=DEBUG to see yt-dlp output.
  • Or just ask SQLite:
sqlite3 scdlbot.sqlite3 "SELECT day, sum(jobs) FROM daily GROUP BY day ORDER BY day DESC LIMIT 14"
sqlite3 scdlbot.sqlite3 "SELECT site, status, sum(jobs) FROM daily WHERE day >= date('now', '-7 day') GROUP BY 1, 2 ORDER BY 3 DESC"
sqlite3 scdlbot.sqlite3 "SELECT count(DISTINCT user_id) FROM user_days WHERE day > date('now', '-30 day')"   # MAU
sqlite3 scdlbot.sqlite3 "SELECT error, count(*) FROM jobs WHERE created_at > strftime('%s', 'now', '-1 day') GROUP BY 1"

The database stores Telegram user and chat ids and downloaded URLs; per-download rows are deleted after JOBS_RETENTION_DAYS. PRIVACY.md is the privacy policy of @scdlbot only. If you run your own bot, write your own (Telegram requires one, see §4 of its terms for bot developers) and set its URL in PRIVACY_POLICY_URL (for /privacy and /help) and in @BotFather. Until you do, Telegram's Standard Privacy Policy for bots applies to your bot, and /privacy links to it.

History

The name tells where it started: scdlbot was a SoundCloud downloader bot built on scdl. Its first version, from January 2016, was a 70-line script that ran scdl --onlymp3 for every link it got and sent the mp3 back. In 2017 it learned Bandcamp with bandcamp-dl, and YouTube and many more sites with youtube-dl, replaced by its fork yt-dlp in 2021. In 2026 the bot was rewritten from scratch, and yt-dlp now does all the downloading.

Many thanks to the authors of scdl, bandcamp-dl, youtube-dl and yt-dlp: this bot wouldn't exist without them.

Disclaimer

The software. scdlbot is open-source software under the MIT License, provided "as is", without warranty of any kind. Like yt-dlp that it's built on, it's a tool:

  • It only processes links that its users send it: it doesn't search, suggest or host anything.
  • It doesn't circumvent DRM: DRM-protected tracks (e.g. SoundCloud Go+) are refused. Like yt-dlp, it doesn't support sites that serve only DRM-protected content or exist for copyright infringement (yt-dlp's policy).
  • It doesn't keep files: the server deletes each file right after sending it to Telegram, so nothing is stored long-term.

Instances. Anyone can run their own bot with this code. Whoever runs a bot is its operator and is solely responsible for it: its settings and changes to the code, how it's used, following the law, Telegram's terms and the terms of the sites it downloads from, and its privacy policy (see Monitoring and stats). The creator of scdlbot doesn't run, control or answer for any bot other than @scdlbot.

@scdlbot. @scdlbot is the bot run by its creator, George Pchelkin (@gpchelkin), under its own Privacy Policy:

  • It only downloads what anyone can stream for free on the site, in a web browser: no paid subscriptions, paywalls or premium content.
  • It's meant for personal use. Users are responsible for respecting the rights of creators and the terms of the sites they download from.
  • Rights holders: write to @gpchelkin. Downloads from specific sites or in specific chats can be blocked right away, and blocking specific links, channels or accounts can easily be added on request.

Metadata

Release files for scdlbot 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for scdlbot 1.0.0
File Size Uploaded Provenance Actions
scdlbot-1.0.0.tar.gz 62.8 kB With provenance Details for scdlbot-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for scdlbot 1.0.0
File Interpreter ABI Platform Actions
scdlbot-1.0.0-py3-none-any.whl Python 3 none any Details for scdlbot-1.0.0-py3-none-any.whl

Total release size: 129.1 kB

Release files / scdlbot-1.0.0.tar.gz

Download URL scdlbot-1.0.0.tar.gz
Size 62.8 kB
Tags Source
SHA-256 checksum
How to use checksums
2322d661730d0bd019becaf99ea38e4368d4c957846b0a96be5cc1ad90254b4e
BLAKE2b-256 checksum
How to use checksums
ae780a7eb76871ff818d983dcb484b46d91c29ae0a6469a06eabba887fe6ace9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Release files / scdlbot-1.0.0-py3-none-any.whl

Download URL scdlbot-1.0.0-py3-none-any.whl
Size 66.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7b703be4ff3f2bc794d30dd46b07217e5c6f3929822b09cd643f54dfa80ee29b
BLAKE2b-256 checksum
How to use checksums
fb103c0cacf34228f4a2078583c4b8d4dcd961582597d850f85330b6caa5e0d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Security for scdlbot 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for scdlbot-1.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page