Skip to content

About

Self-hosted Telegram bot that converts YouTube links into tagged MP3/M4A audio or video, with caching, a download queue and Docker deployment.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

mp3-downloader-bot

A self-hosted Telegram bot that turns YouTube links into tagged audio files with embedded album art. Send it a link, get back an M4A (or MP3, or the video).

Runs in Docker. Designed to keep working when YouTube changes its extraction defences — which it does every few months.

you  ▸ https://youtu.be/dQw4w9WgXcQ
bot  ▸ ⏳ Fetching audio…  ██████░░░░ 62%
bot  ▸ 🎵 [audio file, tagged, with cover art]

Contents


Features

  • M4A by default — copied straight from YouTube's audio stream, no re-encode, so it's faster and smaller than transcoding to MP3. /mp3 if you want MP3.
  • Video downloads — /video <url>, up to 720p. If the result is too big to send, it's automatically downscaled (480p → 360p → 240p) until it fits.
  • Clipping — append timestamps to grab a section: <url> 1:23 4:56.
  • Playlists / albums — up to 50 videos, with a confirmation step first.
  • Metadata + cover art — ID3 / MP4 tags with title, artist, album and embedded artwork, so files land correctly in Apple Music and friends.
  • Queue — up to 5 concurrent downloads, with live position and wait estimates when the queue is busy.
  • Caching — a video already fetched is re-sent instantly. Local cache with a 24-hour TTL and automatic sweeping of stale fragments.
  • Access control — allowlist, admin roles, per-user rate limiting.
  • Operations — health checks, maintenance mode, structured logs, admin error alerts, and a watchdog that kills hung jobs instead of leaving users staring at a spinner.

Requirements

  • Docker and Docker Compose
  • ~2 GB RAM and a few GB of disk for the cache
  • A Telegram bot token from @BotFather

Everything else — yt-dlp, ffmpeg, Deno, the PO-token provider — is installed inside the image. Nothing to set up on the host.

Quick start

git clone <your-fork-url> mp3-downloader-bot
cd mp3-downloader-bot

cp .env.example .env
# Edit .env: set TELEGRAM_BOT_TOKEN, and set ADMIN_USER_IDS to your own
# Telegram user ID (get it from @userinfobot).

# The compose file bind-mounts this file. It must exist, even if empty —
# otherwise Docker helpfully creates a *directory* with that name and yt-dlp
# will fail in a confusing way. See "Cookies" below for when to fill it in.
touch cookies.txt

docker compose up -d --build
docker compose logs -f yt-bot

Then message your bot on Telegram and send it a YouTube link.

Set ALLOWED_USER_IDS before you share the bot's username. If it's empty the bot is open to anyone who finds it, and you'll be running a public YouTube ripper on your own bandwidth and IP address.

Configuration

All configuration is environment variables, read from .env. See .env.example for the annotated full list.

Required

Variable Description
TELEGRAM_BOT_TOKEN Bot token from @BotFather

Access control

Variable Default Description
ADMIN_USER_IDS (empty) Comma-separated user IDs with admin rights. Admins bypass the allowlist and maintenance mode, and receive error alerts.
ALLOWED_USER_IDS (empty) Comma-separated user IDs permitted to use the bot. Empty means everyone.
RATE_LIMIT_REQUESTS 10 Requests allowed per user per period
RATE_LIMIT_PERIOD 3600 Period in seconds
MAINTENANCE_MODE false When true, only admins can use the bot

Find your numeric Telegram user ID with @userinfobot. (The bot's own /userid command is admin-only, so it can't help you bootstrap the first admin.)

Uploads and timeouts

Variable Default Description
TELEGRAM_LOCAL_API_URL (empty) Set to http://yt-bot-api:8081 to use the local Bot API sidecar. See Large files.
TELEGRAM_API_ID / TELEGRAM_API_HASH (empty) Required only for the local Bot API sidecar. From my.telegram.org/apps.
MAX_UPLOAD_MB 48 / 1950 Upload ceiling. Defaults to 48 on the public API, 1950 with the local one.
PIPELINE_TIMEOUT_AUDIO 300 Seconds before a stuck audio job is killed
PIPELINE_TIMEOUT_VIDEO 900 Seconds before a stuck video job is killed
LOG_LEVEL INFO DEBUG, INFO, WARNING, ERROR

Commands

Command Who What
(just send a link) everyone Audio as M4A
/mp3 <url> everyone Audio as MP3
/m4a <url>, /opus <url> everyone Audio as M4A (explicit)
/video <url> everyone Video file, up to 720p
/start everyone Welcome message and feature list
/status admin Cache size, active downloads, queue depth, request stats
/maintenance admin Toggle maintenance mode
/userid admin Show your user ID

Clipping. Every command above accepts trailing timestamps to cut a section:

https://youtu.be/VIDEO_ID 1:23 4:56
https://youtu.be/VIDEO_ID 1:23-4:56
/video https://youtu.be/VIDEO_ID 0:30 1:00

Playlists. Send a playlist URL and the bot shows a confirmation with the track count before downloading. Playlists are capped at 50 videos; longer ones are truncated.

Large files (>50 MB)

Telegram's public Bot API caps bot uploads at 50 MB. The bot handles this in two ways.

By default, when a file exceeds the cap, the bot offers a choice: one compressed file that fits, or the original quality split into several parts.

