README.md

May 7, 2026 ยท View on GitHub

Banger Link Logo

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.

CI Container image License Python 3.12 python-telegram-bot 22


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

CommandWhat it does
/help, /startOnboarding.
/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

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

  1. Create a bot with @BotFather and copy the token into TELEGRAM_TOKEN.
  2. Send /setinline to BotFather to enable @yourbot inline queries.
  3. 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.
  4. 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.

VariableDefaultMeaning
TELEGRAM_TOKEN(required)Token from BotFather.
WHITELISTED_CHAT_IDSemptyComma-separated chat IDs. Empty = answer everywhere.
IGNORED_DOMAINSemptySemicolon-separated domain substrings to skip.
DATA_DIR./dataWhere the SQLite DB lives.
HEALTH_PORT8080Port for the /health endpoint.
LOG_LEVELINFOStandard Python log levels.
DIGEST_TIMEZONEUTCIANA name (e.g. Europe/Lisbon).
DIGEST_HOUR12Hour-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's entityUniqueId.
  • 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

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

License

MIT โ€” see LICENSE.