streamlens
Live streams in one Telegram message that keeps itself up to date. Hand it a list of streams (or a list of channels) and a bot token; it posts who is live, how many viewers they have, how that changed, and a slideshow of frames from the last 20 minutes for each stream.
- No server, no database. Pure Python; state is one small SQLite file.
- Bring your own data (
Publisher.publish([...])) or let it watch channels for you (streamlens watch). - YouTube, Twitch, Kick out of the box; any other platform if you pass the numbers yourself.
- Safe updates: the new message is sent first, then the old one is deleted; a failed send never empties the chat.
- English and Russian texts, time zone for the footer, size limits of Telegram handled for you.
It grew out of a production Airflow + Postgres pipeline that tracks streams around the clock; this is the same
message format, extracted into something you can pip install.
Quick start
pip install streamlens
You need a bot token from @BotFather and a chat id. Add the bot to your group (or make it an admin of your channel) and check that it can post:
export STREAMLENS_TOKEN="123456:ABC..."
export STREAMLENS_CHAT_ID="-1001234567890" # group / channel id, or your own user id
streamlens check
How to find the chat id: add @RawDataBot to the group, or open
https://api.telegram.org/bot<TOKEN>/getUpdatesafter writing anything to your bot.
1. You have the data
from streamlens import Publisher, Stream
publisher = Publisher(token="123456:ABC...", chat_id="-1001234567890", lang="en", tz="Europe/Berlin")
publisher.publish([
Stream(name="Some Twitch channel", url="https://www.twitch.tv/some_channel", online=12400),
Stream(name="Some YouTube channel", url="https://www.youtube.com/@some_channel/live", online=830,
title="Late night talk", image=open("frame.jpg", "rb").read()),
])
Call publish every ~5 minutes (cron, Airflow, a loop). Only name and url are required; the platform is detected
from the URL. Each call stores the frame you pass in image, so the slideshow now / 5 / 10 / 15 / 20 min ago
builds up by itself. The change in viewers versus ~10 minutes ago is computed from the history the same way.
From the shell, with a JSON or CSV file (or - for stdin):
streamlens send examples/streams.json
cat streams.csv | streamlens send - # columns: name,url,online[,title,image,started_at,...]
streamlens send examples/streams.json --dry-run --chat-id 1 # print instead of sending
2. You only have channel links
pip install "streamlens[frames]" # + ffmpeg on PATH, for YouTube/Kick frames
streamlens watch examples/channels.txt --every 5
channels.txt is one URL per line (Name | URL to set a display name). Every 5 minutes it finds out who is live,
grabs a frame and updates the message.
| Platform | Live status | Viewers | Frame | Notes |
|---|---|---|---|---|
| YouTube | yt-dlp | yes | from the stream (ffmpeg) | use the channel /live URL |
| Twitch | public preview | with a free Twitch app: set TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET |
public preview | works without keys, just no viewer count |
| Kick | yt-dlp | no (Kick has no public count) | from the stream (ffmpeg) | pass your own numbers via Publisher if you have them |
Age-restricted YouTube streams need a cookies.txt of a YouTube account (--cookies); it is used only for those.
Options
Option (Publisher(...) / CLI flag) |
Default | What it does |
|---|---|---|
lang / --lang |
en |
en or ru |
tz / --tz |
UTC |
time zone of the "updated" footer, e.g. Europe/Moscow |
mode / --mode |
auto |
rich (frames), text (plain), auto = rich, falls back to text if the Bot API has no rich messages |
split / --split |
one message | several messages, e.g. --split twitch+kick --split youtube |
sort / --sort |
asc |
asc: the biggest stream ends up at the bottom, next to the input field |
min_online / --min-online |
0 |
hide small streams |
exclude / --exclude |
none | names or URLs that are never shown |
state / --state |
./streamlens.db |
where message ids and frames are kept |
streamlens clear deletes the messages the bot posted. --dry-run works everywhere and never touches Telegram.
Deploy
Anything that runs a command every few minutes works. Pick one.
Long-running process (systemd, tmux, a VPS): streamlens watch channels.txt --every 5
cron: */5 * * * * streamlens watch /path/channels.txt --once (env vars in the crontab or an env file)
Docker:
cp examples/channels.txt channels.local.txt # edit
printf 'STREAMLENS_TOKEN=...\nSTREAMLENS_CHAT_ID=...\n' > .env
docker compose up -d
The image contains ffmpeg and Node.js (the JS runtime that yt-dlp needs for YouTube). State lives in a volume.
Existing pipeline (Airflow, a script): import Publisher and call publish() with your data on your schedule.
Telegram limits it handles for you
- One rich message takes at most 50 pictures. With many streams the frames per stream go 5 → 4 → 3 → 2 → 1, and above 50 streams only the biggest ones get a frame.
- Telegram clients fold long rich messages behind "Show more" after roughly 20 pictures in the visible part. This is a client behaviour; no Bot API setting for it is documented. Measured on real messages: the fewer frames per stream, the more streams stay visible before the fold.
- Bot tokens never appear in error messages or logs.
Development
git clone https://github.com/klipbn/streamlens && cd streamlens
pip install -e ".[dev,frames]"
pytest && ruff check .
MIT © Alexey Voronko. Russian version: README.ru.md.
Metadata
Release files for streamlens 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| streamlens-0.1.0.tar.gz | 22.9 kB | Without provenance | Details for streamlens-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| streamlens-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for streamlens-0.1.0-py3-none-any.whl |
Total release size: 46.1 kB
Release files / streamlens-0.1.0.tar.gz
| Download URL | streamlens-0.1.0.tar.gz |
|---|---|
| Size | 22.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
13393cd232cf6b480ef6545714cb6439a357827c3d2390e9e263ecafae6c79a0
|
|
BLAKE2b-256 checksum How to use checksums |
8984bf5c3c6def817be0d1bff8d25529dc87fbce79a845064746dd3faf0fbbff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.5
|
Release files / streamlens-0.1.0-py3-none-any.whl
| Download URL | streamlens-0.1.0-py3-none-any.whl |
|---|---|
| Size | 23.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3e147dc73bb47b1c3d6e92cd494d9e1dccab0b2d24239fdafce0640537963afa
|
|
BLAKE2b-256 checksum How to use checksums |
4578e99d7926ae57d32e4abac325d73aec4fe0584d5a1ee04d5fc83fd15fafd0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.5
|
Security for streamlens 0.1.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.