README.md
May 7, 2026 ยท View on GitHub
Banger Link
Drop a music link in your group chat. The bot replies with links for every other major streaming service, so nobody has to leave their app.
The problem
You're in a group chat with friends. Someone shares a Spotify link. Half the chat is on Apple Music, two people are on YouTube Music, one holdout uses Tidal. To play the song, everyone copy-pastes the title into their own app.
The fix
Banger Link sits in your chat and watches for music links. When it sees one, it replies with the song laid out for every service:
๐ต Lust for Life โ Iggy Pop
๐ข Spotify ๐ Apple Music โถ๏ธ YouTube
๐ถ YouTube Music ๐ Tidal ๐ง Deezer
โ๏ธ SoundCloud ๐ Amazon Music
โจ First time in this chat! ๐
[๐ 0] [๐ 0]
Tap ๐ or ๐ to vote โ the counts update live and the bot remembers your reactions so it can build leaderboards. Re-shares show "Shared 4ร โ first by Alice on Mar 12".
Features
- ๐ Any-to-any link conversion for ten+ streaming services, powered by Songlink/Odesli. No per-platform scrapers to break.
- ๐ / ๐ Per-user reactions with toggle, change-vote, and live count updates.
- ๐ Per-chat leaderboards via
/top,/weekly,/monthly. - ๐ Search this chat's history with
/search <query>, or fire off@bangerbot <query>from any chat to share a tune inline. - ๐ Weekly + monthly digests posted automatically โ recap the chat's bangers without anyone asking.
- ๐ Native Telegram slash-command menu (registered via
setMyCommands). - ๐ณ Multi-arch Docker image (
linux/amd64+linux/arm64) on GHCR. - ๐ฉบ HTTP healthcheck for container orchestration.
Bot commands
| Command | What it does |
|---|---|
/help, /start | Onboarding. |
/top [N] | Top bangers in this chat (all-time, by likes โ dislikes). |
/weekly [N] | Top of the last 7 days. |
/monthly [N] | Top of the last 30 days. |
/search <query> | Title/artist search across this chat's history. |
@bangerbot <query> | Inline mode โ pick a known banger and share it into the current chat. |
Run it
Docker (recommended)
A pre-built multi-arch image is published to GHCR on every push to main.
docker run -d --name banger-link \
--restart unless-stopped \
-e TELEGRAM_TOKEN=<your-bot-token> \
-v $(pwd)/data:/app/data \
ghcr.io/luisg0nc/banger-link:latest
Or use the included compose file:
cp .env.example .env # set TELEGRAM_TOKEN
docker compose up -d
State is one SQLite file under the bound data/ directory.
From source
Requires uv and Python 3.12.
git clone https://github.com/luisg0nc/banger-link.git
cd banger-link
uv sync --extra dev
cp .env.example .env # set TELEGRAM_TOKEN
uv run python -m banger_link
Telegram bot setup
- Create a bot with @BotFather and copy the token into
TELEGRAM_TOKEN. - Send
/setinlineto BotFather to enable@yourbotinline queries. - Add the bot to your group(s). For best UX, give it permission to read all messages (BotFather โ
/setprivacyโ Disable). Otherwise it can only see commands and messages that mention it. - Optional: lock the bot to specific chats via
WHITELISTED_CHAT_IDS.
Configuration
All settings come from env vars (or .env). See .env.example for the full list.
| Variable | Default | Meaning |
|---|---|---|
TELEGRAM_TOKEN | (required) | Token from BotFather. |
WHITELISTED_CHAT_IDS | empty | Comma-separated chat IDs. Empty = answer everywhere. |
IGNORED_DOMAINS | empty | Semicolon-separated domain substrings to skip. |
DATA_DIR | ./data | Where the SQLite DB lives. |
HEALTH_PORT | 8080 | Port for the /health endpoint. |
LOG_LEVEL | INFO | Standard Python log levels. |
DIGEST_TIMEZONE | UTC | IANA name (e.g. Europe/Lisbon). |
DIGEST_HOUR | 12 | Hour-of-day in DIGEST_TIMEZONE when digests are posted. |
Architecture
Telegram update
โโโ MessageHandler โ SonglinkClient โ Repo (upsert song + record mention) โ reply with reaction keyboard
โโโ CallbackQueryHandler (^r:) โ Repo.toggle_reaction โ edit reply_markup
โโโ CommandHandlers โ Repo.top_for_chat / search_chat
โโโ InlineQueryHandler โ Repo.search_global โ InlineQueryResultArticle list
JobQueue
โโโ weekly-digest (Mondays at DIGEST_HOUR) โ leaderboard posted into each active chat
โโโ monthly-digest (every day, no-ops unless day-of-month == 1)
aiohttp on :8080
โโโ /health โ SELECT 1 against the DB
Storage is a single SQLite database (WAL mode) with three tables:
songsโ global catalog, deduplicated by Songlink'sentityUniqueId.chat_songsโ one row per(chat, song)with first-sharer info and mention count.reactionsโ one row per(chat_song, user). Likes/dislikes are computed at read time, no denormalized counters.
callback_data for the reaction buttons is r:<chat_song_id>:<l|d> โ well under Telegram's 64-byte cap.
Tech stack
- python-telegram-bot v22 (async)
- Songlink/Odesli for cross-platform link resolution
- SQLite via aiosqlite for state
- pydantic-settings for config
- httpx for outbound HTTP
- uv + ruff + mypy + pytest for the dev loop
Development
uv run pytest # full suite with coverage
uv run pytest -k toggle --no-cov # filter, no coverage report
uv run pytest tests/test_repo.py::test_toggle_reaction_full_cycle
uv run ruff check . && uv run ruff format .
uv run mypy banger_link
There's a CLAUDE.md at the root that summarises the package layout, inbound flow, and storage invariants in a couple of pages โ read that before making non-trivial changes.
Contributing
Issues and PRs welcome. See CONTRIBUTING.md for the workflow and quality bar. The repo follows Conventional Commits and Semantic Versioning.
Acknowledgements
- Songlink/Odesli โ the heavy lifting behind the cross-platform link resolution.
- python-telegram-bot โ fantastic async Telegram bindings.
License
MIT โ see LICENSE.