DEV Community

Cover image for Inline Telegram Bot that searches song lyrics
Fvbvke
Fvbvke

Posted on • Updated on

Inline Telegram Bot that searches song lyrics

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.
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
Then select your bot from keyboard.
Example results:
Setting inline mode for bot
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

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 and grab the "Client access token":
genius api token page

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'
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

"@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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
    )
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Using the bot

Startup bot:
python main.py
Example usage:
searching songs with bot

loading song

loading button

loaded song lyrics

You can get all source code in my GitHub repository

Thank you for reading! ❤️ ❤️ ❤️

Top comments (0)