From b515fdfd65772c2b002ae0740ebcbce307cbf26b Mon Sep 17 00:00:00 2001 From: BarsTiger Date: Mon, 16 Oct 2023 20:17:10 +0300 Subject: [PATCH] Add spotify search (only search), default menu --- bot/__init__.py | 7 +-- bot/callbacks/__init__.py | 14 ++++++ bot/callbacks/factories/full_menu.py | 6 +++ bot/callbacks/full_menu.py | 12 +++++ bot/handlers/__init__.py | 13 +++++- bot/handlers/inline_default/__init__.py | 1 + .../inline_default/on_inline_default.py | 38 +++++++++++++++ bot/handlers/inline_empty/__init__.py | 1 + bot/handlers/inline_empty/on_inline_empty.py | 23 ++++++++++ bot/handlers/on_chosen/__init__.py | 1 + bot/handlers/on_chosen/spotify.py | 35 ++++++++++++++ bot/keyboards/inline/full_menu.py | 22 +++++++++ bot/middlewares/__init__.py | 2 + bot/middlewares/private_button.py | 19 ++++++++ bot/middlewares/save_chosen.py | 46 +++++++++++++++++++ bot/modules/database/db.py | 8 ++++ bot/modules/spotify/song.py | 12 +++++ bot/modules/youtube/__init__.py | 0 18 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 bot/callbacks/factories/full_menu.py create mode 100644 bot/callbacks/full_menu.py create mode 100644 bot/handlers/inline_default/__init__.py create mode 100644 bot/handlers/inline_default/on_inline_default.py create mode 100644 bot/handlers/inline_empty/__init__.py create mode 100644 bot/handlers/inline_empty/on_inline_empty.py create mode 100644 bot/handlers/on_chosen/__init__.py create mode 100644 bot/handlers/on_chosen/spotify.py create mode 100644 bot/keyboards/inline/full_menu.py create mode 100644 bot/middlewares/private_button.py create mode 100644 bot/middlewares/save_chosen.py create mode 100644 bot/modules/youtube/__init__.py diff --git a/bot/__init__.py b/bot/__init__.py index d3bb4fd..9090de9 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -4,10 +4,11 @@ from rich import print async def runner(): from .common import dp, bot - from . import handlers + from . import handlers, callbacks - dp.include_router( - handlers.router + dp.include_routers( + handlers.router, + callbacks.router, ) await bot.delete_webhook(drop_pending_updates=True) diff --git a/bot/callbacks/__init__.py b/bot/callbacks/__init__.py index e69de29..cf87ccf 100644 --- a/bot/callbacks/__init__.py +++ b/bot/callbacks/__init__.py @@ -0,0 +1,14 @@ +from aiogram import Router +from . import ( + full_menu, +) +from bot.middlewares import PrivateButtonMiddleware + + +router = Router() + +router.callback_query.middleware(PrivateButtonMiddleware()) + +router.include_routers( + full_menu.router, +) diff --git a/bot/callbacks/factories/full_menu.py b/bot/callbacks/factories/full_menu.py new file mode 100644 index 0000000..6d0569d --- /dev/null +++ b/bot/callbacks/factories/full_menu.py @@ -0,0 +1,6 @@ +from typing import Literal +from aiogram.filters.callback_data import CallbackData + + +class FullMenuCallback(CallbackData, prefix='full_menu'): + action: Literal['settings'] diff --git a/bot/callbacks/full_menu.py b/bot/callbacks/full_menu.py new file mode 100644 index 0000000..be8b368 --- /dev/null +++ b/bot/callbacks/full_menu.py @@ -0,0 +1,12 @@ +from aiogram import Router, Bot, F +from aiogram.types import ( + CallbackQuery +) +from .factories.full_menu import FullMenuCallback + +router = Router() + + +@router.callback_query(FullMenuCallback.filter(F.action == 'settings')) +async def on_close(callback_query: CallbackQuery, bot: Bot): + await callback_query.answer('Settings are not available yet') diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 74b3d52..3c25a77 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -1,9 +1,20 @@ from aiogram import Router -from . import initialize +from . import ( + initialize, + inline_default, + inline_empty, + on_chosen, +) +from bot.middlewares import SaveChosenMiddleware router = Router() +router.chosen_inline_result.outer_middleware(SaveChosenMiddleware()) + router.include_routers( initialize.router, + inline_default.router, + inline_empty.router, + on_chosen.router, ) diff --git a/bot/handlers/inline_default/__init__.py b/bot/handlers/inline_default/__init__.py new file mode 100644 index 0000000..28626c4 --- /dev/null +++ b/bot/handlers/inline_default/__init__.py @@ -0,0 +1 @@ +from .on_inline_default import router diff --git a/bot/handlers/inline_default/on_inline_default.py b/bot/handlers/inline_default/on_inline_default.py new file mode 100644 index 0000000..d5d7dbf --- /dev/null +++ b/bot/handlers/inline_default/on_inline_default.py @@ -0,0 +1,38 @@ +from aiogram import Router, Bot, F +from aiogram.types import ( + InlineQuery, InlineQueryResultDocument, InlineQueryResultCachedAudio, + InlineKeyboardMarkup, InlineKeyboardButton, +) + +from bot.modules.spotify import spotify +from bot.modules.database import db + +router = Router() + + +@router.inline_query(F.query != '') +async def default_inline_query(inline_query: InlineQuery, bot: Bot): + await inline_query.answer( + [ + InlineQueryResultDocument( + id='spot::' + audio.id, + title=audio.name, + description=audio.all_artists, + thumb_url=audio.thumbnail, + document_url=audio.preview_url, + mime_type='application/zip', + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text='Downloading...', callback_data='.')] + ] + ) + ) if audio.id not in list(db.spotify.keys()) else + InlineQueryResultCachedAudio( + id='spotc::' + audio.id, + audio_file_id=db.spotify[audio.id], + ) + for audio in spotify.songs.search(inline_query.query) + ], + cache_time=0, + is_personal=True + ) diff --git a/bot/handlers/inline_empty/__init__.py b/bot/handlers/inline_empty/__init__.py new file mode 100644 index 0000000..0370e61 --- /dev/null +++ b/bot/handlers/inline_empty/__init__.py @@ -0,0 +1 @@ +from .on_inline_empty import router diff --git a/bot/handlers/inline_empty/on_inline_empty.py b/bot/handlers/inline_empty/on_inline_empty.py new file mode 100644 index 0000000..5d420ad --- /dev/null +++ b/bot/handlers/inline_empty/on_inline_empty.py @@ -0,0 +1,23 @@ +from aiogram import Router, Bot, F +from aiogram.types import ( + InlineQuery, InputTextMessageContent, InlineQueryResultArticle +) +from bot.keyboards.inline.full_menu import get_full_menu_kb + +router = Router() + + +@router.inline_query(F.query == '') +async def empty_inline_query(inline_query: InlineQuery, bot: Bot): + await inline_query.answer( + [ + InlineQueryResultArticle( + id='show_menu', + title='⚙️ Open menu', + input_message_content=InputTextMessageContent( + message_text='⚙️ Menu' + ), + reply_markup=get_full_menu_kb() + ) + ], cache_time=0 + ) diff --git a/bot/handlers/on_chosen/__init__.py b/bot/handlers/on_chosen/__init__.py new file mode 100644 index 0000000..c20b9c0 --- /dev/null +++ b/bot/handlers/on_chosen/__init__.py @@ -0,0 +1 @@ +from .spotify import router diff --git a/bot/handlers/on_chosen/spotify.py b/bot/handlers/on_chosen/spotify.py new file mode 100644 index 0000000..13de56d --- /dev/null +++ b/bot/handlers/on_chosen/spotify.py @@ -0,0 +1,35 @@ +from aiogram import Router, Bot, F +from aiogram.types import ( + FSInputFile, URLInputFile, InputMediaAudio, + ChosenInlineResult, +) + +from bot.modules.spotify import spotify +from rich import print +from bot.utils.config import config +from bot.modules.database import db + +router = Router() + + +@router.chosen_inline_result(F.result_id.startswith('spot::')) +async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot): + print('TEST: DOWNLOADING NEW') + song = spotify.songs.from_id(chosen_result.result_id.removeprefix('spot::')) + + audio = await bot.send_audio( + chat_id=config.telegram.files_chat, + audio=FSInputFile('tests/test.mp3'), + thumbnail=URLInputFile(song.thumbnail), + performer=song.all_artists, + title=song.name, + ) + + db.spotify[song.id] = audio.audio.file_id + await db.occasionally_write() + + await bot.edit_message_media( + inline_message_id=chosen_result.inline_message_id, + media=InputMediaAudio(media=audio.audio.file_id), + reply_markup=None + ) diff --git a/bot/keyboards/inline/full_menu.py b/bot/keyboards/inline/full_menu.py new file mode 100644 index 0000000..e6d606f --- /dev/null +++ b/bot/keyboards/inline/full_menu.py @@ -0,0 +1,22 @@ +from aiogram.utils.keyboard import (InlineKeyboardMarkup, InlineKeyboardButton, + InlineKeyboardBuilder) +from bot.callbacks.factories.full_menu import FullMenuCallback + + +def get_full_menu_kb() -> InlineKeyboardMarkup: + buttons = [ + [ + InlineKeyboardButton( + text='⚙️ Settings', + callback_data=FullMenuCallback( + action='settings' + ).pack() + ), + InlineKeyboardButton( + text='🎵 Search in SoundCloud', + switch_inline_query_current_chat='sc::' + ) + ] + ] + + return InlineKeyboardBuilder(buttons).as_markup() diff --git a/bot/middlewares/__init__.py b/bot/middlewares/__init__.py index e69de29..e4cf1ae 100644 --- a/bot/middlewares/__init__.py +++ b/bot/middlewares/__init__.py @@ -0,0 +1,2 @@ +from .private_button import PrivateButtonMiddleware +from .save_chosen import SaveChosenMiddleware diff --git a/bot/middlewares/private_button.py b/bot/middlewares/private_button.py new file mode 100644 index 0000000..a2a1743 --- /dev/null +++ b/bot/middlewares/private_button.py @@ -0,0 +1,19 @@ +from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.types import CallbackQuery + +from typing import Any, Awaitable, Callable, Dict + +from bot.modules.database import db + + +class PrivateButtonMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[CallbackQuery, Dict[str, Any]], Awaitable[Any]], + event: CallbackQuery, + data: Dict[str, Any], + ): + if event.from_user.id == db.inline[event.inline_message_id].from_user.id: + return await handler(event, data) + else: + await event.answer('This button is not for you') diff --git a/bot/middlewares/save_chosen.py b/bot/middlewares/save_chosen.py new file mode 100644 index 0000000..6a59e61 --- /dev/null +++ b/bot/middlewares/save_chosen.py @@ -0,0 +1,46 @@ +from aiogram.dispatcher.middlewares.base import BaseMiddleware +from aiogram.types import ChosenInlineResult + +from typing import Any, Awaitable, Callable, Dict +from dataclasses import dataclass + +from bot.modules.database import db + + +@dataclass +class SavedUser: + id: int + first_name: str + last_name: str | None + username: str | None + language_code: str | None + + +@dataclass +class SavedResult: + result_id: str + from_user: SavedUser + query: str + inline_message_id: str + + +class SaveChosenMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[ChosenInlineResult, Dict[str, Any]], Awaitable[Any]], + event: ChosenInlineResult, + data: Dict[str, Any], + ): + db.inline[event.inline_message_id] = SavedResult( + result_id=event.result_id, + from_user=SavedUser( + id=event.from_user.id, + first_name=event.from_user.first_name, + last_name=event.from_user.last_name, + username=event.from_user.username, + language_code=event.from_user.language_code + ), + query=event.query, + inline_message_id=event.inline_message_id + ) + return await handler(event, data) diff --git a/bot/modules/database/db.py b/bot/modules/database/db.py index f5095a9..5102c54 100644 --- a/bot/modules/database/db.py +++ b/bot/modules/database/db.py @@ -2,6 +2,8 @@ from .db_model import DBDict import os.path from bot.utils.config import config +from random import randint + DB = os.path.join(config.local.db_path, 'db') if not os.path.isfile(DB): @@ -12,6 +14,12 @@ class Db(object): def __init__(self): self.fsm = DBDict('fsm') self.config = DBDict('config') + self.inline = DBDict('inline') + self.spotify = DBDict('spotify') async def write(self): await self.config.write() + + async def occasionally_write(self, chance: int = 5): + if randint(1, chance) == 1: + await self.write() diff --git a/bot/modules/spotify/song.py b/bot/modules/spotify/song.py index e447ab6..ab328a6 100644 --- a/bot/modules/spotify/song.py +++ b/bot/modules/spotify/song.py @@ -24,6 +24,10 @@ class SongItem: def all_artists(self): return ', '.join(self.artists) + @property + def full_name(self): + return f"{self.all_artists} - {self.name}" + def __str__(self): return f"{', '.join(self.artists)} - {self.name}" @@ -39,3 +43,11 @@ class Songs(object): return None return [SongItem.from_spotify(item) for item in r['tracks']['items']] + + def from_id(self, song_id: str) -> SongItem | None: + r = self.spotify.track(song_id) + + if r is None: + return None + + return SongItem.from_spotify(r) diff --git a/bot/modules/youtube/__init__.py b/bot/modules/youtube/__init__.py new file mode 100644 index 0000000..e69de29