Or run your own Bot API server, which raises the ceiling to ~2 GB. It's included as an opt-in Compose profile:

  1. Get TELEGRAM_API_ID and TELEGRAM_API_HASH from my.telegram.org/apps.
  2. Add to .env:
    TELEGRAM_API_ID=...
    TELEGRAM_API_HASH=...
    TELEGRAM_LOCAL_API_URL=http://yt-bot-api:8081
    
  3. Detach the bot from Telegram's cloud servers — a one-time, irreversible-ish step, so read the upstream note first:
    curl -X POST "https://api.telegram.org/bot<YOUR_TOKEN>/logOut"
  4. Start with the profile enabled:
    docker compose --profile local-api up -d --build

To go back to the cloud API, call logOut against the local server, then stop the profile.

How it works

Three containers:

Service Role
yt-bot The bot itself. Python, python-telegram-bot, yt-dlp, ffmpeg.
bgutil-provider Mints Proof-of-Origin tokens. YouTube returns 403 on stream URLs without one. Internal network only, no published ports.
telegram-bot-api Optional local Bot API server (local-api profile only).

A request flows: URL parsed and validated → cache checked → queued if all five download slots are busy → yt-dlp fetches the stream → ffmpeg converts or clips if needed → tags and cover art embedded → uploaded to Telegram → cached.

On YouTube's defences. Extraction is the fragile part of any tool like this, and two things in the image exist purely to keep it working:

  • Deno is bundled as yt-dlp's JavaScript runtime. YouTube obfuscates stream URLs behind a JS challenge; without a runtime to solve it, downloads come back throttled or 403.
  • PO tokens from the bgutil-provider sidecar, wired up through /etc/yt-dlp.conf.

yt-dlp.conf also pins the player client to web_embedded, which as of this writing is the one that reliably serves plain-HTTPS stream URLs. The trade-off: videos with embedding disabled can't be downloaded through it. Both of these are moving targets — expect to revisit them when YouTube next changes something.

Cookies

cookies.txt (Netscape cookie-jar format) is bind-mounted into the container and passed to yt-dlp if it's non-empty. You only need it for age-restricted or otherwise login-gated videos.

Use a throwaway Google account. The file is a live session for whatever account you export it from — anyone who gets it is logged in as you. It's in .gitignore for that reason; keep it that way.

Keeping it working

yt-dlp is frozen into the image at build time, and YouTube breaks older versions every few months. The fix is to rebuild on a schedule, not to wait for it to break.

scripts/rebuild-image does this safely: pulls main, rebuilds with --no-cache, waits for the container to report healthy, then runs an actual download as a smoke test — because the health check validates dependencies and connectivity, not extraction, and passes happily while downloads are broken. Only once both gates pass does it prune old layers, so until then the previous image is still there to roll back to.

Install it as a monthly cron job:

30 4 1 * * /path/to/yt-bot/scripts/rebuild-image >> /var/log/yt-bot-rebuild.log 2>&1

For a quick fix without a rebuild, scripts/update-yt-dlp.sh upgrades yt-dlp inside the running container. It's a stopgap — the change is lost on the next restart.

Troubleshooting

docker compose logs -f yt-bot                    # follow logs
docker compose exec yt-bot python healthcheck.py # env, deps, API, cache, disk
docker compose exec yt-bot yt-dlp --version      # what version is actually running
Symptom Likely cause
Every download fails with 403 or "unable to extract" YouTube changed something. Rebuild the image (scripts/rebuild-image) to pick up a current yt-dlp.
One specific video fails, others work Often embedding-disabled — the web_embedded player client can't reach it.
"File must be non-empty" on send The local Bot API server can't see the cache volume. Check that both containers mount yt-bot-cache.
Bot starts then immediately exits Usually a missing TELEGRAM_BOT_TOKEN. The logs say which variable.
cookies.txt errors, or it appears as a folder Docker created a directory because the file didn't exist. rm -rf cookies.txt && touch cookies.txt, then recreate the container.
Cache growing without bound Sweeps run automatically, but docker compose exec yt-bot find /app/cache -name "*.mp3" -mtime +7 -delete clears it out by hand.

Security notes

  • Runs as non-root. The entrypoint fixes volume permissions as root, then drops to the unprivileged ytbot user via gosu. no-new-privileges is set.
  • Input validation. URLs are matched against strict patterns and reduced to an 11-character video ID; every cache path is built from that ID, so a crafted title or URL can't escape the cache directory.
  • No shell interpolation. Every yt-dlp and ffmpeg call uses create_subprocess_exec with an argument list — no shell=True anywhere.
  • Secrets stay in .env, which is gitignored, as are cookies.txt and the Bot API session data.
  • The bot is as open as you leave it. ALLOWED_USER_IDS is the only thing standing between your server and the public; rate limiting is per-user and won't save you from a crowd.

Optional extras

Not required to run the bot:

  • scripts/s3-sync — standalone Bash utility to sync the cache to an S3-compatible bucket (Cloudflare R2), with hand-rolled SigV4 signing. The bot does not call it; wire it into cron yourself if you want off-box backups.
  • scripts/creds — small GPG helper to keep an encrypted copy of your .env (creds encrypt / creds decrypt). If you use it, keep the resulting .gpg file out of a public repo: it's a passphrase-encrypted blob, and publishing one invites offline cracking.

Built with yt-dlp, python-telegram-bot, and ffmpeg.

Licensed under the MIT License.

You are responsible for respecting YouTube's Terms of Service and copyright law wherever you run this.

About

Self-hosted Telegram bot that converts YouTube links into tagged MP3/M4A audio or video, with caching, a download queue and Docker deployment.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages