Inline Telegram Bot that searches song lyrics
Tags: python, telegram, bot, tutorial
Published: 2023-09-27 10:13:21

## Intro
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.

**Contents**
1. Getting API keys
2. Creating the bot
3. Using the bot

## Getting API keys

### Telegram BotAPI
Firstly, let's create bot in [@BotFather](https://t.me/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:
![BotFather commands](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cx0sy496g57a62savv0x.png)
Then select your bot from keyboard.
Example results:
![Setting inline mode for bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/930d9ca9jfl2knftxk74.png)
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)
Example results:
![Setting inline feedback parameter to Enabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnlc0g80d22ha2rq750s.png)

### Genius API
Firstly, login to your genius.com account or register if you don't have one.
Then go to the [https://genius.com/api-clients](https://genius.com/api-clients) and grab the "Client access token":
![genius api token page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/klk5qiz0pdvrleea286g.png)

## Creating the bot
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.

### Config
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()
```

### Basic handlers
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. 

### Genius API utilite
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.

### Inline handlers
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.

#### Final file structure

```
├── main.py
├── config.py
├── .env
├── handlers
│   ├── base.py
│   └── songs_inline.py
├── keyboards
│   ├── song_url.py
├── utils
│   └── genius_api.py
```

song_url.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
```

songs_inline.py
```
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)[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)
```

## Using the bot
Startup bot:
`python main.py`
Example usage:
![searching songs with bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/367zli246cpg0jca6722.png)

![loading song](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a51hv4oc86xug2h7y9e.png)

![loading button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x1crbnex3kpb6eisjt1p.png)

![loaded song lyrics](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fm6d6nm6igjgu6ozpmfh.png)



You can get all source code in my [GitHub repository](https://github.com/krystlepalace/genius-lyrics-bot/)

Thank you for reading! ❤️ ❤️ ❤️