An invite-only, self-hosted Telegram downloader for video, audio, social posts, albums, playlists, and batches.
Status: early 0.1.x self-hosted releases; provider compatibility can change with upstream services.
Features Β· Sources Β· Deployment Β· Usage Β· Security
Media Pocket turns Telegram into a private media inbox. Send one link or a batch; the bot resolves the provider, chooses the natural format, shows live progress, and returns the finished media.
- Natural by default. Video stays video, audio-first sources stay audio, and YouTube follows each user's saved preference.
- Clean in Telegram. Media carries no caption or source URL, non-audio filenames are randomized, and actions arrive separately.
- Built for collections. Single links, batches, albums, and playlists share the same durable worker pipeline.
- Private at every entry point. Invitations protect chats, groups, Business messages, callbacks, and inline mode.
The bot and workers run as separate processes. PostgreSQL stores canonical state, Redis Streams carries jobs and progress, and shared artifact storage connects downloading to Telegram delivery.
Warning
Media Pocket does not bypass provider access controls. Private, deleted, region-restricted, or authentication-gated content may require valid cookies or may remain unavailable. Operators are responsible for complying with provider rules and local law.
| Category | Sources | Available handling |
|---|---|---|
| Video | YouTube | video, audio, playlists, timestamped-album splitting, automatic compatible formats, per-user mode |
| Social | TikTok, Instagram, X / Twitter, Threads, Pinterest | media-first delivery, audio extraction, files, provider collections |
| Audio | Spotify, SoundCloud, Zaycev.net, HitMoz | tracks, collections where supported, native Spotify audio or fallback |
| Direct media | HTTP(S) audio/video URLs, direct 2ch video files | direct download; known 2ch mirrors retry automatically |
| Generic | Other HTTP(S) links | yt-dlp resolution where supported |
Direct media detection uses the URL path extension, including common formats such as MP4, WebM, MP3, M4A, Ogg, Opus, WAV, and FLAC. For 2ch, this applies to direct video-file URLs rather than thread pages.
Video sources can offer Video or Audio and Media or File delivery where those choices apply. Media Pocket automatically selects the best practical source format and prefers native Telegram playback over expensive transcoding. File delivery preserves the downloaded source whenever possible. Audio-first providers expose only relevant controls.
For in-chat playback, Media Pocket prefers source H.264/AAC video and M4A/AAC or MP3 audio. It remuxes compatible streams without re-encoding, converts only an incompatible stream when possible, and falls back to full conversion only when Telegram needs it. Audio includes title, performer, duration, and thumbnail metadata when the provider supplies it; Spotify cover art is also embedded in the prepared audio file.
- Resolve β identify the provider and apply its natural mode or the user's YouTube preference.
- Queue β persist a job snapshot and atomically enqueue it for a worker.
- Process β update one status message with queue position, progress, speed, size, and ETA when available.
- Deliver β send only the file or media group, remove the technical status after success, and keep optional actions in a separate message.
Ordinary links start immediately. YouTube can be configured per user as Video, Audio, or Always ask; only Always ask opens the 15-minute format card. A YouTube URL containing a playlist always asks whether to download the current video or the entire playlist before anything is queued. Explicit /audio URL, /video URL, !a, !audio, !mp3, and !music still override the saved format, but they do not bypass the playlist-scope choice.
A single YouTube video with at least two usable description timestamps pauses before download and offers Split into tracks or the ordinary whole-video/audio path. Splitting downloads the audio once, cuts it at the detected boundaries without re-encoding compatible audio, preserves track order, titles, performer, durations, and cover art, then delivers the tracks in Telegram media groups.
For batches, each link keeps its provider-native mode. If a batch contains YouTube and Always ask is enabled, one choice applies to the batch, with the nearest valid fallback for incompatible items.
Failures remain visible as a compact card with a human-readable reason, a next step, retry, format change, and help actions. Tracebacks, provider internals, and internal error codes are never shown to users.
- a Linux host with Docker Engine and the Docker Compose plugin;
- Git;
- a Telegram bot token from @BotFather;
- Telegram API credentials from my.telegram.org;
- enough CPU, memory, storage, and network capacity for FFmpeg and the intended worker concurrency.
Spotify Premium and provider cookies are optional and only needed for the corresponding provider paths.
Create the bot with @BotFather and keep its token private.
For inline downloads, run /setinline, then /setinlinefeedback and choose Enabled. Media Pocket confirms an inline download from Telegram's chosen-result update, so sampled feedback modes such as 1/10 or 1/100 are not suitable.
If the bot should receive ordinary links in groups without being mentioned, adjust its group privacy setting in BotFather. The bundled local Telegram Bot API service requires TELEGRAM_API_ID and TELEGRAM_API_HASH.
git clone --recurse-submodules --branch v0.1.0 \
https://github.com/segfault-stack/media-pocket.git
cd media-pocket
cp .env.example .env
cp docker-compose.example.yml docker-compose.ymlSet the four required values in .env:
BOT_TOKEN=replace-with-bot-token
POSTGRES_PASSWORD=replace-with-a-long-random-password
TELEGRAM_API_ID=replace-with-api-id
TELEGRAM_API_HASH=replace-with-api-hashdocker compose build
docker compose up -d postgres redis telegram-bot-api
docker compose run --rm migrateThe bot refuses to start until PostgreSQL contains at least one administrator:
docker compose run --rm migrate \
python -m downloader_bot admin add YOUR_TELEGRAM_USER_IDStart the bot and one or more workers:
docker compose up -d --scale worker=2
docker compose psThe image contains the locked Python environment and media toolchain. Compose mounts the checkout at /app read-only and provides separate writable volumes for downloads, logs, cookies, and Spotify state. Restart the bot and workers after source changes; rebuild only when dependencies or the toolchain change.
After startup, docker compose ps should show PostgreSQL and Redis as healthy and the bot and workers as running. If a process exits, inspect a bounded log tail with docker compose logs --tail=100 bot worker; review logs before sharing them because provider URLs and user activity may be sensitive.
PostgreSQL is canonical state. Before changing versions or running new migrations, stop new work and create a logical backup at a new host path:
docker compose stop bot worker
set -C
docker compose exec -T postgres \
pg_dump -U bot_user -d downloader_bot --format=custom \
> media-pocket-backup.dump
test -s media-pocket-backup.dumpset -C prevents accidentally overwriting an existing backup in that shell. Store the resulting dump outside the checkout and test restoration against an isolated PostgreSQL instance before relying on it.
For an update, review an immutable release tag from a clean checkout, initialize its pinned submodules, rebuild, run migrations once, and then restart the stack. Source rollback is safe only when the older revision supports the migrated database schema. Otherwise, restore the verified pre-update backup into isolated state; do not delete or replace the existing PostgreSQL, Redis, Telegram Bot API, download, or Spotify volumes as a rollback shortcut.
Open the bot, run /admin, create an invitation code, and redeem it from each account that should have access. Then send a supported link or several links in one message.
| Command | Purpose |
|---|---|
/start |
onboarding, inline shortcut, settings, help, group installation, and sharing |
/help |
platforms, formats, batches, inline mode, and limitations |
/settings |
YouTube mode, delivery, result actions, cleanup, and progress detail |
/status |
the current user's active and recent jobs, cancellation, and retry |
/audio URL |
download immediately as audio |
/video URL |
download immediately as video |
/redeem CODE |
unlock access with an invitation |
/admin |
create, list, share, and revoke invitations |
@your_bot URL |
submit inline from any chat after access is granted |
Single results can expose contextual actions such as Get audio, Get video, Send as file, Source, and Share. Albums are delivered as media groups followed by one compact action card.
At least one administrator must be created from the command line before the bot starts. Administrators can generate:
- single-use invites β one redemption, no expiry;
- limited-use invites β any configured limit from 2 to 100,000 redemptions, no expiry;
- timed invites β multiple redemptions until the configured expiry.
Users send the bare code or /redeem CODE. A successful redemption grants access until the database record is changed by the operator.
Inline mode is not a side door: unauthorized users only receive a prompt to open the private chat and enter a code. Callback ownership is checked as well, so another user cannot confirm, cancel, or retry someone else's request.
YouTube compatibility stack
The image pins yt-dlp with its default extras, the recommended curl-cffi browser-impersonation transport, yt-dlp EJS challenge scripts, Deno, and FFmpeg/ffprobe.
Compose starts the matching Deno-based BgUtils PO-token provider. The Python plugin connects yt-dlp to that private sidecar, and YouTube extraction uses the mweb client so GVS PO tokens are requested automatically. YOUTUBE_POT_PROVIDER_URL can override the internal endpoint.
This is compatibility plumbing, not an access-control bypass. Cookies may still be required for account-gated material, rate limits still apply, and YouTube can change extraction requirements without notice.
Spotify Premium audio
The image builds a pinned librespot-based spotify-streamer helper from the included Git submodule. With a deployment-owned Spotify Premium session, tracks, albums, and playlists use native Spotify audio first. If authentication is missing or an individual native stream fails, Media Pocket falls back to its YouTube search path.
Authenticate without placing a browser on the server:
SPOTIFY_SSH_HOST=example.com SPOTIFY_SSH_PORT=2222 scripts/spotify authRun the printed SSH tunnel command on your computer, keep it open, and visit the displayed Spotify URL locally. The callback remains bound to the server's 127.0.0.1.
Useful operator commands:
scripts/spotify check
scripts/spotify resolve https://open.spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6
scripts/spotify reset-authSpotify credentials are stored in PostgreSQL. The librespot cache lives in the spotify_cache volume. SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET are optional and support collection expansion for the fallback path; they are not Premium account credentials.
Provider cookies and Media Cookie Broker
yt-dlp can read a combined Netscape cookie jar or provider-specific files from cookies/. Treat every cookie file as a password and never commit it.
The optional cookie-broker Compose profile can run a separately configured cookie-sync image and write refreshed cookie jars into the shared directory:
Configure COOKIE_BROKER_IMAGE, BROKER_URL, BROKER_USERNAME, and secrets/cookie-broker-password first, then run:
sudo chown -R 1000:1000 cookies secrets/cookie-broker-password
sudo chmod 700 cookies
sudo chmod 600 secrets/cookie-broker-password
docker compose --profile cookie-broker up -d cookie-syncThe bot, workers, and cookie-sync share UID/GID 1000:1000 for these deployment-owned files. Keep the secret readable only by that owner.
Media Cookie Broker is a related but separately deployed project. Media Pocket does not bundle it; its image, endpoint, credentials, and network policy remain operator choices.
Media Pocket handles bot tokens, provider sessions, browser cookies, and user-submitted URLs.
- Never commit
.env,secrets/, cookie jars, Spotify sessions, database dumps, or downloaded media. - Use a long random PostgreSQL password.
- Keep the local Telegram Bot API, PostgreSQL, Redis, and the PO-token sidecar off the public internet.
- Expose a cookie broker only through a protected network path.
- Give invitations only to people you trust.
- Match download-size and worker-concurrency limits to the host.
- Rotate a credential immediately if it appears in logs, chat, Git, or an image layer.
The provided .gitignore and .dockerignore block common credential and artifact paths, but they do not replace operator review. CI runs secret and dependency scans on every push and pull request, plus a weekly vulnerability rescan.
Report vulnerabilities privately through the security policy. Never place credentials, cookies, private URLs, user data, or raw production logs in a public issue.
Runtime configuration
| Variable | Purpose |
|---|---|
BOT_TOKEN |
Telegram bot credential |
POSTGRES_PASSWORD |
PostgreSQL password used by the stack |
TELEGRAM_API_ID |
local Telegram Bot API application ID |
TELEGRAM_API_HASH |
local Telegram Bot API application hash |
| Variable | Purpose | Default |
|---|---|---|
DATABASE_URL |
direct process execution; Compose sets it automatically | required outside Compose |
REDIS_URL |
Redis connection URL | redis://redis:6379/0 |
CUSTOM_API_URL |
Telegram Bot API endpoint | https://api.telegram.org in code |
YOUTUBE_POT_PROVIDER_URL |
BgUtils PO-token provider endpoint | http://youtube-pot-provider:4416 in Compose |
ARTIFACT_ROOT |
shared download directory | downloads |
DOWNLOAD_MAX_FILE_SIZE |
maximum artifact size in bytes | 2000000000 |
MAX_PARALLEL_DOWNLOADS |
concurrent downloads per worker | 4 |
YTDLP_RESOLVE_TIMEOUT_SECONDS |
maximum yt-dlp metadata resolution time | 120 |
ARTIFACT_RETENTION_SECONDS |
completed artifact retention | 86400 |
UX_SELECTION_FLOW |
honor YouTube's Always ask picker | true |
SPOTIFY_BITRATE |
native Spotify bitrate: 96, 160, or 320 | 320 |
.env.example is the safe Compose starting point. Complete runtime defaults live in Settings.
Architecture
Dependencies point inward:
bootstrap β adapters / infrastructure β application β domain
downloader_bot/domainβ immutable models and the job state machine;downloader_bot/applicationβ use cases and typed ports;downloader_bot/adapters/telegramβ aiogram routing, presentation, callbacks, and delivery;downloader_bot/infrastructureβ PostgreSQL, Redis Streams, providers, downloads, and artifacts;downloader_bot/bootstrapβ settings, dependency assembly, and process lifecycle.
PostgreSQL is canonical state. Redis Streams carries work and progress. Selection requests survive restarts, confirmation is atomic, active jobs are deduplicated, pending Redis messages can be reclaimed, and completed artifacts are retained for a bounded period.
See architecture details, the code map, and the behavior inventory.
From the repository root, install uv, initialize the pinned submodules, and create the locked Python 3.14 environment:
git submodule update --init --recursive
uv sync --locked
uv run ruff check downloader_bot acceptance_tests main.py
uv run ty check
uv run python -m compileall -q downloader_bot main.py
uv run pytest --cov=downloader_bot --cov-fail-under=80Install and run the local quality gate:
uv run prek install
uv run prek run --all-files
scripts/security-check| Tool | Role |
|---|---|
| uv | Python and dependency locking |
| Ruff | linting and formatting checks |
| ty | static type checking |
| prek | local pre-commit orchestration |
| pytest | network-free acceptance tests and coverage |
| Gitleaks / Trivy | secret, dependency, and deployment scans |
uv.lock is shared by local development and the runtime image. The acceptance suite mocks Telegram, providers, PostgreSQL repositories, and Redis Streams. Production-code coverage must remain at least 80%.
- Self-hosted deployment only; there is no hosted service.
- There is no web administration panel.
- Provider availability can change when upstream sites change.
- Private or authentication-gated media needs valid operator-supplied cookies and may still fail.
- Native Spotify audio needs a deployment-owned Premium session.
- Telegram and local storage limits still apply.
- Cancellation is cooperative and may not interrupt every provider operation instantly.
- The bot does not automate login, CAPTCHA, 2FA, or provider account recovery.
- The repository is published for inspection and self-hosting; there is no formal support response time, compatibility lifetime, or contribution program.
One link in. Clean media out.
MIT β see LICENCE.