Metadata-Version: 2.5
Name: streamlens
Version: 0.1.0
Summary: Live-stream tracker for Telegram: one auto-updating rich message with frames from YouTube, Twitch and Kick streams
Project-URL: Homepage, https://github.com/klipbn/streamlens
Project-URL: Issues, https://github.com/klipbn/streamlens/issues
Project-URL: Changelog, https://github.com/klipbn/streamlens/blob/main/CHANGELOG.md
Author: Alexey Voronko
License-Expression: MIT
License-File: LICENSE
Keywords: bot,kick,livestream,monitoring,telegram,twitch,youtube
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.9
Requires-Dist: requests>=2.25
Requires-Dist: tzdata; platform_system == 'Windows'
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Provides-Extra: frames
Requires-Dist: yt-dlp[default]>=2025.1.1; extra == 'frames'
Description-Content-Type: text/markdown

# streamlens

[![PyPI](https://img.shields.io/pypi/v/streamlens.svg?style=flat-square)](https://pypi.org/project/streamlens/)
[![Python](https://img.shields.io/pypi/pyversions/streamlens.svg?style=flat-square)](https://pypi.org/project/streamlens/)
[![CI](https://img.shields.io/github/actions/workflow/status/klipbn/streamlens/ci.yml?style=flat-square&label=tests)](https://github.com/klipbn/streamlens/actions)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](LICENSE)

**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

```bash
pip install streamlens
```

You need a bot token from [@BotFather](https://t.me/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:

```bash
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](https://t.me/RawDataBot) to the group, or open
> `https://api.telegram.org/bot<TOKEN>/getUpdates` after writing anything to your bot.

### 1. You have the data

```python
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):

```bash
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

```bash
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](https://dev.twitch.tv/console): 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:**

```bash
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

```bash
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](README.ru.md).
