This is part 3 of a five-part guide to running Jellyfin on your own server and connecting it to a Telegram bot. Parts 1 and 2 got Jellyfin itself running behind HTTPS. This part is entirely on the Telegram side; the two meet in part 4.
The idea is simple enough that it sounds like it should take an afternoon: message a Telegram bot with a file, the server downloads it, saves it where Jellyfin is watching, and tells Jellyfin to rescan. The first version I wrote did take an afternoon. It also silently failed on anything over 20 MB, which for video is essentially everything. This part is about why, and the fix, before any of it becomes the script in part 4.
Creating the bot
Telegram bots are created by talking to another bot, @BotFather, which is Telegram's own. In the Telegram app:
- Search for
@BotFather, start a chat. - Send
/newbot. - Give it a display name, then a username - has to end in
bot, has to be globally unique. - BotFather replies with a token.
The whole exchange, start to finish, looks like this:
You: /newbot
BotFather: Alright, a new bot. How are we going to call it?
Please choose a name for your bot.
You: Media Server Upload Bot
BotFather: Good. Now let's choose a username for your bot.
It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
You: my_media_upload_bot
BotFather: Done! Congratulations on your new bot. You will find it at
t.me/my_media_upload_bot. You can now add a description, about
section and profile picture for your bot, see /help for a list
of commands.
Use this token to access the HTTP API:
123456789:AAF-fakeExampleTokenDoNotUseThisOne
Keep your token secure and store it safely, it can be used by
anyone to control your bot.
That token is a bearer credential. Anyone who has it can send messages as your bot and read everything sent to it. Treat it like a password - specifically, don't hardcode it into a script you commit to a public repository, which is a mistake worth naming plainly because I've made it myself. Part 4 covers where it actually belongs.
You also want your own numeric Telegram user ID, so the bot only ever acts on messages from you. Message @userinfobot and it replies with your ID immediately:
You: /start
userinfobot: Id: 123456789
First: Your Name
Username: @your_username
Language: en
The Id field is the number part 4's ALLOWED_USER_ID checks against.
Hitting the wall
A minimal bot that saves whatever document you send it, using the popular
python-telegram-bot library, is short:
from telegram.ext import Application, MessageHandler, filters
async def save_file(update, context):
doc = update.message.document
tg_file = await doc.get_file()
await tg_file.download_to_drive(f"/home/ubuntu/files/ott/{doc.file_name}")
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(MessageHandler(filters.Document.ALL, save_file))
app.run_polling()
Send it a 200 MB rip of something and get_file() raises. The error is some
variation of "file is too big," and it is not a bug in the library. Telegram's
standard, cloud-hosted Bot API - the one every bot talks to at
api.telegram.org by default - will not hand a bot a file over 20 MB through
getFile, full stop, regardless of your plan or account type.
The fix: run your own copy of the Bot API server
Telegram publishes the actual server that api.telegram.org runs -
tdlib/telegram-bot-api - as open
source, specifically so you can run your own instance instead of the shared one.
Point your bot at your own server and two things change: the file size ceiling
goes from 20 MB to 2000 MB, and getFile stops returning a URL to download and
starts returning a path on disk, because the file is already sitting right
there on your machine - no second download needed at all.
Standard: bot code ─► api.telegram.org ─► "here's a URL, 20 MB max"
Local: bot code ─► your own server ─► "here's a path on disk, up to 2 GB"
Getting an API ID and hash
Running your own server needs credentials for the underlying Telegram client
protocol, separate from the bot token. Go to
my.telegram.org, log in with your phone number, and
under API development tools, register an application - the name and
description can be anything. You get back an api_id (a number) and an
api_hash (a string). These identify your server to Telegram's network, not
your bot specifically.
Running the server
The easiest path on Ubuntu is the aiogram/telegram-bot-api
Docker image, a prebuilt copy of the official server:
sudo apt install -y docker.io
sudo mkdir -p /home/ubuntu/telegram-bot-api-data
sudo docker run -d \
--name telegram-bot-api \
--restart always \
-p 127.0.0.1:8081:8081 \
-e TELEGRAM_API_ID=<your api_id> \
-e TELEGRAM_API_HASH=<your api_hash> \
-e TELEGRAM_LOCAL=true \
-v /home/ubuntu/telegram-bot-api-data:/var/lib/telegram-bot-api \
aiogram/telegram-bot-api:latest
A few of these flags matter more than they look:
-p 127.0.0.1:8081:8081binds the port to localhost only. Nothing outside the box can reach it. Your bot script, running on the same machine, talks to it over127.0.0.1.TELEGRAM_LOCAL=trueis what switchesgetFilefrom "here's a URL" to "here's a path," and lifts the size cap to 2 GB.-v /home/ubuntu/telegram-bot-api-data:/var/lib/telegram-bot-apiis a bind mount, not a named Docker volume, and that distinction matters here specifically: it means the files the container writes to/var/lib/telegram-bot-apiinside itself land, byte for byte, at/home/ubuntu/telegram-bot-api-dataon the actual host filesystem - a path your Python script, running outside the container, can read directly.
Check it's up:
$ curl http://127.0.0.1:8081
{"ok":false,"error_code":404,"description":"Not Found"}
That 404 is expected and good - it means something is listening and answering in
Telegram's API shape. It just doesn't have a route for a bare /.
Pointing your bot at it
One change to the minimal script from earlier: tell the Application builder to
use your server instead of Telegram's.
from telegram.ext import Application
app = (
Application.builder()
.token("YOUR_BOT_TOKEN")
.base_url("http://127.0.0.1:8081/bot")
.base_file_url("http://127.0.0.1:8081/file/bot")
.build()
)
That's the entire fix. Same bot token, same BotFather setup, same
filters.Document.ALL handler from before - just talking to a server that lives
on your own box instead of Telegram's, with a 2 GB ceiling instead of a 20 MB
one. Part 4 is what the handler actually does with the path it gets back.
What people get wrong
- Registering a second
api_id/api_hashper bot. You don't need to. One registration at my.telegram.org covers your local server regardless of how many bot tokens you point at it. - Exposing port 8081 publicly. It has no authentication of its own - it
trusts whatever's on
127.0.0.1. Bind it to localhost, as above, and never to0.0.0.0. - Expecting
download_to_drive()to still make sense. Against a local server it works, but it re-copies a file that's already sitting on disk one directory over. Part 4 reads the path directly instead, which is faster and is the actual reason to run this server in the first place. - Forgetting the file lives under the bot's token as a folder name. Inside
/home/ubuntu/telegram-bot-api-data, Telegram organizes files per bot token, so the real path is a couple of directories deeper than the mount point. Part 4 shows the exact pattern that shows up in practice.
Next in the series
Part 4 writes the actual handler: taking the path the local server hands back, copying it into Jellyfin's library folder, fixing a permissions issue that will bite you on the first real file, and calling Jellyfin's refresh endpoint so the new file shows up without you lifting a finger.