From 7380beeabbd58ae880715dfb7070072b52671037 Mon Sep 17 00:00:00 2001 From: BarsTiger Date: Wed, 25 Oct 2023 15:48:07 +0300 Subject: [PATCH] Did some refactor, add YouTube as search variant --- bot/filters/__init__.py | 1 + bot/filters/search.py | 13 +++++ bot/handlers/__init__.py | 2 + .../inline_default/on_inline_default.py | 2 +- bot/handlers/inline_song/__init__.py | 10 ++++ bot/handlers/inline_song/on_inline_deezer.py | 17 +++++++ bot/handlers/inline_song/on_inline_spotify.py | 17 +++++++ bot/handlers/inline_song/on_inline_youtube.py | 17 +++++++ bot/handlers/on_chosen/__init__.py | 3 +- bot/handlers/on_chosen/youtube.py | 49 +++++++++++++++++++ bot/markups/__init__.py | 0 bot/markups/spotify/search.py | 33 ------------- bot/modules/common/__init__.py | 1 + bot/modules/common/song/__init__.py | 1 + bot/modules/common/song/song.py | 21 ++++++++ bot/modules/database/db.py | 1 + bot/modules/deezer/song.py | 41 +++------------- bot/modules/spotify/song.py | 21 ++------ bot/modules/youtube/song.py | 29 +++++------ bot/results/__init__.py | 1 + bot/results/common/__init__.py | 1 + .../deezer => results/common}/search.py | 32 +++++++----- bot/{markups => results}/deezer/__init__.py | 0 bot/results/deezer/search.py | 21 ++++++++ bot/{markups => results}/spotify/__init__.py | 0 bot/results/spotify/search.py | 21 ++++++++ bot/results/youtube/__init__.py | 6 +++ bot/results/youtube/search.py | 21 ++++++++ 28 files changed, 265 insertions(+), 117 deletions(-) create mode 100644 bot/filters/search.py create mode 100644 bot/handlers/inline_song/__init__.py create mode 100644 bot/handlers/inline_song/on_inline_deezer.py create mode 100644 bot/handlers/inline_song/on_inline_spotify.py create mode 100644 bot/handlers/inline_song/on_inline_youtube.py create mode 100644 bot/handlers/on_chosen/youtube.py delete mode 100644 bot/markups/__init__.py delete mode 100644 bot/markups/spotify/search.py create mode 100644 bot/modules/common/__init__.py create mode 100644 bot/modules/common/song/__init__.py create mode 100644 bot/modules/common/song/song.py create mode 100644 bot/results/__init__.py create mode 100644 bot/results/common/__init__.py rename bot/{markups/deezer => results/common}/search.py (51%) rename bot/{markups => results}/deezer/__init__.py (100%) create mode 100644 bot/results/deezer/search.py rename bot/{markups => results}/spotify/__init__.py (100%) create mode 100644 bot/results/spotify/search.py create mode 100644 bot/results/youtube/__init__.py create mode 100644 bot/results/youtube/search.py diff --git a/bot/filters/__init__.py b/bot/filters/__init__.py index e69de29..ede329d 100644 --- a/bot/filters/__init__.py +++ b/bot/filters/__init__.py @@ -0,0 +1 @@ +from .search import ServiceSearchFilter diff --git a/bot/filters/search.py b/bot/filters/search.py new file mode 100644 index 0000000..588a31b --- /dev/null +++ b/bot/filters/search.py @@ -0,0 +1,13 @@ +from aiogram.filters import BaseFilter +from aiogram.types import InlineQuery + + +class ServiceSearchFilter(BaseFilter): + def __init__(self, service_letter: str): + self.service_letter = f'{service_letter}:' + + async def __call__(self, inline_query: InlineQuery): + return ( + inline_query.query.startswith(self.service_letter) and + inline_query.query != self.service_letter + ) diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 3c25a77..5f253ea 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -1,6 +1,7 @@ from aiogram import Router from . import ( initialize, + inline_song, inline_default, inline_empty, on_chosen, @@ -14,6 +15,7 @@ router.chosen_inline_result.outer_middleware(SaveChosenMiddleware()) router.include_routers( initialize.router, + inline_song.router, inline_default.router, inline_empty.router, on_chosen.router, diff --git a/bot/handlers/inline_default/on_inline_default.py b/bot/handlers/inline_default/on_inline_default.py index 52e3b03..4029162 100644 --- a/bot/handlers/inline_default/on_inline_default.py +++ b/bot/handlers/inline_default/on_inline_default.py @@ -2,7 +2,7 @@ from aiogram import Router, F from aiogram.types import InlineQuery -from bot.markups.deezer import get_deezer_search_results +from bot.results.deezer import get_deezer_search_results router = Router() diff --git a/bot/handlers/inline_song/__init__.py b/bot/handlers/inline_song/__init__.py new file mode 100644 index 0000000..354a94d --- /dev/null +++ b/bot/handlers/inline_song/__init__.py @@ -0,0 +1,10 @@ +from aiogram import Router + +from . import on_inline_spotify, on_inline_deezer, on_inline_youtube + +router = Router() +router.include_routers( + on_inline_spotify.router, + on_inline_deezer.router, + on_inline_youtube.router, +) diff --git a/bot/handlers/inline_song/on_inline_deezer.py b/bot/handlers/inline_song/on_inline_deezer.py new file mode 100644 index 0000000..1db08f3 --- /dev/null +++ b/bot/handlers/inline_song/on_inline_deezer.py @@ -0,0 +1,17 @@ +from aiogram import Router + +from aiogram.types import InlineQuery + +from bot.results.deezer import get_deezer_search_results +from bot.filters import ServiceSearchFilter + +router = Router() + + +@router.inline_query(ServiceSearchFilter('d')) +async def search_deezer_inline_query(inline_query: InlineQuery): + await inline_query.answer( + await get_deezer_search_results(inline_query.query.removeprefix('d:')), + cache_time=0, + is_personal=True + ) diff --git a/bot/handlers/inline_song/on_inline_spotify.py b/bot/handlers/inline_song/on_inline_spotify.py new file mode 100644 index 0000000..532a858 --- /dev/null +++ b/bot/handlers/inline_song/on_inline_spotify.py @@ -0,0 +1,17 @@ +from aiogram import Router + +from aiogram.types import InlineQuery + +from bot.results.spotify import get_spotify_search_results +from bot.filters import ServiceSearchFilter + +router = Router() + + +@router.inline_query(ServiceSearchFilter('s')) +async def search_spotify_inline_query(inline_query: InlineQuery): + await inline_query.answer( + await get_spotify_search_results(inline_query.query.removeprefix('s:')), + cache_time=0, + is_personal=True + ) diff --git a/bot/handlers/inline_song/on_inline_youtube.py b/bot/handlers/inline_song/on_inline_youtube.py new file mode 100644 index 0000000..11c22b2 --- /dev/null +++ b/bot/handlers/inline_song/on_inline_youtube.py @@ -0,0 +1,17 @@ +from aiogram import Router + +from aiogram.types import InlineQuery + +from bot.results.youtube import get_youtube_search_results +from bot.filters import ServiceSearchFilter + +router = Router() + + +@router.inline_query(ServiceSearchFilter('y')) +async def search_youtube_inline_query(inline_query: InlineQuery): + await inline_query.answer( + await get_youtube_search_results(inline_query.query.removeprefix('y:')), + cache_time=0, + is_personal=True + ) diff --git a/bot/handlers/on_chosen/__init__.py b/bot/handlers/on_chosen/__init__.py index 65c73da..ba2899d 100644 --- a/bot/handlers/on_chosen/__init__.py +++ b/bot/handlers/on_chosen/__init__.py @@ -1,11 +1,12 @@ from aiogram import Router -from . import spotify, deezer +from . import spotify, deezer, youtube router = Router() router.include_routers( spotify.router, deezer.router, + youtube.router, ) __all__ = ['router'] diff --git a/bot/handlers/on_chosen/youtube.py b/bot/handlers/on_chosen/youtube.py new file mode 100644 index 0000000..38ea9ee --- /dev/null +++ b/bot/handlers/on_chosen/youtube.py @@ -0,0 +1,49 @@ +from aiogram import Router, Bot, F +from aiogram.types import ( + BufferedInputFile, URLInputFile, InputMediaAudio, + ChosenInlineResult, +) + +from bot.modules.youtube import youtube, AgeRestrictedError +from bot.utils.config import config +from bot.modules.database import db + +router = Router() + + +@router.chosen_inline_result(F.result_id.startswith('yt::')) +async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot): + song = youtube.songs.from_id(chosen_result.result_id.removeprefix('yt::')) + + try: + bytestream = await song.to_bytestream() + except AgeRestrictedError: + await bot.edit_message_caption( + inline_message_id=chosen_result.inline_message_id, + caption='🔞 This song is age restricted, so I can\'t download it. ' + 'Try downloading it from Deezer or SoundCloud', + reply_markup=None + ) + return + + audio = await bot.send_audio( + chat_id=config.telegram.files_chat, + audio=BufferedInputFile( + file=bytestream.file, + filename=bytestream.filename, + ), + thumbnail=URLInputFile(song.thumbnail), + performer=song.all_artists, + title=song.name, + duration=bytestream.duration, + ) + + db.spotify[song.id] = audio.audio.file_id + + await bot.edit_message_media( + inline_message_id=chosen_result.inline_message_id, + media=InputMediaAudio(media=audio.audio.file_id), + reply_markup=None + ) + + await db.occasionally_write() diff --git a/bot/markups/__init__.py b/bot/markups/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bot/markups/spotify/search.py b/bot/markups/spotify/search.py deleted file mode 100644 index bf23245..0000000 --- a/bot/markups/spotify/search.py +++ /dev/null @@ -1,33 +0,0 @@ -from aiogram.types import ( - InlineQueryResultDocument, InlineQueryResultCachedAudio, - InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResult -) - -from bot.modules.spotify import spotify -from bot.modules.database import db - - -async def get_spotify_search_results(query: str) -> list[ - InlineQueryResultDocument | InlineQueryResultCachedAudio -]: - return [ - InlineQueryResultDocument( - id='spot::' + audio.id, - title=audio.name, - description=audio.all_artists, - thumb_url=audio.thumbnail, - document_url=audio.preview_url or audio.thumbnail, - mime_type='application/zip', - reply_markup=InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text='Downloading...', callback_data='.')] - ] - ), - caption=audio.full_name, - ) 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(query, limit=50) - ] diff --git a/bot/modules/common/__init__.py b/bot/modules/common/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/bot/modules/common/__init__.py @@ -0,0 +1 @@ +pass diff --git a/bot/modules/common/song/__init__.py b/bot/modules/common/song/__init__.py new file mode 100644 index 0000000..dca9ce0 --- /dev/null +++ b/bot/modules/common/song/__init__.py @@ -0,0 +1 @@ +from .song import BaseSongItem diff --git a/bot/modules/common/song/song.py b/bot/modules/common/song/song.py new file mode 100644 index 0000000..21b7099 --- /dev/null +++ b/bot/modules/common/song/song.py @@ -0,0 +1,21 @@ +from attrs import define + + +@define +class BaseSongItem: + name: str + id: str + artists: list[str] + preview_url: str | None + thumbnail: str + + @property + def all_artists(self): + return ', '.join(self.artists) + + @property + def full_name(self): + return f"{self.all_artists} - {self.name}" + + def __str__(self): + return self.full_name diff --git a/bot/modules/database/db.py b/bot/modules/database/db.py index c5f28e7..c6775fa 100644 --- a/bot/modules/database/db.py +++ b/bot/modules/database/db.py @@ -17,6 +17,7 @@ class Db(object): self.inline = DBDict('inline') self.spotify = DBDict('spotify') self.deezer = DBDict('deezer') + self.youtube = DBDict('youtube') async def write(self): await self.config.write() diff --git a/bot/modules/deezer/song.py b/bot/modules/deezer/song.py index 0464f6a..9954466 100644 --- a/bot/modules/deezer/song.py +++ b/bot/modules/deezer/song.py @@ -2,43 +2,25 @@ from attrs import define from .driver import DeezerDriver +from ..common.song import BaseSongItem + @define -class SongItem: - name: str - id: int - id_s: str - artist: str - preview_url: str | None - thumbnail: str - +class SongItem(BaseSongItem): @classmethod def from_deezer(cls, song_item: dict): return cls( name=song_item['title'], - id=song_item['id'], - id_s=str(song_item['id']), - artist=song_item['artist']['name'], + id=str(song_item['id']), + artists=[song_item['artist']['name']], preview_url=song_item.get('preview'), thumbnail=song_item['album']['cover_medium'] ) - @property - def full_name(self): - return f"{self.artist} - {self.name}" - - def __str__(self): - return self.full_name - @define -class FullSongItem: - name: str - id: str - artists: list[str] - preview_url: str | None +class FullSongItem(BaseSongItem): duration: int - thumbnail: str track_dict: dict @classmethod @@ -60,17 +42,6 @@ class FullSongItem: track_dict=song_item ) - @property - def all_artists(self): - return ', '.join(self.artists) - - @property - def full_name(self): - return f"{self.all_artists} - {self.name}" - - def __str__(self): - return self.full_name - @define class Songs(object): diff --git a/bot/modules/spotify/song.py b/bot/modules/spotify/song.py index 5057f24..fcd453b 100644 --- a/bot/modules/spotify/song.py +++ b/bot/modules/spotify/song.py @@ -1,15 +1,11 @@ from attrs import define import spotipy +from ..common.song import BaseSongItem + @define -class SongItem: - name: str - id: str - artists: list[str] - preview_url: str | None - thumbnail: str - +class SongItem(BaseSongItem): @classmethod def from_spotify(cls, song_item: dict): return cls( @@ -21,17 +17,6 @@ class SongItem: thumbnail=song_item['album']['images'][1]['url'] ) - @property - 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}" - @define class Songs(object): diff --git a/bot/modules/youtube/song.py b/bot/modules/youtube/song.py index 2f8c71c..2f4e3a9 100644 --- a/bot/modules/youtube/song.py +++ b/bot/modules/youtube/song.py @@ -5,13 +5,12 @@ from .downloader import Downloader, YouTubeBytestream from typing import Awaitable +from ..common.song import BaseSongItem + @define -class SongItem: - name: str - id: str - artists: list[str] - thumbnail: str +class SongItem(BaseSongItem): + preview_url: None = None @classmethod def from_youtube(cls, song_item: dict): @@ -22,16 +21,14 @@ class SongItem: thumbnail=song_item['thumbnails'][1]['url'] ) - @property - def all_artists(self): - return ', '.join(self.artists) - - @property - def full_name(self): - return f"{self.all_artists} - {self.name}" - - def __str__(self): - return self.full_name + @classmethod + def from_details(cls, details: dict): + return cls( + name=details['title'], + id=details['videoId'], + artists=details['author'].split(' & '), + thumbnail=details['thumbnail']['thumbnails'][1]['url'] + ) def to_bytestream(self) -> Awaitable[YouTubeBytestream]: return Downloader.from_id(self.id).to_bytestream() @@ -58,4 +55,4 @@ class Songs(object): if r is None: return None - return SongItem.from_youtube(r) + return SongItem.from_details(r['videoDetails']) diff --git a/bot/results/__init__.py b/bot/results/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/bot/results/__init__.py @@ -0,0 +1 @@ +pass diff --git a/bot/results/common/__init__.py b/bot/results/common/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/bot/results/common/__init__.py @@ -0,0 +1 @@ +pass diff --git a/bot/markups/deezer/search.py b/bot/results/common/search.py similarity index 51% rename from bot/markups/deezer/search.py rename to bot/results/common/search.py index f2ca2f7..00a8f7b 100644 --- a/bot/markups/deezer/search.py +++ b/bot/results/common/search.py @@ -3,18 +3,25 @@ from aiogram.types import ( InlineKeyboardMarkup, InlineKeyboardButton ) -from bot.modules.deezer import deezer -from bot.modules.database import db +from bot.modules.database.db import DBDict + +from bot.modules.common.song import BaseSongItem +from typing import TypeVar -async def get_deezer_search_results(query: str) -> list[ - InlineQueryResultDocument | InlineQueryResultCachedAudio -]: - return [ +BaseSongT = TypeVar('BaseSongT', bound=BaseSongItem) + + +async def get_common_search_result( + audio: BaseSongT, + db_table: DBDict, + service_id: str +) -> InlineQueryResultDocument | InlineQueryResultCachedAudio: + return ( InlineQueryResultDocument( - id='deez::' + audio.id_s, + id=f'{service_id}::' + audio.id, title=audio.name, - description=audio.artist, + description=audio.all_artists, thumb_url=audio.thumbnail, document_url=audio.preview_url or audio.thumbnail, mime_type='application/zip', @@ -24,10 +31,9 @@ async def get_deezer_search_results(query: str) -> list[ ] ), caption=audio.full_name, - ) if audio.id_s not in list(db.deezer.keys()) else + ) if audio.id not in list(db_table.keys()) else InlineQueryResultCachedAudio( - id='deezc::' + audio.id_s, - audio_file_id=db.deezer[audio.id_s], + id=f'{service_id}c::' + audio.id, + audio_file_id=db_table[audio.id], ) - for audio in await deezer.songs.search(query, limit=50) - ] + ) diff --git a/bot/markups/deezer/__init__.py b/bot/results/deezer/__init__.py similarity index 100% rename from bot/markups/deezer/__init__.py rename to bot/results/deezer/__init__.py diff --git a/bot/results/deezer/search.py b/bot/results/deezer/search.py new file mode 100644 index 0000000..dc65199 --- /dev/null +++ b/bot/results/deezer/search.py @@ -0,0 +1,21 @@ +from aiogram.types import ( + InlineQueryResultDocument, InlineQueryResultCachedAudio +) + +from bot.modules.deezer import deezer +from bot.modules.database import db + +from ..common.search import get_common_search_result + + +async def get_deezer_search_results(query: str) -> list[ + InlineQueryResultDocument | InlineQueryResultCachedAudio +]: + return [ + await get_common_search_result( + audio=audio, + db_table=db.deezer, + service_id='deez' + ) + for audio in await deezer.songs.search(query, limit=50) + ] diff --git a/bot/markups/spotify/__init__.py b/bot/results/spotify/__init__.py similarity index 100% rename from bot/markups/spotify/__init__.py rename to bot/results/spotify/__init__.py diff --git a/bot/results/spotify/search.py b/bot/results/spotify/search.py new file mode 100644 index 0000000..d9865b8 --- /dev/null +++ b/bot/results/spotify/search.py @@ -0,0 +1,21 @@ +from aiogram.types import ( + InlineQueryResultDocument, InlineQueryResultCachedAudio +) + +from bot.modules.spotify import spotify +from bot.modules.database import db + +from ..common.search import get_common_search_result + + +async def get_spotify_search_results(query: str) -> list[ + InlineQueryResultDocument | InlineQueryResultCachedAudio +]: + return [ + await get_common_search_result( + audio=audio, + db_table=db.spotify, + service_id='spot' + ) + for audio in spotify.songs.search(query, limit=50) + ] diff --git a/bot/results/youtube/__init__.py b/bot/results/youtube/__init__.py new file mode 100644 index 0000000..b95c9c6 --- /dev/null +++ b/bot/results/youtube/__init__.py @@ -0,0 +1,6 @@ +from .search import get_youtube_search_results + + +__all__ = [ + 'get_youtube_search_results' +] diff --git a/bot/results/youtube/search.py b/bot/results/youtube/search.py new file mode 100644 index 0000000..c87a648 --- /dev/null +++ b/bot/results/youtube/search.py @@ -0,0 +1,21 @@ +from aiogram.types import ( + InlineQueryResultDocument, InlineQueryResultCachedAudio +) + +from bot.modules.youtube import youtube +from bot.modules.database import db + +from ..common.search import get_common_search_result + + +async def get_youtube_search_results(query: str) -> list[ + InlineQueryResultDocument | InlineQueryResultCachedAudio +]: + return [ + await get_common_search_result( + audio=audio, + db_table=db.youtube, + service_id='yt' + ) + for audio in youtube.songs.search(query, limit=40) + ]