Have you ever wanted to share the lyrics of a song with your friends in telegram dialogue, but you were too lazy to open a browser and search for the lyrics manually? With this bot, you just need to enter the name in inline mode and select the result from the inline menu with search results.
P.S. This is also my first article, so I'd love to see any feedback.
- Getting API keys
- Creating the bot
- Using the bot
Firstly, let's create bot in @BotFather.
Grab your token, we will use it later.
Our bot will work in inline mode. This means that the user just needs to write a @your_bot_name and a query to get the search results with song names.
Enbaling inline mode:
Then select your bot from keyboard.
We also need to activate the inline feedback setting so that the bot can load the lyrics of the song into the message (more on that later)
Firstly, login to your genius.com account or register if you don't have one.
Then go to the https://genius.com/api-clients and grab the "Client access token":
For this bot we'll use aiogram - asynchronous framework for creating fast and structured telegram bots, also we need lyricsgenius lib for interacting with Genius API and pydantic_settings to create config script.
pip install aiogram lyricsgenius pydantic_settings
In aiogram you have 3 main parts of your project: Bot, Dispatcher and handlers.
Bot - Telegram bot instance, you need token to get your bot.
Dispatcher - an object that receives updates from Telegram and then selects a handler to process the received update.
Handler - an asynchronous function that receives the next update from the dispatcher/router and processes it. You need to register it in the dispatcher. You can place handlers inside different Routers that act as a dispatcher for a subset of handlers.
Let's create basic bot with simple functionality.
In your project directory create file .env and fill it like this:
BOT_TOKEN='PASTE YOUR BOT TOKEN' GENIUS_TOKEN='PASTE YOUR CLIENT ACCESS TOKEN'
To use this variables we need to create config file.
Our config.py file should look like this:
from typing import Optional from pydantic_settings import BaseSettings from pydantic import SecretStr class Settings(BaseSettings): bot_token: SecretStr genius_token: SecretStr class Config: env_file = "./.env" env_file_encoding = "utf-8" env_nested_delimiter = "__" CONFIG = Settings()
We can now securely access our tokens, let's create basic bot in main.py:
import asyncio from aiogram import Bot, Dispatcher from config import CONFIG bot = Bot(token=CONFIG.bot_token.get_secret_value()) async def main(): dp = Dispatcher() await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())
At this moment we have no handlers, so bot won't react to any messages.
To create basic handlers, create folder handlers/ and file base.py in it at your project directory.
File structure should look like this:
├── main.py ├── config.py ├── .env ├── handlers │ └── base.py
In base.py, firstly we import Router to create router, filter Command so bot can react to some commands and type Message:
from aiogram import Router from aiogram.filters import Command from aiogram.types import Message router = Router()
Then we register handler functions by wrapping them in special decorators.
@router.message(Command("start")) async def start_command(message: Message): await message.answer("This bot can search and send song lyrics from genius.com\nFor more help use /help") @router.message(Command("help")) async def help_command(message: Message): await message.answer("Use this bot in inline mode. Just type @lyrics_genius_bot <song name> and results will appear soon!")
"@router.message" means that bot will react at messages, and filter Command("start") as argument for decorator means that handler will be executed only if message contains command "/start"
Finally, import router in main.py and register it:
from handlers import base
async def main(): dp = Dispatcher() dp.include_routers( base.router, ) await dp.start_polling(bot)
Congrats! We made basic bot.
Now let's create utilite to search songs lyrics along genis.com:
from config import CONFIG import lyricsgenius as lg class LyricsFinder: __instance = None def __new__(cls, *args, **kwargs): if cls.__instance is None: cls.__instance = super(LyricsFinder, cls).__new__(cls) cls.__instance.init() return cls.__instance def init(self): self.genius = lg.Genius(CONFIG.genius_token.get_secret_value(), timeout=40) async def search_song(self, name="", page=1): result = self.genius.search_songs(name, per_page=5, page=page) return result['hits'] # current song: result['hits'][0<N<=5]["result"] # ["full_title"] or ["header_image_thumbnail_url"] or just ["id"] async def get_lyrics(self, song_id=None): return str(self.genius.lyrics(song_id))
This class is built upon Singelton pattern and describes connection to Genius API. It has two methods: for searching songs by query and for getting lyrics of songs by id.
Bot should receive inline queries, perform search and return user a list of results. When user chooses result from list, bot should send message "Loading..." with inline button that contains link to song on genius.com
To create that button we need to make inline keyboard with dynamic parameters, so button will contain song's title and link. Create folder keyboards/ and file song_url.py in it. Also create file songs_inline.py in handlers/ so we can create router for inline queries.
├── main.py ├── config.py ├── .env ├── handlers │ ├── base.py │ └── songs_inline.py ├── keyboards │ ├── song_url.py ├── utils │ └── genius_api.py
from aiogram.utils.keyboard import InlineKeyboardBuilder def song_url_button(url=None, title=None): builder = InlineKeyboardBuilder() builder.button(text=title, url=url) return builder
from uuid import uuid4 import main from aiogram import Router from aiogram.types import InlineQuery, InlineQueryResultArticle, \ InputTextMessageContent, ChosenInlineResult import lyricsgenius as lg from utils.genius_api import LyricsFinder from config import CONFIG from keyboards import song_url router = Router() finder = LyricsFinder() @router.inline_query() async def show_results(inline_query: InlineQuery): songs = await finder.search_song(inline_query.query) results =  for song in songs: results.append(InlineQueryResultArticle( id=str(song['result']["id"]), title=song['result']["title"], description=song["result"]["artist_names"], input_message_content=InputTextMessageContent( message_text="Loading text...", ), reply_markup=song_url.song_url_button(url=song['result']["url"], title=song['result']["full_title"]).as_markup(), thumbnail_url=song["result"]["header_image_thumbnail_url"] )) await inline_query.answer(results, is_personal=True) @router.chosen_inline_result() async def load_lyrics( chosen_result: ChosenInlineResult, ): l = str(await finder.get_lyrics(int(chosen_result.result_id))) message = await main.bot.edit_message_text( inline_message_id=chosen_result.inline_message_id, text=l.split("\n", 1) )
Finally, register the router:
from handlers import base, songs_inline
async def main(): dp = Dispatcher() dp.include_routers( base.router, songs_inline.router ) await dp.start_polling(bot)
You can get all source code in my GitHub repository
Thank you for reading! ❤️ ❤️ ❤️