used black
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
from bot import main
|
from bot import main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ def main():
|
|||||||
|
|
||||||
plugins()
|
plugins()
|
||||||
|
|
||||||
print('Starting...')
|
print("Starting...")
|
||||||
with contextlib.suppress(KeyboardInterrupt):
|
with contextlib.suppress(KeyboardInterrupt):
|
||||||
asyncio.run(runner())
|
asyncio.run(runner())
|
||||||
|
|
||||||
print('[red]Stopped.[/]')
|
print("[red]Stopped.[/]")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from . import main
|
from . import main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from aiogram import Router, F, Bot
|
from aiogram import Router, F, Bot
|
||||||
from aiogram.types import (
|
from aiogram.types import CallbackQuery
|
||||||
CallbackQuery
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.factories.full_menu import FullMenuCallback
|
from bot.factories.full_menu import FullMenuCallback
|
||||||
|
|
||||||
@@ -10,10 +8,10 @@ from bot.keyboards.inline.settings import get_settings_kb
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(FullMenuCallback.filter(F.action == 'settings'))
|
@router.callback_query(FullMenuCallback.filter(F.action == "settings"))
|
||||||
async def on_settings(callback_query: CallbackQuery, bot: Bot):
|
async def on_settings(callback_query: CallbackQuery, bot: Bot):
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
inline_message_id=callback_query.inline_message_id,
|
inline_message_id=callback_query.inline_message_id,
|
||||||
text='⚙️ Settings',
|
text="⚙️ Settings",
|
||||||
reply_markup=get_settings_kb()
|
reply_markup=get_settings_kb(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from aiogram import Router, F, Bot
|
from aiogram import Router, F, Bot
|
||||||
from aiogram.types import (
|
from aiogram.types import CallbackQuery
|
||||||
CallbackQuery
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.factories.full_menu import FullMenuCallback
|
from bot.factories.full_menu import FullMenuCallback
|
||||||
|
|
||||||
@@ -10,10 +8,10 @@ from bot.keyboards.inline.full_menu import get_full_menu_kb
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(FullMenuCallback.filter(F.action == 'home'))
|
@router.callback_query(FullMenuCallback.filter(F.action == "home"))
|
||||||
async def on_home(callback_query: CallbackQuery, bot: Bot):
|
async def on_home(callback_query: CallbackQuery, bot: Bot):
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
inline_message_id=callback_query.inline_message_id,
|
inline_message_id=callback_query.inline_message_id,
|
||||||
text='⚙️ Menu',
|
text="⚙️ Menu",
|
||||||
reply_markup=get_full_menu_kb()
|
reply_markup=get_full_menu_kb(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from aiogram import Router, Bot
|
from aiogram import Router, Bot
|
||||||
from aiogram.types import (
|
from aiogram.types import CallbackQuery
|
||||||
CallbackQuery
|
|
||||||
)
|
|
||||||
from aiogram.exceptions import TelegramBadRequest
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
|
|
||||||
from bot.factories.open_setting import OpenSettingCallback, SettingChoiceCallback
|
from bot.factories.open_setting import OpenSettingCallback, SettingChoiceCallback
|
||||||
@@ -14,25 +12,20 @@ router = Router()
|
|||||||
|
|
||||||
@router.callback_query(OpenSettingCallback.filter())
|
@router.callback_query(OpenSettingCallback.filter())
|
||||||
async def on_settings(
|
async def on_settings(
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery, callback_data: OpenSettingCallback, bot: Bot
|
||||||
callback_data: OpenSettingCallback,
|
|
||||||
bot: Bot
|
|
||||||
):
|
):
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
inline_message_id=callback_query.inline_message_id,
|
inline_message_id=callback_query.inline_message_id,
|
||||||
text=settings_strings[callback_data.s_id].description,
|
text=settings_strings[callback_data.s_id].description,
|
||||||
reply_markup=get_setting_kb(
|
reply_markup=get_setting_kb(
|
||||||
callback_data.s_id,
|
callback_data.s_id, str(callback_query.from_user.id)
|
||||||
str(callback_query.from_user.id)
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(SettingChoiceCallback.filter())
|
@router.callback_query(SettingChoiceCallback.filter())
|
||||||
async def on_change_setting(
|
async def on_change_setting(
|
||||||
callback_query: CallbackQuery,
|
callback_query: CallbackQuery, callback_data: SettingChoiceCallback, bot: Bot
|
||||||
callback_data: SettingChoiceCallback,
|
|
||||||
bot: Bot
|
|
||||||
):
|
):
|
||||||
UserSettings(callback_query.from_user.id)[callback_data.s_id] = callback_data.choice
|
UserSettings(callback_query.from_user.id)[callback_data.s_id] = callback_data.choice
|
||||||
try:
|
try:
|
||||||
@@ -40,9 +33,8 @@ async def on_change_setting(
|
|||||||
inline_message_id=callback_query.inline_message_id,
|
inline_message_id=callback_query.inline_message_id,
|
||||||
text=settings_strings[callback_data.s_id].description,
|
text=settings_strings[callback_data.s_id].description,
|
||||||
reply_markup=get_setting_kb(
|
reply_markup=get_setting_kb(
|
||||||
callback_data.s_id,
|
callback_data.s_id, str(callback_query.from_user.id)
|
||||||
str(callback_query.from_user.id)
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
except TelegramBadRequest:
|
except TelegramBadRequest:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ dp = Dispatcher(storage=InDbStorage())
|
|||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['bot', 'dp', 'config', 'console']
|
__all__ = ["bot", "dp", "config", "console"]
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ from typing import Literal
|
|||||||
from aiogram.filters.callback_data import CallbackData
|
from aiogram.filters.callback_data import CallbackData
|
||||||
|
|
||||||
|
|
||||||
class FullMenuCallback(CallbackData, prefix='full_menu'):
|
class FullMenuCallback(CallbackData, prefix="full_menu"):
|
||||||
action: Literal['home', 'settings']
|
action: Literal["home", "settings"]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from aiogram.filters.callback_data import CallbackData
|
from aiogram.filters.callback_data import CallbackData
|
||||||
|
|
||||||
|
|
||||||
class OpenSettingCallback(CallbackData, prefix='setting'):
|
class OpenSettingCallback(CallbackData, prefix="setting"):
|
||||||
s_id: str
|
s_id: str
|
||||||
|
|
||||||
|
|
||||||
class SettingChoiceCallback(CallbackData, prefix='s_choice'):
|
class SettingChoiceCallback(CallbackData, prefix="s_choice"):
|
||||||
s_id: str
|
s_id: str
|
||||||
choice: str
|
choice: str
|
||||||
|
|||||||
@@ -4,22 +4,21 @@ from aiogram.types import InlineQuery
|
|||||||
|
|
||||||
class ServiceSearchFilter(BaseFilter):
|
class ServiceSearchFilter(BaseFilter):
|
||||||
def __init__(self, service_letter: str):
|
def __init__(self, service_letter: str):
|
||||||
self.service_letter = f'{service_letter}:'
|
self.service_letter = f"{service_letter}:"
|
||||||
|
|
||||||
async def __call__(self, inline_query: InlineQuery):
|
async def __call__(self, inline_query: InlineQuery):
|
||||||
return (
|
return (
|
||||||
inline_query.query.startswith(self.service_letter) and
|
inline_query.query.startswith(self.service_letter)
|
||||||
inline_query.query != self.service_letter
|
and inline_query.query != self.service_letter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ServiceSearchMultiletterFilter(BaseFilter):
|
class ServiceSearchMultiletterFilter(BaseFilter):
|
||||||
def __init__(self, service_lettes: list[str]):
|
def __init__(self, service_lettes: list[str]):
|
||||||
self.service_letter = [f'{letter}:' for letter in service_lettes]
|
self.service_letter = [f"{letter}:" for letter in service_lettes]
|
||||||
|
|
||||||
async def __call__(self, inline_query: InlineQuery):
|
async def __call__(self, inline_query: InlineQuery):
|
||||||
return (
|
return (
|
||||||
any(inline_query.query.startswith(letter) for letter in
|
any(inline_query.query.startswith(letter) for letter in self.service_letter)
|
||||||
self.service_letter) and
|
and inline_query.query not in self.service_letter
|
||||||
inline_query.query not in self.service_letter
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,24 +9,21 @@ class MusicUrlFilter(BaseFilter):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def __call__(self, inline_query: InlineQuery):
|
async def __call__(self, inline_query: InlineQuery):
|
||||||
if not inline_query.query.strip().startswith('http'):
|
if not inline_query.query.strip().startswith("http"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
url = urlparse(inline_query.query)
|
url = urlparse(inline_query.query)
|
||||||
return (
|
return url.scheme in ["http", "https"] and any(
|
||||||
url.scheme in ['http', 'https'] and
|
map(
|
||||||
any(
|
url.netloc.endswith,
|
||||||
map(
|
[
|
||||||
url.netloc.endswith,
|
"youtube.com",
|
||||||
[
|
"youtu.be",
|
||||||
'youtube.com',
|
"open.spotify.com",
|
||||||
'youtu.be',
|
"spotify.link",
|
||||||
'open.spotify.com',
|
"deezer.page.link",
|
||||||
'spotify.link',
|
"deezer.com",
|
||||||
'deezer.page.link',
|
"soundcloud.com",
|
||||||
'deezer.com',
|
],
|
||||||
'soundcloud.com'
|
)
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ router = Router()
|
|||||||
|
|
||||||
@router.startup()
|
@router.startup()
|
||||||
async def startup(bot: Bot):
|
async def startup(bot: Bot):
|
||||||
print(f'[green]Started as[/] @{(await bot.me()).username}')
|
print(f"[green]Started as[/] @{(await bot.me()).username}")
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ from bot.modules.settings import UserSettings
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(F.query != '')
|
@router.inline_query(F.query != "")
|
||||||
async def default_inline_query(inline_query: InlineQuery, settings: UserSettings):
|
async def default_inline_query(inline_query: InlineQuery, settings: UserSettings):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await {
|
await {
|
||||||
'd': get_deezer_search_results,
|
"d": get_deezer_search_results,
|
||||||
'c': get_soundcloud_search_results,
|
"c": get_soundcloud_search_results,
|
||||||
'y': get_youtube_search_results,
|
"y": get_youtube_search_results,
|
||||||
's': get_spotify_search_results
|
"s": get_spotify_search_results,
|
||||||
}[settings['default_search_provider'].value](inline_query.query, settings),
|
}[settings["default_search_provider"].value](inline_query.query, settings),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import (
|
from aiogram.types import InlineQuery, InputTextMessageContent, InlineQueryResultArticle
|
||||||
InlineQuery, InputTextMessageContent, InlineQueryResultArticle
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.keyboards.inline.full_menu import get_full_menu_kb
|
from bot.keyboards.inline.full_menu import get_full_menu_kb
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(F.query == '')
|
@router.inline_query(F.query == "")
|
||||||
async def empty_inline_query(inline_query: InlineQuery):
|
async def empty_inline_query(inline_query: InlineQuery):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
[
|
[
|
||||||
InlineQueryResultArticle(
|
InlineQueryResultArticle(
|
||||||
id='show_menu',
|
id="show_menu",
|
||||||
title='⚙️ Open menu',
|
title="⚙️ Open menu",
|
||||||
input_message_content=InputTextMessageContent(
|
input_message_content=InputTextMessageContent(message_text="⚙️ Menu"),
|
||||||
message_text='⚙️ Menu'
|
reply_markup=get_full_menu_kb(),
|
||||||
),
|
|
||||||
reply_markup=get_full_menu_kb()
|
|
||||||
)
|
)
|
||||||
], cache_time=0
|
],
|
||||||
|
cache_time=0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from bot.filters import ServiceSearchFilter
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(ServiceSearchFilter('error'))
|
@router.inline_query(ServiceSearchFilter("error"))
|
||||||
async def search_spotify_inline_query(inline_query: InlineQuery):
|
async def search_spotify_inline_query(inline_query: InlineQuery):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await get_error_search_results(inline_query.query.removeprefix('error:')),
|
await get_error_search_results(inline_query.query.removeprefix("error:")),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
|
|
||||||
from . import (on_inline_spotify, on_inline_deezer, on_inline_youtube,
|
from . import (
|
||||||
on_inline_soundcloud)
|
on_inline_spotify,
|
||||||
|
on_inline_deezer,
|
||||||
|
on_inline_youtube,
|
||||||
|
on_inline_soundcloud,
|
||||||
|
)
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
router.include_routers(
|
router.include_routers(
|
||||||
on_inline_spotify.router,
|
on_inline_spotify.router,
|
||||||
on_inline_deezer.router,
|
on_inline_deezer.router,
|
||||||
on_inline_youtube.router,
|
on_inline_youtube.router,
|
||||||
on_inline_soundcloud.router
|
on_inline_soundcloud.router,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ from bot.modules.settings import UserSettings
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(ServiceSearchFilter('d'))
|
@router.inline_query(ServiceSearchFilter("d"))
|
||||||
async def search_deezer_inline_query(inline_query: InlineQuery, settings: UserSettings):
|
async def search_deezer_inline_query(inline_query: InlineQuery, settings: UserSettings):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await get_deezer_search_results(
|
await get_deezer_search_results(
|
||||||
inline_query.query.removeprefix('d:'),
|
inline_query.query.removeprefix("d:"), settings
|
||||||
settings
|
|
||||||
),
|
),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ from bot.modules.settings import UserSettings
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(ServiceSearchMultiletterFilter(['c', 'с']))
|
@router.inline_query(ServiceSearchMultiletterFilter(["c", "с"]))
|
||||||
async def search_soundcloud_inline_query(
|
async def search_soundcloud_inline_query(
|
||||||
inline_query: InlineQuery,
|
inline_query: InlineQuery, settings: UserSettings
|
||||||
settings: UserSettings
|
|
||||||
):
|
):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await get_soundcloud_search_results(
|
await get_soundcloud_search_results(
|
||||||
inline_query.query.removeprefix('c:').removesuffix('с:'),
|
inline_query.query.removeprefix("c:").removesuffix("с:"), settings
|
||||||
settings
|
|
||||||
),
|
),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ from bot.modules.settings import UserSettings
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(ServiceSearchFilter('s'))
|
@router.inline_query(ServiceSearchFilter("s"))
|
||||||
async def search_spotify_inline_query(
|
async def search_spotify_inline_query(
|
||||||
inline_query: InlineQuery,
|
inline_query: InlineQuery, settings: UserSettings
|
||||||
settings: UserSettings
|
|
||||||
):
|
):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await get_spotify_search_results(inline_query.query.removeprefix('s:'),
|
await get_spotify_search_results(
|
||||||
settings),
|
inline_query.query.removeprefix("s:"), settings
|
||||||
|
),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ from bot.modules.settings import UserSettings
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query(ServiceSearchFilter('y'))
|
@router.inline_query(ServiceSearchFilter("y"))
|
||||||
async def search_youtube_inline_query(inline_query: InlineQuery,
|
async def search_youtube_inline_query(
|
||||||
settings: UserSettings):
|
inline_query: InlineQuery, settings: UserSettings
|
||||||
|
):
|
||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await get_youtube_search_results(inline_query.query.removeprefix('y:'),
|
await get_youtube_search_results(
|
||||||
settings),
|
inline_query.query.removeprefix("y:"), settings
|
||||||
|
),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ async def url_deezer_inline_query(inline_query: InlineQuery, settings: UserSetti
|
|||||||
await inline_query.answer(
|
await inline_query.answer(
|
||||||
await get_url_results(inline_query.query, settings),
|
await get_url_results(inline_query.query, settings),
|
||||||
cache_time=0,
|
cache_time=0,
|
||||||
is_personal=True
|
is_personal=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ router.include_routers(
|
|||||||
suppress_verify.router,
|
suppress_verify.router,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['router']
|
__all__ = ["router"]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from aiogram import Router, Bot, F
|
from aiogram import Router, Bot, F
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
BufferedInputFile, URLInputFile, InputMediaAudio,
|
BufferedInputFile,
|
||||||
|
URLInputFile,
|
||||||
|
InputMediaAudio,
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,11 +13,11 @@ from bot.modules.database import db
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.chosen_inline_result(F.result_id.startswith('deez::'))
|
@router.chosen_inline_result(F.result_id.startswith("deez::"))
|
||||||
async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
||||||
bytestream: DeezerBytestream = await (await deezer.downloader.from_id(
|
bytestream: DeezerBytestream = await (
|
||||||
chosen_result.result_id.removeprefix('deez::')
|
await deezer.downloader.from_id(chosen_result.result_id.removeprefix("deez::"))
|
||||||
)).to_bytestream()
|
).to_bytestream()
|
||||||
|
|
||||||
audio = await bot.send_audio(
|
audio = await bot.send_audio(
|
||||||
chat_id=config.telegram.files_chat,
|
chat_id=config.telegram.files_chat,
|
||||||
@@ -34,5 +36,5 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
|||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id),
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from aiogram import Router, Bot, F
|
from aiogram import Router, Bot, F
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
BufferedInputFile, InputMediaAudio,
|
BufferedInputFile,
|
||||||
|
InputMediaAudio,
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,42 +17,37 @@ router = Router()
|
|||||||
|
|
||||||
|
|
||||||
@router.chosen_inline_result(
|
@router.chosen_inline_result(
|
||||||
F.result_id.startswith('spotc::') | F.result_id.startswith('ytc::')
|
F.result_id.startswith("spotc::") | F.result_id.startswith("ytc::")
|
||||||
)
|
)
|
||||||
async def on_cached_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
async def on_cached_chosen(
|
||||||
settings: UserSettings):
|
chosen_result: ChosenInlineResult, bot: Bot, settings: UserSettings
|
||||||
if settings['recode_youtube'].value != 'yes':
|
):
|
||||||
|
if settings["recode_youtube"].value != "yes":
|
||||||
await bot.edit_message_reply_markup(
|
await bot.edit_message_reply_markup(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id, reply_markup=None
|
||||||
reply_markup=None
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (
|
if type(
|
||||||
type(
|
db.recoded.get(
|
||||||
db.recoded.get(
|
song_id := chosen_result.result_id.removeprefix("spotc::").removeprefix(
|
||||||
song_id := chosen_result.result_id
|
"ytc::"
|
||||||
.removeprefix('spotc::')
|
|
||||||
.removeprefix('ytc::')
|
|
||||||
)
|
)
|
||||||
) in [bool, type(None)]
|
)
|
||||||
):
|
) in [bool, type(None)]:
|
||||||
await bot.edit_message_reply_markup(
|
await bot.edit_message_reply_markup(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id, reply_markup=None
|
||||||
reply_markup=None
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🔄 Recoding...',
|
caption="🔄 Recoding...",
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
message = await bot.forward_message(
|
message = await bot.forward_message(
|
||||||
config.telegram.files_chat,
|
config.telegram.files_chat, config.telegram.files_chat, db.recoded[song_id]
|
||||||
config.telegram.files_chat,
|
|
||||||
db.recoded[song_id]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
song_io: BytesIO = await bot.download( # type: ignore
|
song_io: BytesIO = await bot.download( # type: ignore
|
||||||
@@ -76,7 +72,7 @@ async def on_cached_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
),
|
),
|
||||||
thumbnail=BufferedInputFile(
|
thumbnail=BufferedInputFile(
|
||||||
file=(await bot.download(message.audio.thumbnail.file_id)).read(),
|
file=(await bot.download(message.audio.thumbnail.file_id)).read(),
|
||||||
filename='thumbnail.jpg'
|
filename="thumbnail.jpg",
|
||||||
),
|
),
|
||||||
performer=message.audio.performer,
|
performer=message.audio.performer,
|
||||||
title=message.audio.title,
|
title=message.audio.title,
|
||||||
@@ -85,15 +81,15 @@ async def on_cached_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
|
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='',
|
caption="",
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id)
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
if chosen_result.result_id.startswith('spotc::'):
|
if chosen_result.result_id.startswith("spotc::"):
|
||||||
db.spotify[song_id] = audio.audio.file_id
|
db.spotify[song_id] = audio.audio.file_id
|
||||||
else:
|
else:
|
||||||
db.youtube[song_id] = audio.audio.file_id
|
db.youtube[song_id] = audio.audio.file_id
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from aiogram import Router, Bot, F
|
from aiogram import Router, Bot, F
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
BufferedInputFile, URLInputFile, InputMediaAudio,
|
BufferedInputFile,
|
||||||
|
URLInputFile,
|
||||||
|
InputMediaAudio,
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,11 +13,13 @@ from bot.modules.database import db
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.chosen_inline_result(F.result_id.startswith('sc::'))
|
@router.chosen_inline_result(F.result_id.startswith("sc::"))
|
||||||
async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
||||||
bytestream: SoundCloudBytestream = await (await soundcloud.downloader.from_id(
|
bytestream: SoundCloudBytestream = await (
|
||||||
chosen_result.result_id.removeprefix('sc::')
|
await soundcloud.downloader.from_id(
|
||||||
)).to_bytestream()
|
chosen_result.result_id.removeprefix("sc::")
|
||||||
|
)
|
||||||
|
).to_bytestream()
|
||||||
|
|
||||||
audio = await bot.send_audio(
|
audio = await bot.send_audio(
|
||||||
chat_id=config.telegram.files_chat,
|
chat_id=config.telegram.files_chat,
|
||||||
@@ -33,5 +37,5 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
|||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id),
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from aiogram import Router, Bot, F
|
from aiogram import Router, Bot, F
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
BufferedInputFile, URLInputFile, InputMediaAudio,
|
BufferedInputFile,
|
||||||
|
URLInputFile,
|
||||||
|
InputMediaAudio,
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,16 +18,17 @@ router = Router()
|
|||||||
|
|
||||||
|
|
||||||
def not_strict_name(song, yt_song):
|
def not_strict_name(song, yt_song):
|
||||||
if 'feat' in yt_song.name.lower():
|
if "feat" in yt_song.name.lower():
|
||||||
return any(artist.lower() in yt_song.name.lower() for artist in song.artists)
|
return any(artist.lower() in yt_song.name.lower() for artist in song.artists)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router.chosen_inline_result(F.result_id.startswith('spot::'))
|
@router.chosen_inline_result(F.result_id.startswith("spot::"))
|
||||||
async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
async def on_new_chosen(
|
||||||
settings: UserSettings):
|
chosen_result: ChosenInlineResult, bot: Bot, settings: UserSettings
|
||||||
song = spotify.songs.from_id(chosen_result.result_id.removeprefix('spot::'))
|
):
|
||||||
|
song = spotify.songs.from_id(chosen_result.result_id.removeprefix("spot::"))
|
||||||
|
|
||||||
bytestream = None
|
bytestream = None
|
||||||
audio = None
|
audio = None
|
||||||
@@ -34,14 +37,15 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
song.full_name,
|
song.full_name,
|
||||||
exact_match=True,
|
exact_match=True,
|
||||||
)
|
)
|
||||||
if settings['exact_spotify_search'].value == 'yes':
|
if settings["exact_spotify_search"].value == "yes":
|
||||||
if ((song.all_artists != yt_song.all_artists or song.name != yt_song.name)
|
if (
|
||||||
and not not_strict_name(song, yt_song)):
|
song.all_artists != yt_song.all_artists or song.name != yt_song.name
|
||||||
|
) and not not_strict_name(song, yt_song):
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🙄 Cannot find this song on YouTube, trying Deezer...',
|
caption="🙄 Cannot find this song on YouTube, trying Deezer...",
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
parse_mode='HTML',
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
yt_song = None
|
yt_song = None
|
||||||
bytestream = False
|
bytestream = False
|
||||||
@@ -66,9 +70,9 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
except AgeRestrictedError:
|
except AgeRestrictedError:
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🔞 This song is age restricted, trying Deezer...',
|
caption="🔞 This song is age restricted, trying Deezer...",
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
parse_mode='HTML',
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
yt_song = None
|
yt_song = None
|
||||||
|
|
||||||
@@ -99,29 +103,29 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
assert e
|
assert e
|
||||||
|
|
||||||
if audio:
|
if audio:
|
||||||
if settings['exact_spotify_search'].value == 'yes':
|
if settings["exact_spotify_search"].value == "yes":
|
||||||
db.spotify[song.id] = audio.audio.file_id
|
db.spotify[song.id] = audio.audio.file_id
|
||||||
|
|
||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id),
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🤷♂️ Cannot download this song',
|
caption="🤷♂️ Cannot download this song",
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
parse_mode='HTML',
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
|
|
||||||
if yt_song and settings['recode_youtube'].value == 'yes':
|
if yt_song and settings["recode_youtube"].value == "yes":
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🔄 Recoding...',
|
caption="🔄 Recoding...",
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
parse_mode='HTML',
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
await bytestream.rerender()
|
await bytestream.rerender()
|
||||||
|
|
||||||
@@ -139,20 +143,20 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
db.youtube[yt_song.id] = audio.audio.file_id
|
db.youtube[yt_song.id] = audio.audio.file_id
|
||||||
db.recoded[yt_song.id] = True
|
db.recoded[yt_song.id] = True
|
||||||
|
|
||||||
if settings['exact_spotify_search'].value == 'yes':
|
if settings["exact_spotify_search"].value == "yes":
|
||||||
db.spotify[song.id] = audio.audio.file_id
|
db.spotify[song.id] = audio.audio.file_id
|
||||||
db.recoded[song.id] = True
|
db.recoded[song.id] = True
|
||||||
|
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='',
|
caption="",
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id)
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
)
|
)
|
||||||
elif yt_song and settings['recode_youtube'].value == 'no':
|
elif yt_song and settings["recode_youtube"].value == "no":
|
||||||
db.recoded[yt_song.id] = audio.message_id
|
db.recoded[yt_song.id] = audio.message_id
|
||||||
if settings['exact_spotify_search'].value == 'yes':
|
if settings["exact_spotify_search"].value == "yes":
|
||||||
db.recoded[song.id] = audio.message_id
|
db.recoded[song.id] = audio.message_id
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ router = Router()
|
|||||||
|
|
||||||
|
|
||||||
@router.chosen_inline_result(
|
@router.chosen_inline_result(
|
||||||
F.result_id.startswith('deezc::') | F.result_id.startswith('scc::')
|
F.result_id.startswith("deezc::") | F.result_id.startswith("scc::")
|
||||||
)
|
)
|
||||||
async def on_unneeded_cached_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
async def on_unneeded_cached_chosen(chosen_result: ChosenInlineResult, bot: Bot):
|
||||||
await bot.edit_message_reply_markup(
|
await bot.edit_message_reply_markup(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id, reply_markup=None
|
||||||
reply_markup=None
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from aiogram import Router, Bot, F
|
from aiogram import Router, Bot, F
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
BufferedInputFile, URLInputFile, InputMediaAudio,
|
BufferedInputFile,
|
||||||
|
URLInputFile,
|
||||||
|
InputMediaAudio,
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,19 +14,20 @@ from bot.modules.settings import UserSettings
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.chosen_inline_result(F.result_id.startswith('yt::'))
|
@router.chosen_inline_result(F.result_id.startswith("yt::"))
|
||||||
async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
async def on_new_chosen(
|
||||||
settings: UserSettings):
|
chosen_result: ChosenInlineResult, bot: Bot, settings: UserSettings
|
||||||
song = youtube.songs.from_id(chosen_result.result_id.removeprefix('yt::'))
|
):
|
||||||
|
song = youtube.songs.from_id(chosen_result.result_id.removeprefix("yt::"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bytestream = await song.to_bytestream()
|
bytestream = await song.to_bytestream()
|
||||||
except AgeRestrictedError:
|
except AgeRestrictedError:
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🔞 This song is age restricted, so I can\'t download it. '
|
caption="🔞 This song is age restricted, so I can't download it. "
|
||||||
'Try downloading it from Deezer or SoundCloud',
|
"Try downloading it from Deezer or SoundCloud",
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -45,14 +48,14 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id),
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings['recode_youtube'].value == 'yes':
|
if settings["recode_youtube"].value == "yes":
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
caption='🔄 Recoding...',
|
caption="🔄 Recoding...",
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
await bytestream.rerender()
|
await bytestream.rerender()
|
||||||
@@ -75,7 +78,7 @@ async def on_new_chosen(chosen_result: ChosenInlineResult, bot: Bot,
|
|||||||
await bot.edit_message_media(
|
await bot.edit_message_media(
|
||||||
inline_message_id=chosen_result.inline_message_id,
|
inline_message_id=chosen_result.inline_message_id,
|
||||||
media=InputMediaAudio(media=audio.audio.file_id),
|
media=InputMediaAudio(media=audio.audio.file_id),
|
||||||
reply_markup=None
|
reply_markup=None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
db.recoded[song.id] = audio.message_id
|
db.recoded[song.id] = audio.message_id
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
from aiogram.utils.keyboard import (InlineKeyboardMarkup, InlineKeyboardButton,
|
from aiogram.utils.keyboard import (
|
||||||
InlineKeyboardBuilder)
|
InlineKeyboardMarkup,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardBuilder,
|
||||||
|
)
|
||||||
from bot.factories.full_menu import FullMenuCallback
|
from bot.factories.full_menu import FullMenuCallback
|
||||||
|
|
||||||
from bot.keyboards.inline import search_variants as sv
|
from bot.keyboards.inline import search_variants as sv
|
||||||
|
|
||||||
|
|
||||||
def get_full_menu_kb() -> InlineKeyboardMarkup:
|
def get_full_menu_kb() -> InlineKeyboardMarkup:
|
||||||
buttons = (sv.get_search_variants(
|
buttons = sv.get_search_variants(
|
||||||
query='',
|
query="", services=sv.soundcloud | sv.spotify | sv.deezer | sv.youtube
|
||||||
services=
|
|
||||||
sv.soundcloud |
|
|
||||||
sv.spotify |
|
|
||||||
sv.deezer |
|
|
||||||
sv.youtube
|
|
||||||
) + [
|
) + [
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text='⚙️ Settings',
|
text="⚙️ Settings",
|
||||||
callback_data=FullMenuCallback(
|
callback_data=FullMenuCallback(action="settings").pack(),
|
||||||
action='settings'
|
)
|
||||||
).pack()
|
],
|
||||||
)
|
]
|
||||||
],
|
|
||||||
])
|
|
||||||
|
|
||||||
return InlineKeyboardBuilder(buttons).as_markup()
|
return InlineKeyboardBuilder(buttons).as_markup()
|
||||||
|
|||||||
@@ -1,42 +1,34 @@
|
|||||||
from aiogram.utils.keyboard import (InlineKeyboardMarkup, InlineKeyboardButton,
|
from aiogram.utils.keyboard import (
|
||||||
InlineKeyboardBuilder)
|
InlineKeyboardMarkup,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardBuilder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
deezer = {
|
deezer = {"d": "🎵 Search in Deezer"}
|
||||||
'd': '🎵 Search in Deezer'
|
soundcloud = {"c": "☁️ Search in SoundCloud"}
|
||||||
}
|
youtube = {"y": "▶️ Search in YouTube"}
|
||||||
soundcloud = {
|
spotify = {"s": "🎧 Search in Spotify"}
|
||||||
'c': '☁️ Search in SoundCloud'
|
|
||||||
}
|
|
||||||
youtube = {
|
|
||||||
'y': '▶️ Search in YouTube'
|
|
||||||
}
|
|
||||||
spotify = {
|
|
||||||
's': '🎧 Search in Spotify'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_search_variants(
|
def get_search_variants(
|
||||||
query: str,
|
query: str,
|
||||||
services: dict[str, str],
|
services: dict[str, str],
|
||||||
) -> list[list[InlineKeyboardButton]]:
|
) -> list[list[InlineKeyboardButton]]:
|
||||||
buttons = [
|
buttons = [
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=services[key],
|
text=services[key], switch_inline_query_current_chat=f"{key}:{query}"
|
||||||
switch_inline_query_current_chat=f'{key}:{query}'
|
|
||||||
)
|
)
|
||||||
] for key in services.keys()
|
]
|
||||||
|
for key in services.keys()
|
||||||
]
|
]
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
def get_search_variants_kb(
|
def get_search_variants_kb(
|
||||||
query: str,
|
query: str,
|
||||||
services: dict[str, str],
|
services: dict[str, str],
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
return InlineKeyboardBuilder(get_search_variants(
|
return InlineKeyboardBuilder(get_search_variants(query, services)).as_markup()
|
||||||
query,
|
|
||||||
services
|
|
||||||
)).as_markup()
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from aiogram.utils.keyboard import (InlineKeyboardMarkup, InlineKeyboardButton,
|
from aiogram.utils.keyboard import (
|
||||||
InlineKeyboardBuilder)
|
InlineKeyboardMarkup,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardBuilder,
|
||||||
|
)
|
||||||
from bot.factories.open_setting import SettingChoiceCallback
|
from bot.factories.open_setting import SettingChoiceCallback
|
||||||
from bot.factories.full_menu import FullMenuCallback
|
from bot.factories.full_menu import FullMenuCallback
|
||||||
|
|
||||||
@@ -11,22 +14,21 @@ def get_setting_kb(s_id: str, user_id: str) -> InlineKeyboardMarkup:
|
|||||||
buttons = [
|
buttons = [
|
||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=(
|
text=("✅ " if setting.value == choice else "")
|
||||||
'✅ ' if setting.value == choice else ''
|
+ setting.choices[choice],
|
||||||
) + setting.choices[choice],
|
|
||||||
callback_data=SettingChoiceCallback(
|
callback_data=SettingChoiceCallback(
|
||||||
s_id=s_id,
|
s_id=s_id,
|
||||||
choice=choice,
|
choice=choice,
|
||||||
).pack()
|
).pack(),
|
||||||
)
|
)
|
||||||
] for choice in setting.choices.keys()
|
]
|
||||||
] + [[
|
for choice in setting.choices.keys()
|
||||||
InlineKeyboardButton(
|
] + [
|
||||||
text='🔙',
|
[
|
||||||
callback_data=FullMenuCallback(
|
InlineKeyboardButton(
|
||||||
action='settings'
|
text="🔙", callback_data=FullMenuCallback(action="settings").pack()
|
||||||
).pack()
|
)
|
||||||
)
|
]
|
||||||
]]
|
]
|
||||||
|
|
||||||
return InlineKeyboardBuilder(buttons).as_markup()
|
return InlineKeyboardBuilder(buttons).as_markup()
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from aiogram.utils.keyboard import (InlineKeyboardMarkup, InlineKeyboardButton,
|
from aiogram.utils.keyboard import (
|
||||||
InlineKeyboardBuilder)
|
InlineKeyboardMarkup,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardBuilder,
|
||||||
|
)
|
||||||
from bot.factories.open_setting import OpenSettingCallback
|
from bot.factories.open_setting import OpenSettingCallback
|
||||||
from bot.factories.full_menu import FullMenuCallback
|
from bot.factories.full_menu import FullMenuCallback
|
||||||
|
|
||||||
@@ -13,16 +16,16 @@ def get_settings_kb() -> InlineKeyboardMarkup:
|
|||||||
text=settings_strings[setting_id].name,
|
text=settings_strings[setting_id].name,
|
||||||
callback_data=OpenSettingCallback(
|
callback_data=OpenSettingCallback(
|
||||||
s_id=setting_id,
|
s_id=setting_id,
|
||||||
).pack()
|
).pack(),
|
||||||
)
|
)
|
||||||
] for setting_id in settings_strings.keys()
|
]
|
||||||
] + [[
|
for setting_id in settings_strings.keys()
|
||||||
InlineKeyboardButton(
|
] + [
|
||||||
text='🔙',
|
[
|
||||||
callback_data=FullMenuCallback(
|
InlineKeyboardButton(
|
||||||
action='home'
|
text="🔙", callback_data=FullMenuCallback(action="home").pack()
|
||||||
).pack()
|
)
|
||||||
)
|
]
|
||||||
]]
|
]
|
||||||
|
|
||||||
return InlineKeyboardBuilder(buttons).as_markup()
|
return InlineKeyboardBuilder(buttons).as_markup()
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ from bot.modules.settings import UserSettings
|
|||||||
|
|
||||||
class SettingsInjectorMiddleware(BaseMiddleware):
|
class SettingsInjectorMiddleware(BaseMiddleware):
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
event: TelegramObject,
|
event: TelegramObject,
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
):
|
):
|
||||||
if (not hasattr(event, 'from_user') and
|
if not hasattr(event, "from_user") and (
|
||||||
(not hasattr(event, 'inline_query') or event.inline_query is None)):
|
not hasattr(event, "inline_query") or event.inline_query is None
|
||||||
|
):
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
elif hasattr(event, 'inline_query') and event.inline_query is not None:
|
elif hasattr(event, "inline_query") and event.inline_query is not None:
|
||||||
settings = UserSettings(event.inline_query.from_user.id)
|
settings = UserSettings(event.inline_query.from_user.id)
|
||||||
data['settings'] = settings
|
data["settings"] = settings
|
||||||
else:
|
else:
|
||||||
settings = UserSettings(event.from_user.id)
|
settings = UserSettings(event.from_user.id)
|
||||||
data['settings'] = settings
|
data["settings"] = settings
|
||||||
|
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from bot.modules.database import db
|
|||||||
|
|
||||||
class PrivateButtonMiddleware(BaseMiddleware):
|
class PrivateButtonMiddleware(BaseMiddleware):
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
handler: Callable[[CallbackQuery, Dict[str, Any]], Awaitable[Any]],
|
handler: Callable[[CallbackQuery, Dict[str, Any]], Awaitable[Any]],
|
||||||
event: CallbackQuery,
|
event: CallbackQuery,
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
):
|
):
|
||||||
if event.from_user.id == db.inline[event.inline_message_id].from_user.id:
|
if event.from_user.id == db.inline[event.inline_message_id].from_user.id:
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
else:
|
else:
|
||||||
await event.answer('This button is not for you')
|
await event.answer("This button is not for you")
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ class SavedResult:
|
|||||||
|
|
||||||
class SaveChosenMiddleware(BaseMiddleware):
|
class SaveChosenMiddleware(BaseMiddleware):
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
handler: Callable[[ChosenInlineResult, Dict[str, Any]], Awaitable[Any]],
|
handler: Callable[[ChosenInlineResult, Dict[str, Any]], Awaitable[Any]],
|
||||||
event: ChosenInlineResult,
|
event: ChosenInlineResult,
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
):
|
):
|
||||||
db.inline[event.inline_message_id] = SavedResult(
|
db.inline[event.inline_message_id] = SavedResult(
|
||||||
result_id=event.result_id,
|
result_id=event.result_id,
|
||||||
@@ -38,9 +38,9 @@ class SaveChosenMiddleware(BaseMiddleware):
|
|||||||
first_name=event.from_user.first_name,
|
first_name=event.from_user.first_name,
|
||||||
last_name=event.from_user.last_name,
|
last_name=event.from_user.last_name,
|
||||||
username=event.from_user.username,
|
username=event.from_user.username,
|
||||||
language_code=event.from_user.language_code
|
language_code=event.from_user.language_code,
|
||||||
),
|
),
|
||||||
query=event.query,
|
query=event.query,
|
||||||
inline_message_id=event.inline_message_id
|
inline_message_id=event.inline_message_id,
|
||||||
)
|
)
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class BaseSongItem:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def all_artists(self):
|
def all_artists(self):
|
||||||
return ', '.join(self.artists)
|
return ", ".join(self.artists)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ from .db import Db
|
|||||||
|
|
||||||
db = Db()
|
db = Db()
|
||||||
|
|
||||||
__all__ = ['db']
|
__all__ = ["db"]
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ from .db_model import DBDict
|
|||||||
|
|
||||||
class Db(object):
|
class Db(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.fsm = DBDict('fsm')
|
self.fsm = DBDict("fsm")
|
||||||
self.config = DBDict('config')
|
self.config = DBDict("config")
|
||||||
self.inline = DBDict('inline')
|
self.inline = DBDict("inline")
|
||||||
self.errors = DBDict('errors')
|
self.errors = DBDict("errors")
|
||||||
self.settings = DBDict('settings')
|
self.settings = DBDict("settings")
|
||||||
self.spotify = DBDict('spotify')
|
self.spotify = DBDict("spotify")
|
||||||
self.deezer = DBDict('deezer')
|
self.deezer = DBDict("deezer")
|
||||||
self.youtube = DBDict('youtube')
|
self.youtube = DBDict("youtube")
|
||||||
self.soundcloud = DBDict('soundcloud')
|
self.soundcloud = DBDict("soundcloud")
|
||||||
self.recoded = DBDict('recoded')
|
self.recoded = DBDict("recoded")
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ deezer = Deezer(
|
|||||||
arl=config.tokens.deezer.arl,
|
arl=config.tokens.deezer.arl,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['deezer', 'DeezerBytestream']
|
__all__ = ["deezer", "DeezerBytestream"]
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ class DeezerBytestream:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytestream(
|
def from_bytestream(
|
||||||
cls,
|
cls, bytestream: BytesIO, filename: str, full_song: FullSongItem
|
||||||
bytestream: BytesIO,
|
|
||||||
filename: str,
|
|
||||||
full_song: FullSongItem
|
|
||||||
):
|
):
|
||||||
bytestream.seek(0)
|
bytestream.seek(0)
|
||||||
return cls(
|
return cls(
|
||||||
@@ -38,21 +35,18 @@ class Downloader:
|
|||||||
song: FullSongItem
|
song: FullSongItem
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def build(
|
async def build(cls, song_id: str, driver: DeezerDriver):
|
||||||
cls,
|
|
||||||
song_id: str,
|
|
||||||
driver: DeezerDriver
|
|
||||||
):
|
|
||||||
track = await driver.reverse_get_track(song_id)
|
track = await driver.reverse_get_track(song_id)
|
||||||
try:
|
try:
|
||||||
return cls(
|
return cls(
|
||||||
song_id=str(song_id),
|
song_id=str(song_id),
|
||||||
driver=driver,
|
driver=driver,
|
||||||
track=track['results'],
|
track=track["results"],
|
||||||
song=await FullSongItem.from_deezer(track)
|
song=await FullSongItem.from_deezer(track),
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
from icecream import ic
|
from icecream import ic
|
||||||
|
|
||||||
ic(track)
|
ic(track)
|
||||||
await driver.renew_engine()
|
await driver.renew_engine()
|
||||||
return await cls.build(song_id, driver)
|
return await cls.build(song_id, driver)
|
||||||
@@ -65,7 +59,7 @@ class Downloader:
|
|||||||
audio = BytesIO()
|
audio = BytesIO()
|
||||||
|
|
||||||
async for chunk in self.driver.engine.get_data_iter(
|
async for chunk in self.driver.engine.get_data_iter(
|
||||||
await self._get_download_url(quality=quality)
|
await self._get_download_url(quality=quality)
|
||||||
):
|
):
|
||||||
if i % 3 > 0 or len(chunk) < 2 * 1024:
|
if i % 3 > 0 or len(chunk) < 2 * 1024:
|
||||||
audio.write(chunk)
|
audio.write(chunk)
|
||||||
@@ -76,18 +70,16 @@ class Downloader:
|
|||||||
return DeezerBytestream.from_bytestream(
|
return DeezerBytestream.from_bytestream(
|
||||||
filename=self.song.full_name + track_formats.TRACK_FORMAT_MAP[quality].ext,
|
filename=self.song.full_name + track_formats.TRACK_FORMAT_MAP[quality].ext,
|
||||||
bytestream=audio,
|
bytestream=audio,
|
||||||
full_song=self.song
|
full_song=self.song,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _get_download_url(self, quality: str = 'MP3_128'):
|
async def _get_download_url(self, quality: str = "MP3_128"):
|
||||||
md5_origin = self.track["MD5_ORIGIN"]
|
md5_origin = self.track["MD5_ORIGIN"]
|
||||||
track_id = self.track["SNG_ID"]
|
track_id = self.track["SNG_ID"]
|
||||||
media_version = self.track["MEDIA_VERSION"]
|
media_version = self.track["MEDIA_VERSION"]
|
||||||
|
|
||||||
url_decrypter = UrlDecrypter(
|
url_decrypter = UrlDecrypter(
|
||||||
md5_origin=md5_origin,
|
md5_origin=md5_origin, track_id=track_id, media_version=media_version
|
||||||
track_id=track_id,
|
|
||||||
media_version=media_version
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return url_decrypter.get_url_for(track_formats.TRACK_FORMAT_MAP[quality])
|
return url_decrypter.get_url_for(track_formats.TRACK_FORMAT_MAP[quality])
|
||||||
@@ -98,7 +90,4 @@ class DownloaderBuilder:
|
|||||||
driver: DeezerDriver
|
driver: DeezerDriver
|
||||||
|
|
||||||
async def from_id(self, song_id: str):
|
async def from_id(self, song_id: str):
|
||||||
return await Downloader.build(
|
return await Downloader.build(song_id=song_id, driver=self.driver)
|
||||||
song_id=song_id,
|
|
||||||
driver=self.driver
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -10,30 +10,19 @@ class DeezerDriver:
|
|||||||
engine: DeezerEngine
|
engine: DeezerEngine
|
||||||
|
|
||||||
async def get_track(self, track_id: int | str):
|
async def get_track(self, track_id: int | str):
|
||||||
data = await self.engine.call_legacy_api(
|
data = await self.engine.call_legacy_api(f"track/{track_id}")
|
||||||
f'track/{track_id}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def reverse_get_track(self, track_id: str):
|
async def reverse_get_track(self, track_id: str):
|
||||||
return await self.engine.call_api(
|
return await self.engine.call_api("song.getData", params={"SNG_ID": track_id})
|
||||||
'song.getData',
|
|
||||||
params={
|
|
||||||
'SNG_ID': track_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def search(self, query: str, limit: int = 30):
|
async def search(self, query: str, limit: int = 30):
|
||||||
data = await self.engine.call_legacy_api(
|
data = await self.engine.call_legacy_api(
|
||||||
'search/track',
|
"search/track", params={"q": clean_query(query), "limit": limit}
|
||||||
params={
|
|
||||||
'q': clean_query(query),
|
|
||||||
'limit': limit
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return data['data']
|
return data["data"]
|
||||||
|
|
||||||
async def renew_engine(self):
|
async def renew_engine(self):
|
||||||
self.engine = await self.engine.from_arl(self.engine.arl)
|
self.engine = await self.engine.from_arl(self.engine.arl)
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from attrs import define
|
|||||||
|
|
||||||
HTTP_HEADERS = {
|
HTTP_HEADERS = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
"(KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
"(KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||||
"Content-Language": "en-US",
|
"Content-Language": "en-US",
|
||||||
"Cache-Control": "max-age=0",
|
"Cache-Control": "max-age=0",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
|
"Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
|
||||||
"Accept-Language": "en-US,en;q=0.9,en-US;q=0.8,en;q=0.7",
|
"Accept-Language": "en-US,en;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
"Connection": 'keep-alive'
|
"Connection": "keep-alive",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -25,28 +25,22 @@ class DeezerEngine:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_arl(cls, arl: str):
|
async def from_arl(cls, arl: str):
|
||||||
cookies = {'arl': arl}
|
cookies = {"arl": arl}
|
||||||
data, cookies = await cls(cookies).call_api(
|
data, cookies = await cls(cookies).call_api(
|
||||||
'deezer.getUserData', get_cookies=True
|
"deezer.getUserData", get_cookies=True
|
||||||
)
|
)
|
||||||
|
|
||||||
data = data['results']
|
data = data["results"]
|
||||||
token = data['checkForm']
|
token = data["checkForm"]
|
||||||
|
|
||||||
return cls(
|
return cls(cookies=cookies, arl=arl, token=token)
|
||||||
cookies=cookies,
|
|
||||||
arl=arl,
|
|
||||||
token=token
|
|
||||||
)
|
|
||||||
|
|
||||||
async def call_legacy_api(
|
async def call_legacy_api(self, request_point: str, params: dict = None):
|
||||||
self, request_point: str, params: dict = None
|
|
||||||
):
|
|
||||||
async with aiohttp.ClientSession(cookies=self.cookies) as session:
|
async with aiohttp.ClientSession(cookies=self.cookies) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
f"https://api.deezer.com/{request_point}",
|
f"https://api.deezer.com/{request_point}",
|
||||||
params=params,
|
params=params,
|
||||||
headers=HTTP_HEADERS
|
headers=HTTP_HEADERS,
|
||||||
) as r:
|
) as r:
|
||||||
return await r.json()
|
return await r.json()
|
||||||
|
|
||||||
@@ -63,31 +57,26 @@ class DeezerEngine:
|
|||||||
|
|
||||||
async def get_data_iter(self, url: str):
|
async def get_data_iter(self, url: str):
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
cookies=self.cookies,
|
cookies=self.cookies, headers=HTTP_HEADERS
|
||||||
headers=HTTP_HEADERS
|
|
||||||
) as session:
|
) as session:
|
||||||
r = await session.get(
|
r = await session.get(url, allow_redirects=True)
|
||||||
url,
|
|
||||||
allow_redirects=True
|
|
||||||
)
|
|
||||||
async for chunk in self._iter_exact_chunks(r):
|
async for chunk in self._iter_exact_chunks(r):
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
async def call_api(
|
async def call_api(
|
||||||
self, method: str, params: dict = None,
|
self, method: str, params: dict = None, get_cookies: bool = False
|
||||||
get_cookies: bool = False
|
|
||||||
):
|
):
|
||||||
async with aiohttp.ClientSession(cookies=self.cookies) as session:
|
async with aiohttp.ClientSession(cookies=self.cookies) as session:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
f"https://www.deezer.com/ajax/gw-light.php",
|
f"https://www.deezer.com/ajax/gw-light.php",
|
||||||
params={
|
params={
|
||||||
'method': method,
|
"method": method,
|
||||||
'api_version': '1.0',
|
"api_version": "1.0",
|
||||||
'input': '3',
|
"input": "3",
|
||||||
'api_token': self.token or 'null',
|
"api_token": self.token or "null",
|
||||||
},
|
},
|
||||||
headers=HTTP_HEADERS,
|
headers=HTTP_HEADERS,
|
||||||
json=params
|
json=params,
|
||||||
) as r:
|
) as r:
|
||||||
if not get_cookies:
|
if not get_cookies:
|
||||||
return await r.json()
|
return await r.json()
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ class SongItem(BaseSongItem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_deezer(cls, song_item: dict):
|
def from_deezer(cls, song_item: dict):
|
||||||
return cls(
|
return cls(
|
||||||
name=song_item['title'],
|
name=song_item["title"],
|
||||||
id=str(song_item['id']),
|
id=str(song_item["id"]),
|
||||||
artists=[song_item['artist']['name']],
|
artists=[song_item["artist"]["name"]],
|
||||||
preview_url=song_item.get('preview'),
|
preview_url=song_item.get("preview"),
|
||||||
thumbnail=song_item['album']['cover_medium']
|
thumbnail=song_item["album"]["cover_medium"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,21 +25,23 @@ class FullSongItem(BaseSongItem):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_deezer(cls, song_item: dict):
|
async def from_deezer(cls, song_item: dict):
|
||||||
if song_item.get('results'):
|
if song_item.get("results"):
|
||||||
song_item = song_item['results']
|
song_item = song_item["results"]
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
name=song_item['SNG_TITLE'],
|
name=song_item["SNG_TITLE"],
|
||||||
id=song_item['SNG_ID'],
|
id=song_item["SNG_ID"],
|
||||||
artists=[artist['ART_NAME'] for artist in song_item['ARTISTS']],
|
artists=[artist["ART_NAME"] for artist in song_item["ARTISTS"]],
|
||||||
preview_url=(song_item.get('MEDIA').get('HREF')
|
preview_url=(
|
||||||
if type(song_item.get('MEDIA')) is dict and
|
song_item.get("MEDIA").get("HREF")
|
||||||
song_item.get('MEDIA').get('TYPE') == 'preview'
|
if type(song_item.get("MEDIA")) is dict
|
||||||
else None),
|
and song_item.get("MEDIA").get("TYPE") == "preview"
|
||||||
thumbnail=f'https://e-cdns-images.dzcdn.net/images/cover/'
|
else None
|
||||||
f'{song_item["ALB_PICTURE"]}/320x320.jpg',
|
),
|
||||||
duration=int(song_item['DURATION']),
|
thumbnail=f"https://e-cdns-images.dzcdn.net/images/cover/"
|
||||||
track_dict=song_item
|
f'{song_item["ALB_PICTURE"]}/320x320.jpg',
|
||||||
|
duration=int(song_item["DURATION"]),
|
||||||
|
track_dict=song_item,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,32 +19,11 @@ class TrackFormat:
|
|||||||
|
|
||||||
|
|
||||||
TRACK_FORMAT_MAP = {
|
TRACK_FORMAT_MAP = {
|
||||||
FLAC: TrackFormat(
|
FLAC: TrackFormat(code=9, ext=".flac"),
|
||||||
code=9,
|
MP3_128: TrackFormat(code=1, ext=".mp3"),
|
||||||
ext=".flac"
|
MP3_256: TrackFormat(code=5, ext=".mp3"),
|
||||||
),
|
MP3_320: TrackFormat(code=3, ext=".mp3"),
|
||||||
MP3_128: TrackFormat(
|
MP4_RA1: TrackFormat(code=13, ext=".mp4"),
|
||||||
code=1,
|
MP4_RA2: TrackFormat(code=14, ext=".mp4"),
|
||||||
ext=".mp3"
|
MP4_RA3: TrackFormat(code=15, ext=".mp3"),
|
||||||
),
|
|
||||||
MP3_256: TrackFormat(
|
|
||||||
code=5,
|
|
||||||
ext=".mp3"
|
|
||||||
),
|
|
||||||
MP3_320: TrackFormat(
|
|
||||||
code=3,
|
|
||||||
ext=".mp3"
|
|
||||||
),
|
|
||||||
MP4_RA1: TrackFormat(
|
|
||||||
code=13,
|
|
||||||
ext=".mp4"
|
|
||||||
),
|
|
||||||
MP4_RA2: TrackFormat(
|
|
||||||
code=14,
|
|
||||||
ext=".mp4"
|
|
||||||
),
|
|
||||||
MP4_RA3: TrackFormat(
|
|
||||||
code=15,
|
|
||||||
ext=".mp3"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ class UrlDecrypter:
|
|||||||
media_version: str
|
media_version: str
|
||||||
|
|
||||||
def get_url_for(self, track_format: TrackFormat):
|
def get_url_for(self, track_format: TrackFormat):
|
||||||
step1 = (f'{self.md5_origin}¤{track_format.code}¤'
|
step1 = (
|
||||||
f'{self.track_id}¤{self.media_version}')
|
f"{self.md5_origin}¤{track_format.code}¤"
|
||||||
|
f"{self.track_id}¤{self.media_version}"
|
||||||
|
)
|
||||||
m = hashlib.md5()
|
m = hashlib.md5()
|
||||||
m.update(bytes([ord(x) for x in step1]))
|
m.update(bytes([ord(x) for x in step1]))
|
||||||
|
|
||||||
step2 = f'{m.hexdigest()}¤{step1}¤'
|
step2 = f"{m.hexdigest()}¤{step1}¤"
|
||||||
step2 = step2.ljust(80, " ")
|
step2 = step2.ljust(80, " ")
|
||||||
|
|
||||||
cipher = Cipher(
|
cipher = Cipher(
|
||||||
algorithm=algorithms.AES(
|
algorithm=algorithms.AES(key=bytes("jo6aey6haid2Teih", "ascii")),
|
||||||
key=bytes('jo6aey6haid2Teih', 'ascii')
|
|
||||||
),
|
|
||||||
mode=modes.ECB(),
|
mode=modes.ECB(),
|
||||||
backend=default_backend()
|
backend=default_backend(),
|
||||||
)
|
)
|
||||||
|
|
||||||
encryptor = cipher.encryptor()
|
encryptor = cipher.encryptor()
|
||||||
@@ -55,7 +55,7 @@ class UrlDecrypter:
|
|||||||
|
|
||||||
cdn = self.md5_origin[0]
|
cdn = self.md5_origin[0]
|
||||||
|
|
||||||
return f'https://e-cdns-proxy-{cdn}.dzcdn.net/mobile/1/{step3}'
|
return f"https://e-cdns-proxy-{cdn}.dzcdn.net/mobile/1/{step3}"
|
||||||
|
|
||||||
|
|
||||||
@define
|
@define
|
||||||
@@ -69,12 +69,10 @@ class ChunkDecrypter:
|
|||||||
cipher = Cipher(
|
cipher = Cipher(
|
||||||
algorithms.Blowfish(get_blowfish_key(track_id)),
|
algorithms.Blowfish(get_blowfish_key(track_id)),
|
||||||
modes.CBC(bytes([i for i in range(8)])),
|
modes.CBC(bytes([i for i in range(8)])),
|
||||||
default_backend()
|
default_backend(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(cipher=cipher)
|
||||||
cipher=cipher
|
|
||||||
)
|
|
||||||
|
|
||||||
def decrypt_chunk(self, chunk: bytes):
|
def decrypt_chunk(self, chunk: bytes):
|
||||||
decryptor = self.cipher.decryptor()
|
decryptor = self.cipher.decryptor()
|
||||||
@@ -82,7 +80,7 @@ class ChunkDecrypter:
|
|||||||
|
|
||||||
|
|
||||||
def get_blowfish_key(track_id: str):
|
def get_blowfish_key(track_id: str):
|
||||||
secret = 'g4el58wc0zvf9na1'
|
secret = "g4el58wc0zvf9na1"
|
||||||
|
|
||||||
m = hashlib.md5()
|
m = hashlib.md5()
|
||||||
m.update(bytes([ord(x) for x in track_id]))
|
m.update(bytes([ord(x) for x in track_id]))
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ async def on_error(event: ErrorEvent, bot: Bot):
|
|||||||
|
|
||||||
await bot.edit_message_caption(
|
await bot.edit_message_caption(
|
||||||
inline_message_id=event.update.chosen_inline_result.inline_message_id,
|
inline_message_id=event.update.chosen_inline_result.inline_message_id,
|
||||||
caption=f'💔 <b>ERROR</b> occurred. Use this code to get more information: '
|
caption=f"💔 <b>ERROR</b> occurred. Use this code to get more information: "
|
||||||
f'<code>{error_id}</code>',
|
f"<code>{error_id}</code>",
|
||||||
parse_mode='HTML',
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -53,7 +53,7 @@ async def on_error(event: ErrorEvent, bot: Bot):
|
|||||||
exception=pretty_exception,
|
exception=pretty_exception,
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(f'[red]{error_id} occurred[/]')
|
console.print(f"[red]{error_id} occurred[/]")
|
||||||
console.print(event)
|
console.print(event)
|
||||||
console.print(traceback)
|
console.print(traceback)
|
||||||
console.print(f'-{error_id} occurred-')
|
console.print(f"-{error_id} occurred-")
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ class PrettyException:
|
|||||||
🐊 <code>{e.__traceback__.tb_frame.f_code.co_filename.replace(os.getcwd(), "")}\r
|
🐊 <code>{e.__traceback__.tb_frame.f_code.co_filename.replace(os.getcwd(), "")}\r
|
||||||
</code>:{e.__traceback__.tb_frame.f_lineno}
|
</code>:{e.__traceback__.tb_frame.f_lineno}
|
||||||
"""
|
"""
|
||||||
self.short = (f'{e.__class__.__name__}: '
|
self.short = (
|
||||||
f'{"".join(traceback.format_exception_only(e)).strip()}')
|
f"{e.__class__.__name__}: "
|
||||||
|
f'{"".join(traceback.format_exception_only(e)).strip()}'
|
||||||
|
)
|
||||||
|
|
||||||
self.pretty_exception = (f"{self.long}\n\n"
|
self.pretty_exception = (
|
||||||
f"⬇️ Trace:"
|
f"{self.long}\n\n" f"⬇️ Trace:" f"{self.get_full_stack()}"
|
||||||
f"{self.get_full_stack()}")
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_full_stack():
|
def get_full_stack():
|
||||||
@@ -40,9 +42,11 @@ class PrettyException:
|
|||||||
|
|
||||||
full_stack = "\n".join(
|
full_stack = "\n".join(
|
||||||
[
|
[
|
||||||
format_line(line)
|
(
|
||||||
if re.search(line_regex, line)
|
format_line(line)
|
||||||
else f"<code>{line}</code>"
|
if re.search(line_regex, line)
|
||||||
|
else f"<code>{line}</code>"
|
||||||
|
)
|
||||||
for line in full_stack.splitlines()
|
for line in full_stack.splitlines()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ class MemoryStorageRecord:
|
|||||||
|
|
||||||
class StorageDict(DefaultDict):
|
class StorageDict(DefaultDict):
|
||||||
def __init__(self, default_factory=None) -> None:
|
def __init__(self, default_factory=None) -> None:
|
||||||
if type(db.fsm.get('fsm')) is not dict:
|
if type(db.fsm.get("fsm")) is not dict:
|
||||||
db.fsm['fsm'] = dict()
|
db.fsm["fsm"] = dict()
|
||||||
|
|
||||||
super().__init__(default_factory, db.fsm['fsm'])
|
super().__init__(default_factory, db.fsm["fsm"])
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
super().__setitem__(key, value)
|
super().__setitem__(key, value)
|
||||||
db.fsm['fsm'] = dict(self)
|
db.fsm["fsm"] = dict(self)
|
||||||
|
|
||||||
|
|
||||||
class InDbStorage(BaseStorage):
|
class InDbStorage(BaseStorage):
|
||||||
|
|||||||
@@ -11,46 +11,32 @@ class Setting:
|
|||||||
|
|
||||||
|
|
||||||
settings_strings: dict[str, Setting] = {
|
settings_strings: dict[str, Setting] = {
|
||||||
'search_preview': Setting(
|
"search_preview": Setting(
|
||||||
name='Search preview',
|
name="Search preview",
|
||||||
description='Show only covers (better display), '
|
description="Show only covers (better display), "
|
||||||
'or add 30 seconds of track preview whenever possible?',
|
"or add 30 seconds of track preview whenever possible?",
|
||||||
choices={
|
choices={"cover": "Cover picture", "preview": "Audio preview"},
|
||||||
'cover': 'Cover picture',
|
|
||||||
'preview': 'Audio preview'
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
'recode_youtube': Setting(
|
"recode_youtube": Setting(
|
||||||
name='Recode YouTube (and Spotify)',
|
name="Recode YouTube (and Spotify)",
|
||||||
description='Recode when downloading from YouTube (and Spotify) to '
|
description="Recode when downloading from YouTube (and Spotify) to "
|
||||||
'more compatible format (may take some time)',
|
"more compatible format (may take some time)",
|
||||||
choices={
|
choices={"no": "Send original file", "yes": "Recode to libmp3lame"},
|
||||||
'no': 'Send original file',
|
|
||||||
'yes': 'Recode to libmp3lame'
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
'exact_spotify_search': Setting(
|
"exact_spotify_search": Setting(
|
||||||
name='Only exact Spotify matches',
|
name="Only exact Spotify matches",
|
||||||
description='When searching on Youtube from Spotify, show only exact matches, '
|
description="When searching on Youtube from Spotify, show only exact matches, "
|
||||||
'may protect against inaccurate matches, but at the same time it '
|
"may protect against inaccurate matches, but at the same time it "
|
||||||
'can lose reuploaded tracks. Should be enabled always, except in '
|
"can lose reuploaded tracks. Should be enabled always, except in "
|
||||||
'situations where the track is not found on both YouTube and '
|
"situations where the track is not found on both YouTube and "
|
||||||
'Deezer',
|
"Deezer",
|
||||||
choices={
|
choices={"yes": "Only exact matches", "no": "Fuzzy matches also"},
|
||||||
'yes': 'Only exact matches',
|
),
|
||||||
'no': 'Fuzzy matches also'
|
"default_search_provider": Setting(
|
||||||
},
|
name="Default search provider",
|
||||||
|
description="Which service to use when searching without service filter",
|
||||||
|
choices={"d": "Deezer", "c": "SoundCloud", "y": "YouTube", "s": "Spotify"},
|
||||||
),
|
),
|
||||||
'default_search_provider': Setting(
|
|
||||||
name='Default search provider',
|
|
||||||
description='Which service to use when searching without service filter',
|
|
||||||
choices={
|
|
||||||
'd': 'Deezer',
|
|
||||||
'c': 'SoundCloud',
|
|
||||||
'y': 'YouTube',
|
|
||||||
's': 'Spotify'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -64,8 +50,8 @@ class UserSettings:
|
|||||||
|
|
||||||
if db.settings.get(self.user_id) is None:
|
if db.settings.get(self.user_id) is None:
|
||||||
db.settings[self.user_id] = dict(
|
db.settings[self.user_id] = dict(
|
||||||
(setting, list(settings_strings[setting].choices)[0]) for setting in
|
(setting, list(settings_strings[setting].choices)[0])
|
||||||
settings_strings
|
for setting in settings_strings
|
||||||
)
|
)
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ soundcloud = SoundCloud(
|
|||||||
client_id=config.tokens.soundcloud.client_id,
|
client_id=config.tokens.soundcloud.client_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['soundcloud', 'SoundCloudBytestream']
|
__all__ = ["soundcloud", "SoundCloudBytestream"]
|
||||||
|
|||||||
@@ -15,18 +15,9 @@ class SoundCloudBytestream:
|
|||||||
song: SongItem
|
song: SongItem
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(
|
def from_bytes(cls, bytes_: bytes, filename: str, duration: int, song: SongItem):
|
||||||
cls,
|
|
||||||
bytes_: bytes,
|
|
||||||
filename: str,
|
|
||||||
duration: int,
|
|
||||||
song: SongItem
|
|
||||||
):
|
|
||||||
return cls(
|
return cls(
|
||||||
file=bytes_,
|
file=bytes_, filename=filename, duration=int(duration / 1000), song=song
|
||||||
filename=filename,
|
|
||||||
duration=int(duration / 1000),
|
|
||||||
song=song
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,60 +31,53 @@ class Downloader:
|
|||||||
song: SongItem
|
song: SongItem
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def build(
|
async def build(cls, song_id: str, driver: SoundCloudDriver):
|
||||||
cls,
|
|
||||||
song_id: str,
|
|
||||||
driver: SoundCloudDriver
|
|
||||||
):
|
|
||||||
track = await driver.get_track(song_id)
|
track = await driver.get_track(song_id)
|
||||||
song = SongItem.from_soundcloud(track)
|
song = SongItem.from_soundcloud(track)
|
||||||
|
|
||||||
if url := cls._try_get_progressive(track['media']['transcodings']):
|
if url := cls._try_get_progressive(track["media"]["transcodings"]):
|
||||||
method = cls._progressive
|
method = cls._progressive
|
||||||
else:
|
else:
|
||||||
url = track['media']['transcodings'][0]['url']
|
url = track["media"]["transcodings"][0]["url"]
|
||||||
method = cls._hls if \
|
method = (
|
||||||
(track['media']['transcodings'][0]['format']['protocol']
|
cls._hls
|
||||||
== 'hls') else cls._progressive
|
if (track["media"]["transcodings"][0]["format"]["protocol"] == "hls")
|
||||||
|
else cls._progressive
|
||||||
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
driver=driver,
|
driver=driver,
|
||||||
duration=track['duration'],
|
duration=track["duration"],
|
||||||
method=method,
|
method=method,
|
||||||
download_url=url,
|
download_url=url,
|
||||||
filename=f'{track["title"]}.mp3',
|
filename=f'{track["title"]}.mp3',
|
||||||
song=song
|
song=song,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _try_get_progressive(urls: list) -> str | None:
|
def _try_get_progressive(urls: list) -> str | None:
|
||||||
for transcode in urls:
|
for transcode in urls:
|
||||||
if transcode['format']['protocol'] == 'progressive':
|
if transcode["format"]["protocol"] == "progressive":
|
||||||
return transcode['url']
|
return transcode["url"]
|
||||||
|
|
||||||
async def _progressive(self, url: str) -> bytes:
|
async def _progressive(self, url: str) -> bytes:
|
||||||
return await self.driver.engine.read_data(
|
return await self.driver.engine.read_data(
|
||||||
url=(await self.driver.engine.get(
|
url=(await self.driver.engine.get(url))["url"]
|
||||||
url
|
|
||||||
))['url']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _hls(self, url: str) -> bytes:
|
async def _hls(self, url: str) -> bytes:
|
||||||
m3u8_obj = m3u8.loads(
|
m3u8_obj = m3u8.loads(
|
||||||
(await self.driver.engine.read_data(
|
(
|
||||||
(await self.driver.engine.get(
|
await self.driver.engine.read_data(
|
||||||
url=url
|
(await self.driver.engine.get(url=url))["url"]
|
||||||
))['url']
|
)
|
||||||
)).decode()
|
).decode()
|
||||||
)
|
)
|
||||||
|
|
||||||
content = bytearray()
|
content = bytearray()
|
||||||
for segment in m3u8_obj.files:
|
for segment in m3u8_obj.files:
|
||||||
content.extend(
|
content.extend(
|
||||||
await self.driver.engine.read_data(
|
await self.driver.engine.read_data(url=segment, append_client_id=False)
|
||||||
url=segment,
|
|
||||||
append_client_id=False
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
@@ -103,7 +87,7 @@ class Downloader:
|
|||||||
bytes_=await self.method(self, self.download_url),
|
bytes_=await self.method(self, self.download_url),
|
||||||
filename=self.filename,
|
filename=self.filename,
|
||||||
duration=self.duration,
|
duration=self.duration,
|
||||||
song=self.song
|
song=self.song,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +96,4 @@ class DownloaderBuilder:
|
|||||||
driver: SoundCloudDriver
|
driver: SoundCloudDriver
|
||||||
|
|
||||||
async def from_id(self, song_id: str):
|
async def from_id(self, song_id: str):
|
||||||
return await Downloader.build(
|
return await Downloader.build(song_id=song_id, driver=self.driver)
|
||||||
song_id=song_id,
|
|
||||||
driver=self.driver
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -8,23 +8,12 @@ class SoundCloudDriver:
|
|||||||
engine: SoundCloudEngine
|
engine: SoundCloudEngine
|
||||||
|
|
||||||
async def get_track(self, track_id: int | str):
|
async def get_track(self, track_id: int | str):
|
||||||
return await self.engine.call(
|
return await self.engine.call(f"tracks/{track_id}")
|
||||||
f'tracks/{track_id}'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def search(self, query: str, limit: int = 30):
|
async def search(self, query: str, limit: int = 30):
|
||||||
return (await self.engine.call(
|
return (
|
||||||
'search/tracks',
|
await self.engine.call("search/tracks", params={"q": query, "limit": limit})
|
||||||
params={
|
)["collection"]
|
||||||
'q': query,
|
|
||||||
'limit': limit
|
|
||||||
}
|
|
||||||
))['collection']
|
|
||||||
|
|
||||||
async def resolve_url(self, url: str):
|
async def resolve_url(self, url: str):
|
||||||
return await self.engine.call(
|
return await self.engine.call("resolve", params={"url": url})
|
||||||
'resolve',
|
|
||||||
params={
|
|
||||||
'url': url
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -8,27 +8,33 @@ class SoundCloudEngine:
|
|||||||
|
|
||||||
async def call(self, request_point: str, params: dict = None):
|
async def call(self, request_point: str, params: dict = None):
|
||||||
return await self.get(
|
return await self.get(
|
||||||
url=f'https://api-v2.soundcloud.com/{request_point}',
|
url=f"https://api-v2.soundcloud.com/{request_point}", params=params
|
||||||
params=params
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get(self, url: str, params: dict = None):
|
async def get(self, url: str, params: dict = None):
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url,
|
url,
|
||||||
params=(params or {}) | {
|
params=(params or {})
|
||||||
'client_id': self.client_id,
|
| {
|
||||||
},
|
"client_id": self.client_id,
|
||||||
|
},
|
||||||
) as r:
|
) as r:
|
||||||
return await r.json()
|
return await r.json()
|
||||||
|
|
||||||
async def read_data(self, url: str, params: dict = None,
|
async def read_data(
|
||||||
append_client_id: bool = True):
|
self, url: str, params: dict = None, append_client_id: bool = True
|
||||||
|
):
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url,
|
url,
|
||||||
params=(params or {}) | ({
|
params=(params or {})
|
||||||
'client_id': self.client_id,
|
| (
|
||||||
} if append_client_id else {}),
|
{
|
||||||
|
"client_id": self.client_id,
|
||||||
|
}
|
||||||
|
if append_client_id
|
||||||
|
else {}
|
||||||
|
),
|
||||||
) as r:
|
) as r:
|
||||||
return await r.content.read()
|
return await r.content.read()
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ class SongItem(BaseSongItem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_soundcloud(cls, song_item: dict):
|
def from_soundcloud(cls, song_item: dict):
|
||||||
return cls(
|
return cls(
|
||||||
name=song_item['title'],
|
name=song_item["title"],
|
||||||
id=str(song_item['id']),
|
id=str(song_item["id"]),
|
||||||
artists=[],
|
artists=[],
|
||||||
thumbnail=(song_item['artwork_url'] or song_item['user']['avatar_url'] or
|
thumbnail=(
|
||||||
'https://soundcloud.com/images/default_avatar_large.png')
|
song_item["artwork_url"]
|
||||||
.replace('large.jpg', 't300x300.jpg'),
|
or song_item["user"]["avatar_url"]
|
||||||
preview_url=None
|
or "https://soundcloud.com/images/default_avatar_large.png"
|
||||||
|
).replace("large.jpg", "t300x300.jpg"),
|
||||||
|
preview_url=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from bot.utils.config import config
|
|||||||
|
|
||||||
spotify = Spotify(
|
spotify = Spotify(
|
||||||
client_id=config.tokens.spotify.client_id,
|
client_id=config.tokens.spotify.client_id,
|
||||||
client_secret=config.tokens.spotify.client_secret
|
client_secret=config.tokens.spotify.client_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['spotify']
|
__all__ = ["spotify"]
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ class SongItem(BaseSongItem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_spotify(cls, song_item: dict):
|
def from_spotify(cls, song_item: dict):
|
||||||
return cls(
|
return cls(
|
||||||
name=song_item['name'],
|
name=song_item["name"],
|
||||||
id=song_item['id'],
|
id=song_item["id"],
|
||||||
artists=[artist['name'] for artist in song_item['artists']],
|
artists=[artist["name"] for artist in song_item["artists"]],
|
||||||
preview_url=song_item['preview_url'].split('?')[0] if
|
preview_url=(
|
||||||
song_item['preview_url'] is not None else None,
|
song_item["preview_url"].split("?")[0]
|
||||||
thumbnail=song_item['album']['images'][1]['url']
|
if song_item["preview_url"] is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
thumbnail=song_item["album"]["images"][1]["url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ class Songs(object):
|
|||||||
if r is None:
|
if r is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return [SongItem.from_spotify(item) for item in r['tracks']['items']]
|
return [SongItem.from_spotify(item) for item in r["tracks"]["items"]]
|
||||||
|
|
||||||
def from_id(self, song_id: str) -> SongItem | None:
|
def from_id(self, song_id: str) -> SongItem | None:
|
||||||
r = self.spotify.track(song_id)
|
r = self.spotify.track(song_id)
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ class Spotify(object):
|
|||||||
def __init__(self, client_id, client_secret):
|
def __init__(self, client_id, client_secret):
|
||||||
self.spotify = spotipy.Spotify(
|
self.spotify = spotipy.Spotify(
|
||||||
client_credentials_manager=SpotifyClientCredentials(
|
client_credentials_manager=SpotifyClientCredentials(
|
||||||
client_id=client_id,
|
client_id=client_id, client_secret=client_secret
|
||||||
client_secret=client_secret
|
|
||||||
),
|
),
|
||||||
backoff_factor=0.1,
|
backoff_factor=0.1,
|
||||||
retries=10
|
retries=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.songs = Songs(self.spotify)
|
self.songs = Songs(self.spotify)
|
||||||
|
|||||||
@@ -10,26 +10,28 @@ async def get_url_after_redirect(url: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def get_id(recognised: RecognisedService):
|
async def get_id(recognised: RecognisedService):
|
||||||
if recognised.name == 'yt':
|
if recognised.name == "yt":
|
||||||
return recognised.parse_result.path.replace('/', '') if (
|
return (
|
||||||
recognised.parse_result.netloc.endswith('youtu.be')
|
recognised.parse_result.path.replace("/", "")
|
||||||
) else recognised.parse_result.query.split('=')[1].split('&')[0]
|
if (recognised.parse_result.netloc.endswith("youtu.be"))
|
||||||
|
else recognised.parse_result.query.split("=")[1].split("&")[0]
|
||||||
|
)
|
||||||
|
|
||||||
elif recognised.name == 'spot':
|
elif recognised.name == "spot":
|
||||||
if recognised.parse_result.netloc.endswith('open.spotify.com'):
|
if recognised.parse_result.netloc.endswith("open.spotify.com"):
|
||||||
return recognised.parse_result.path.split('/')[2]
|
return recognised.parse_result.path.split("/")[2]
|
||||||
else:
|
else:
|
||||||
url = await get_url_after_redirect(recognised.parse_result.geturl())
|
url = await get_url_after_redirect(recognised.parse_result.geturl())
|
||||||
return url.split('/')[-1].split('?')[0]
|
return url.split("/")[-1].split("?")[0]
|
||||||
|
|
||||||
elif recognised.name == 'deez':
|
elif recognised.name == "deez":
|
||||||
if recognised.parse_result.netloc.endswith('deezer.com'):
|
if recognised.parse_result.netloc.endswith("deezer.com"):
|
||||||
return recognised.parse_result.path.split('/')[-1]
|
return recognised.parse_result.path.split("/")[-1]
|
||||||
else:
|
else:
|
||||||
url = await get_url_after_redirect(recognised.parse_result.geturl())
|
url = await get_url_after_redirect(recognised.parse_result.geturl())
|
||||||
return url.split('/')[-1].split('?')[0]
|
return url.split("/")[-1].split("?")[0]
|
||||||
|
|
||||||
elif recognised.name == 'sc':
|
elif recognised.name == "sc":
|
||||||
if not recognised.parse_result.netloc.startswith('on'):
|
if not recognised.parse_result.netloc.startswith("on"):
|
||||||
return recognised.parse_result.geturl()
|
return recognised.parse_result.geturl()
|
||||||
return await get_url_after_redirect(recognised.parse_result.geturl())
|
return await get_url_after_redirect(recognised.parse_result.geturl())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from bot.modules.soundcloud import soundcloud
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RecognisedService:
|
class RecognisedService:
|
||||||
name: Literal['yt', 'spot', 'deez', 'sc']
|
name: Literal["yt", "spot", "deez", "sc"]
|
||||||
db_table: DBDict
|
db_table: DBDict
|
||||||
by_id_func: Callable | Awaitable
|
by_id_func: Callable | Awaitable
|
||||||
parse_result: ParseResult
|
parse_result: ParseResult
|
||||||
@@ -22,33 +22,33 @@ class RecognisedService:
|
|||||||
|
|
||||||
def recognise_music_service(url: str) -> RecognisedService | None:
|
def recognise_music_service(url: str) -> RecognisedService | None:
|
||||||
url = urlparse(url)
|
url = urlparse(url)
|
||||||
if url.netloc.endswith('youtube.com') or url.netloc.endswith('youtu.be'):
|
if url.netloc.endswith("youtube.com") or url.netloc.endswith("youtu.be"):
|
||||||
return RecognisedService(
|
return RecognisedService(
|
||||||
name='yt',
|
name="yt",
|
||||||
db_table=db.youtube,
|
db_table=db.youtube,
|
||||||
by_id_func=youtube.songs.from_id,
|
by_id_func=youtube.songs.from_id,
|
||||||
parse_result=url
|
parse_result=url,
|
||||||
)
|
)
|
||||||
elif url.netloc.endswith('open.spotify.com') or url.netloc.endswith('spotify.link'):
|
elif url.netloc.endswith("open.spotify.com") or url.netloc.endswith("spotify.link"):
|
||||||
return RecognisedService(
|
return RecognisedService(
|
||||||
name='spot',
|
name="spot",
|
||||||
db_table=db.spotify,
|
db_table=db.spotify,
|
||||||
by_id_func=spotify.songs.from_id,
|
by_id_func=spotify.songs.from_id,
|
||||||
parse_result=url
|
parse_result=url,
|
||||||
)
|
)
|
||||||
elif url.netloc.endswith('deezer.page.link') or url.netloc.endswith('deezer.com'):
|
elif url.netloc.endswith("deezer.page.link") or url.netloc.endswith("deezer.com"):
|
||||||
return RecognisedService(
|
return RecognisedService(
|
||||||
name='deez',
|
name="deez",
|
||||||
db_table=db.deezer,
|
db_table=db.deezer,
|
||||||
by_id_func=deezer.songs.from_id,
|
by_id_func=deezer.songs.from_id,
|
||||||
parse_result=url
|
parse_result=url,
|
||||||
)
|
)
|
||||||
elif url.netloc.endswith('soundcloud.com'):
|
elif url.netloc.endswith("soundcloud.com"):
|
||||||
return RecognisedService(
|
return RecognisedService(
|
||||||
name='sc',
|
name="sc",
|
||||||
db_table=db.soundcloud,
|
db_table=db.soundcloud,
|
||||||
by_id_func=soundcloud.songs.from_url,
|
by_id_func=soundcloud.songs.from_url,
|
||||||
parse_result=url
|
parse_result=url,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -15,19 +15,19 @@ class SongItem(BaseSongItem):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_youtube(cls, song_item: dict):
|
def from_youtube(cls, song_item: dict):
|
||||||
return cls(
|
return cls(
|
||||||
name=song_item['title'],
|
name=song_item["title"],
|
||||||
id=song_item['videoId'],
|
id=song_item["videoId"],
|
||||||
artists=[artist['name'] for artist in song_item['artists']],
|
artists=[artist["name"] for artist in song_item["artists"]],
|
||||||
thumbnail=song_item['thumbnails'][1]['url']
|
thumbnail=song_item["thumbnails"][1]["url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_details(cls, details: dict):
|
def from_details(cls, details: dict):
|
||||||
return cls(
|
return cls(
|
||||||
name=details['title'],
|
name=details["title"],
|
||||||
id=details['videoId'],
|
id=details["videoId"],
|
||||||
artists=details['author'].split(' & '),
|
artists=details["author"].split(" & "),
|
||||||
thumbnail=details['thumbnail']['thumbnails'][1]['url']
|
thumbnail=details["thumbnail"]["thumbnails"][1]["url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_bytestream(self) -> Awaitable[YouTubeBytestream]:
|
def to_bytestream(self) -> Awaitable[YouTubeBytestream]:
|
||||||
@@ -39,16 +39,10 @@ class Songs(object):
|
|||||||
ytm: ytmusicapi.YTMusic
|
ytm: ytmusicapi.YTMusic
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self, query: str, limit: int = 10, exact_match: bool = False
|
||||||
query: str,
|
|
||||||
limit: int = 10,
|
|
||||||
exact_match: bool = False
|
|
||||||
) -> list[SongItem] | None:
|
) -> list[SongItem] | None:
|
||||||
r = self.ytm.search(
|
r = self.ytm.search(
|
||||||
query,
|
query, limit=limit, filter="songs", ignore_spelling=exact_match
|
||||||
limit=limit,
|
|
||||||
filter='songs',
|
|
||||||
ignore_spelling=exact_match
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if r is None:
|
if r is None:
|
||||||
@@ -68,4 +62,4 @@ class Songs(object):
|
|||||||
if r is None:
|
if r is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return SongItem.from_details(r['videoDetails'])
|
return SongItem.from_details(r["videoDetails"])
|
||||||
|
|||||||
@@ -9,6 +9,4 @@ class YouTube(object):
|
|||||||
self.ytm = ytmusicapi.YTMusic()
|
self.ytm = ytmusicapi.YTMusic()
|
||||||
|
|
||||||
self.download = Downloader
|
self.download = Downloader
|
||||||
self.songs = Songs(
|
self.songs = Songs(self.ytm)
|
||||||
self.ytm
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
InlineQueryResultDocument, InlineQueryResultCachedAudio,
|
InlineQueryResultDocument,
|
||||||
InlineKeyboardMarkup, InlineKeyboardButton
|
InlineQueryResultCachedAudio,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
InlineKeyboardButton,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bot.modules.database.db import DBDict
|
from bot.modules.database.db import DBDict
|
||||||
@@ -10,37 +12,38 @@ from bot.modules.common.song import BaseSongItem
|
|||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
|
|
||||||
BaseSongT = TypeVar('BaseSongT', bound=BaseSongItem)
|
BaseSongT = TypeVar("BaseSongT", bound=BaseSongItem)
|
||||||
|
|
||||||
|
|
||||||
async def get_common_search_result(
|
async def get_common_search_result(
|
||||||
audio: BaseSongT,
|
audio: BaseSongT, db_table: DBDict, service_id: str, settings: UserSettings
|
||||||
db_table: DBDict,
|
|
||||||
service_id: str,
|
|
||||||
settings: UserSettings
|
|
||||||
) -> InlineQueryResultDocument | InlineQueryResultCachedAudio:
|
) -> InlineQueryResultDocument | InlineQueryResultCachedAudio:
|
||||||
return (
|
return (
|
||||||
InlineQueryResultDocument(
|
InlineQueryResultDocument(
|
||||||
id=f'{service_id}::' + audio.id,
|
id=f"{service_id}::" + audio.id,
|
||||||
title=audio.name,
|
title=audio.name,
|
||||||
description=audio.all_artists,
|
description=audio.all_artists,
|
||||||
thumb_url=audio.thumbnail,
|
thumb_url=audio.thumbnail,
|
||||||
document_url=(audio.preview_url or audio.thumbnail) if
|
document_url=(
|
||||||
settings['search_preview'].value == 'preview' else audio.thumbnail,
|
(audio.preview_url or audio.thumbnail)
|
||||||
mime_type='application/zip',
|
if settings["search_preview"].value == "preview"
|
||||||
|
else audio.thumbnail
|
||||||
|
),
|
||||||
|
mime_type="application/zip",
|
||||||
reply_markup=InlineKeyboardMarkup(
|
reply_markup=InlineKeyboardMarkup(
|
||||||
inline_keyboard=[
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text='Downloading...', callback_data='.')]
|
[InlineKeyboardButton(text="Downloading...", callback_data=".")]
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
caption=audio.full_name,
|
caption=audio.full_name,
|
||||||
) if audio.id not in list(db_table.keys()) else
|
)
|
||||||
InlineQueryResultCachedAudio(
|
if audio.id not in list(db_table.keys())
|
||||||
id=f'{service_id}c::' + audio.id,
|
else InlineQueryResultCachedAudio(
|
||||||
|
id=f"{service_id}c::" + audio.id,
|
||||||
audio_file_id=db_table[audio.id],
|
audio_file_id=db_table[audio.id],
|
||||||
reply_markup=InlineKeyboardMarkup(
|
reply_markup=InlineKeyboardMarkup(
|
||||||
inline_keyboard=[
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text='Verifying...', callback_data='.')]
|
[InlineKeyboardButton(text="Verifying...", callback_data=".")]
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .search import get_deezer_search_results
|
from .search import get_deezer_search_results
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['get_deezer_search_results']
|
__all__ = ["get_deezer_search_results"]
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import InlineQueryResultDocument, InlineQueryResultCachedAudio
|
||||||
InlineQueryResultDocument, InlineQueryResultCachedAudio
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.modules.deezer import deezer
|
from bot.modules.deezer import deezer
|
||||||
from bot.modules.database import db
|
from bot.modules.database import db
|
||||||
@@ -9,15 +7,12 @@ from bot.modules.settings import UserSettings
|
|||||||
from ..common.search import get_common_search_result
|
from ..common.search import get_common_search_result
|
||||||
|
|
||||||
|
|
||||||
async def get_deezer_search_results(query: str, settings: UserSettings) -> list[
|
async def get_deezer_search_results(
|
||||||
InlineQueryResultDocument | InlineQueryResultCachedAudio
|
query: str, settings: UserSettings
|
||||||
]:
|
) -> list[InlineQueryResultDocument | InlineQueryResultCachedAudio]:
|
||||||
return [
|
return [
|
||||||
await get_common_search_result(
|
await get_common_search_result(
|
||||||
audio=audio,
|
audio=audio, db_table=db.deezer, service_id="deez", settings=settings
|
||||||
db_table=db.deezer,
|
|
||||||
service_id='deez',
|
|
||||||
settings=settings
|
|
||||||
)
|
)
|
||||||
for audio in await deezer.songs.search(query, limit=50)
|
for audio in await deezer.songs.search(query, limit=50)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
InlineQueryResultArticle, InputTextMessageContent,
|
InlineQueryResultArticle,
|
||||||
|
InputTextMessageContent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bot.modules.database import db
|
from bot.modules.database import db
|
||||||
@@ -8,24 +9,27 @@ from bot.modules.error import Error
|
|||||||
from bot.common import console
|
from bot.common import console
|
||||||
|
|
||||||
|
|
||||||
async def get_error_search_results(error_id: str) -> (list[InlineQueryResultArticle]
|
async def get_error_search_results(
|
||||||
| None):
|
error_id: str,
|
||||||
|
) -> list[InlineQueryResultArticle] | None:
|
||||||
error: Error = db.errors.get(error_id)
|
error: Error = db.errors.get(error_id)
|
||||||
if error is None:
|
if error is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
console.print(f'{error_id} requested')
|
console.print(f"{error_id} requested")
|
||||||
console.print(error.traceback)
|
console.print(error.traceback)
|
||||||
console.print(f'-{error_id} requested-')
|
console.print(f"-{error_id} requested-")
|
||||||
|
|
||||||
return [(
|
return [
|
||||||
InlineQueryResultArticle(
|
(
|
||||||
id=error_id,
|
InlineQueryResultArticle(
|
||||||
title=f'Error {error_id}',
|
id=error_id,
|
||||||
description=error.exception.short,
|
title=f"Error {error_id}",
|
||||||
input_message_content=InputTextMessageContent(
|
description=error.exception.short,
|
||||||
message_text=error.exception.long,
|
input_message_content=InputTextMessageContent(
|
||||||
parse_mode='HTML',
|
message_text=error.exception.long,
|
||||||
),
|
parse_mode="HTML",
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from .search import get_soundcloud_search_results
|
from .search import get_soundcloud_search_results
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ["get_soundcloud_search_results"]
|
||||||
'get_soundcloud_search_results'
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import InlineQueryResultDocument, InlineQueryResultCachedAudio
|
||||||
InlineQueryResultDocument, InlineQueryResultCachedAudio
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.modules.soundcloud import soundcloud
|
from bot.modules.soundcloud import soundcloud
|
||||||
from bot.modules.database import db
|
from bot.modules.database import db
|
||||||
@@ -9,15 +7,12 @@ from bot.modules.settings import UserSettings
|
|||||||
from ..common.search import get_common_search_result
|
from ..common.search import get_common_search_result
|
||||||
|
|
||||||
|
|
||||||
async def get_soundcloud_search_results(query: str, settings: UserSettings) -> list[
|
async def get_soundcloud_search_results(
|
||||||
InlineQueryResultDocument | InlineQueryResultCachedAudio
|
query: str, settings: UserSettings
|
||||||
]:
|
) -> list[InlineQueryResultDocument | InlineQueryResultCachedAudio]:
|
||||||
return [
|
return [
|
||||||
await get_common_search_result(
|
await get_common_search_result(
|
||||||
audio=audio,
|
audio=audio, db_table=db.soundcloud, service_id="sc", settings=settings
|
||||||
db_table=db.soundcloud,
|
|
||||||
service_id='sc',
|
|
||||||
settings=settings
|
|
||||||
)
|
)
|
||||||
for audio in await soundcloud.songs.search(query, limit=50)
|
for audio in await soundcloud.songs.search(query, limit=50)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from .search import get_spotify_search_results
|
from .search import get_spotify_search_results
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ["get_spotify_search_results"]
|
||||||
'get_spotify_search_results'
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import InlineQueryResultDocument, InlineQueryResultCachedAudio
|
||||||
InlineQueryResultDocument, InlineQueryResultCachedAudio
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.modules.spotify import spotify
|
from bot.modules.spotify import spotify
|
||||||
from bot.modules.database import db
|
from bot.modules.database import db
|
||||||
@@ -9,15 +7,12 @@ from bot.modules.settings import UserSettings
|
|||||||
from ..common.search import get_common_search_result
|
from ..common.search import get_common_search_result
|
||||||
|
|
||||||
|
|
||||||
async def get_spotify_search_results(query: str, settings: UserSettings) -> list[
|
async def get_spotify_search_results(
|
||||||
InlineQueryResultDocument | InlineQueryResultCachedAudio
|
query: str, settings: UserSettings
|
||||||
]:
|
) -> list[InlineQueryResultDocument | InlineQueryResultCachedAudio]:
|
||||||
return [
|
return [
|
||||||
await get_common_search_result(
|
await get_common_search_result(
|
||||||
audio=audio,
|
audio=audio, db_table=db.spotify, service_id="spot", settings=settings
|
||||||
db_table=db.spotify,
|
|
||||||
service_id='spot',
|
|
||||||
settings=settings
|
|
||||||
)
|
)
|
||||||
for audio in spotify.songs.search(query, limit=50)
|
for audio in spotify.songs.search(query, limit=50)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import InlineQueryResultDocument, InlineQueryResultCachedAudio
|
||||||
InlineQueryResultDocument, InlineQueryResultCachedAudio
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.modules.url import recognise_music_service, get_id
|
from bot.modules.url import recognise_music_service, get_id
|
||||||
from bot.modules.settings import UserSettings
|
from bot.modules.settings import UserSettings
|
||||||
@@ -10,9 +8,9 @@ from ..common.search import get_common_search_result
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
async def get_url_results(query: str, settings: UserSettings) -> list[
|
async def get_url_results(
|
||||||
InlineQueryResultDocument | InlineQueryResultCachedAudio
|
query: str, settings: UserSettings
|
||||||
]:
|
) -> list[InlineQueryResultDocument | InlineQueryResultCachedAudio]:
|
||||||
service = recognise_music_service(query)
|
service = recognise_music_service(query)
|
||||||
if inspect.iscoroutinefunction(service.by_id_func):
|
if inspect.iscoroutinefunction(service.by_id_func):
|
||||||
audio = await service.by_id_func(await get_id(service))
|
audio = await service.by_id_func(await get_id(service))
|
||||||
@@ -26,6 +24,6 @@ async def get_url_results(query: str, settings: UserSettings) -> list[
|
|||||||
audio=audio,
|
audio=audio,
|
||||||
db_table=service.db_table,
|
db_table=service.db_table,
|
||||||
service_id=service.name,
|
service_id=service.name,
|
||||||
settings=settings
|
settings=settings,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from .search import get_youtube_search_results
|
from .search import get_youtube_search_results
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ["get_youtube_search_results"]
|
||||||
'get_youtube_search_results'
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from aiogram.types import (
|
from aiogram.types import InlineQueryResultDocument, InlineQueryResultCachedAudio
|
||||||
InlineQueryResultDocument, InlineQueryResultCachedAudio
|
|
||||||
)
|
|
||||||
|
|
||||||
from bot.modules.youtube import youtube
|
from bot.modules.youtube import youtube
|
||||||
from bot.modules.database import db
|
from bot.modules.database import db
|
||||||
@@ -9,15 +7,12 @@ from bot.modules.settings import UserSettings
|
|||||||
from ..common.search import get_common_search_result
|
from ..common.search import get_common_search_result
|
||||||
|
|
||||||
|
|
||||||
async def get_youtube_search_results(query: str, settings: UserSettings) -> list[
|
async def get_youtube_search_results(
|
||||||
InlineQueryResultDocument | InlineQueryResultCachedAudio
|
query: str, settings: UserSettings
|
||||||
]:
|
) -> list[InlineQueryResultDocument | InlineQueryResultCachedAudio]:
|
||||||
return [
|
return [
|
||||||
await get_common_search_result(
|
await get_common_search_result(
|
||||||
audio=audio,
|
audio=audio, db_table=db.youtube, service_id="yt", settings=settings
|
||||||
db_table=db.youtube,
|
|
||||||
service_id='yt',
|
|
||||||
settings=settings
|
|
||||||
)
|
)
|
||||||
for audio in youtube.songs.search(query, limit=40)
|
for audio in youtube.songs.search(query, limit=40)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class Config(dict):
|
|||||||
def __init__(self, _config: dict = None):
|
def __init__(self, _config: dict = None):
|
||||||
try:
|
try:
|
||||||
if _config is None:
|
if _config is None:
|
||||||
config = tomllib.load(open('config.toml', 'rb'))
|
config = tomllib.load(open("config.toml", "rb"))
|
||||||
|
|
||||||
super().__init__(**config)
|
super().__init__(**config)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ class SignatureGenerator:
|
|||||||
|
|
||||||
# Used when processing input:
|
# Used when processing input:
|
||||||
|
|
||||||
self.ring_buffer_of_samples: RingBuffer[int] = RingBuffer(buffer_size=2048, default_value=0)
|
self.ring_buffer_of_samples: RingBuffer[int] = RingBuffer(
|
||||||
|
buffer_size=2048, default_value=0
|
||||||
|
)
|
||||||
|
|
||||||
self.fft_outputs: RingBuffer[List[float]] = RingBuffer(
|
self.fft_outputs: RingBuffer[List[float]] = RingBuffer(
|
||||||
buffer_size=256, default_value=[0.0 * 1025]
|
buffer_size=256, default_value=[0.0 * 1025]
|
||||||
@@ -91,12 +93,15 @@ class SignatureGenerator:
|
|||||||
self.next_signature.number_samples / self.next_signature.sample_rate_hz
|
self.next_signature.number_samples / self.next_signature.sample_rate_hz
|
||||||
< self.MAX_TIME_SECONDS
|
< self.MAX_TIME_SECONDS
|
||||||
or sum(
|
or sum(
|
||||||
len(peaks) for peaks in self.next_signature.frequency_band_to_sound_peaks.values()
|
len(peaks)
|
||||||
|
for peaks in self.next_signature.frequency_band_to_sound_peaks.values()
|
||||||
)
|
)
|
||||||
< self.MAX_PEAKS
|
< self.MAX_PEAKS
|
||||||
):
|
):
|
||||||
self.process_input(
|
self.process_input(
|
||||||
self.input_pending_processing[self.samples_processed : self.samples_processed + 128]
|
self.input_pending_processing[
|
||||||
|
self.samples_processed : self.samples_processed + 128
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.samples_processed += 128
|
self.samples_processed += 128
|
||||||
|
|
||||||
@@ -107,7 +112,9 @@ class SignatureGenerator:
|
|||||||
self.next_signature.number_samples = 0
|
self.next_signature.number_samples = 0
|
||||||
self.next_signature.frequency_band_to_sound_peaks = {}
|
self.next_signature.frequency_band_to_sound_peaks = {}
|
||||||
|
|
||||||
self.ring_buffer_of_samples: RingBuffer[int] = RingBuffer(buffer_size=2048, default_value=0)
|
self.ring_buffer_of_samples: RingBuffer[int] = RingBuffer(
|
||||||
|
buffer_size=2048, default_value=0
|
||||||
|
)
|
||||||
self.fft_outputs: RingBuffer[List[float]] = RingBuffer(
|
self.fft_outputs: RingBuffer[List[float]] = RingBuffer(
|
||||||
buffer_size=256, default_value=[0.0 * 1025]
|
buffer_size=256, default_value=[0.0 * 1025]
|
||||||
)
|
)
|
||||||
@@ -124,7 +131,9 @@ class SignatureGenerator:
|
|||||||
self.do_peak_spreading_and_recognition()
|
self.do_peak_spreading_and_recognition()
|
||||||
|
|
||||||
def do_fft(self, batch_of_128_s16le_mono_samples):
|
def do_fft(self, batch_of_128_s16le_mono_samples):
|
||||||
type_ring = self.ring_buffer_of_samples.position + len(batch_of_128_s16le_mono_samples)
|
type_ring = self.ring_buffer_of_samples.position + len(
|
||||||
|
batch_of_128_s16le_mono_samples
|
||||||
|
)
|
||||||
self.ring_buffer_of_samples[
|
self.ring_buffer_of_samples[
|
||||||
self.ring_buffer_of_samples.position : type_ring
|
self.ring_buffer_of_samples.position : type_ring
|
||||||
] = batch_of_128_s16le_mono_samples
|
] = batch_of_128_s16le_mono_samples
|
||||||
@@ -159,10 +168,13 @@ class SignatureGenerator:
|
|||||||
temporary_array_1[1] = np.roll(temporary_array_1[1], -1)
|
temporary_array_1[1] = np.roll(temporary_array_1[1], -1)
|
||||||
temporary_array_1[2] = np.roll(temporary_array_1[2], -2)
|
temporary_array_1[2] = np.roll(temporary_array_1[2], -2)
|
||||||
|
|
||||||
origin_last_fft_np = np.hstack([temporary_array_1.max(axis=0)[:-3], origin_last_fft[-3:]])
|
origin_last_fft_np = np.hstack(
|
||||||
|
[temporary_array_1.max(axis=0)[:-3], origin_last_fft[-3:]]
|
||||||
|
)
|
||||||
|
|
||||||
i1, i2, i3 = [
|
i1, i2, i3 = [
|
||||||
(self.spread_fft_output.position + former_fft_num) % self.spread_fft_output.buffer_size
|
(self.spread_fft_output.position + former_fft_num)
|
||||||
|
% self.spread_fft_output.buffer_size
|
||||||
for former_fft_num in [-1, -3, -6]
|
for former_fft_num in [-1, -3, -6]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -234,27 +246,38 @@ class SignatureGenerator:
|
|||||||
fft_number = self.spread_fft_output.num_written - 46
|
fft_number = self.spread_fft_output.num_written - 46
|
||||||
|
|
||||||
peak_magnitude = (
|
peak_magnitude = (
|
||||||
np.log(max(1 / 64, fft_minus_46[bin_position])) * 1477.3 + 6144
|
np.log(max(1 / 64, fft_minus_46[bin_position])) * 1477.3
|
||||||
|
+ 6144
|
||||||
)
|
)
|
||||||
peak_magnitude_before = (
|
peak_magnitude_before = (
|
||||||
np.log(max(1 / 64, fft_minus_46[bin_position - 1])) * 1477.3 + 6144
|
np.log(max(1 / 64, fft_minus_46[bin_position - 1])) * 1477.3
|
||||||
|
+ 6144
|
||||||
)
|
)
|
||||||
peak_magnitude_after = (
|
peak_magnitude_after = (
|
||||||
np.log(max(1 / 64, fft_minus_46[bin_position + 1])) * 1477.3 + 6144
|
np.log(max(1 / 64, fft_minus_46[bin_position + 1])) * 1477.3
|
||||||
|
+ 6144
|
||||||
)
|
)
|
||||||
|
|
||||||
peak_variation_1 = (
|
peak_variation_1 = (
|
||||||
peak_magnitude * 2 - peak_magnitude_before - peak_magnitude_after
|
peak_magnitude * 2
|
||||||
|
- peak_magnitude_before
|
||||||
|
- peak_magnitude_after
|
||||||
)
|
)
|
||||||
peak_variation_2 = (
|
peak_variation_2 = (
|
||||||
(peak_magnitude_after - peak_magnitude_before) * 32 / peak_variation_1
|
(peak_magnitude_after - peak_magnitude_before)
|
||||||
|
* 32
|
||||||
|
/ peak_variation_1
|
||||||
)
|
)
|
||||||
|
|
||||||
corrected_peak_frequency_bin = bin_position * 64 + peak_variation_2
|
corrected_peak_frequency_bin = (
|
||||||
|
bin_position * 64 + peak_variation_2
|
||||||
|
)
|
||||||
|
|
||||||
assert peak_variation_1 > 0
|
assert peak_variation_1 > 0
|
||||||
|
|
||||||
frequency_hz = corrected_peak_frequency_bin * (16000 / 2 / 1024 / 64)
|
frequency_hz = corrected_peak_frequency_bin * (
|
||||||
|
16000 / 2 / 1024 / 64
|
||||||
|
)
|
||||||
|
|
||||||
if 250 < frequency_hz < 520:
|
if 250 < frequency_hz < 520:
|
||||||
band = FrequencyBand.hz_250_520
|
band = FrequencyBand.hz_250_520
|
||||||
@@ -267,7 +290,10 @@ class SignatureGenerator:
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if band not in self.next_signature.frequency_band_to_sound_peaks:
|
if (
|
||||||
|
band
|
||||||
|
not in self.next_signature.frequency_band_to_sound_peaks
|
||||||
|
):
|
||||||
self.next_signature.frequency_band_to_sound_peaks[band] = []
|
self.next_signature.frequency_band_to_sound_peaks[band] = []
|
||||||
|
|
||||||
self.next_signature.frequency_band_to_sound_peaks[band].append(
|
self.next_signature.frequency_band_to_sound_peaks[band].append(
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ class Shazam(Converter, Geo, Request):
|
|||||||
self.language = language
|
self.language = language
|
||||||
self.endpoint_country = endpoint_country
|
self.endpoint_country = endpoint_country
|
||||||
|
|
||||||
async def top_world_tracks(self, limit: int = 200, offset: int = 0) -> Dict[str, Any]:
|
async def top_world_tracks(
|
||||||
|
self, limit: int = 200, offset: int = 0
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Search top world tracks
|
Search top world tracks
|
||||||
|
|
||||||
@@ -292,7 +294,9 @@ class Shazam(Converter, Geo, Request):
|
|||||||
headers=self.headers(),
|
headers=self.headers(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def search_track(self, query: str, limit: int = 10, offset: int = 0) -> Dict[str, Any]:
|
async def search_track(
|
||||||
|
self, query: str, limit: int = 10, offset: int = 0
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Search all tracks by prefix
|
Search all tracks by prefix
|
||||||
:param query: Track full title or prefix title
|
:param query: Track full title or prefix title
|
||||||
|
|||||||
@@ -60,5 +60,7 @@ class Converter:
|
|||||||
signature_generator.feed_input(audio.get_array_of_samples())
|
signature_generator.feed_input(audio.get_array_of_samples())
|
||||||
signature_generator.MAX_TIME_SECONDS = 12
|
signature_generator.MAX_TIME_SECONDS = 12
|
||||||
if audio.duration_seconds > 12 * 3:
|
if audio.duration_seconds > 12 * 3:
|
||||||
signature_generator.samples_processed += 16000 * (int(audio.duration_seconds / 2) - 6)
|
signature_generator.samples_processed += 16000 * (
|
||||||
|
int(audio.duration_seconds / 2) - 6
|
||||||
|
)
|
||||||
return signature_generator
|
return signature_generator
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ class ShazamUrl:
|
|||||||
)
|
)
|
||||||
LISTENING_COUNTER = "https://www.shazam.com/services/count/v2/web/track/{}"
|
LISTENING_COUNTER = "https://www.shazam.com/services/count/v2/web/track/{}"
|
||||||
|
|
||||||
SEARCH_ARTIST_V2 = (
|
SEARCH_ARTIST_V2 = "https://www.shazam.com/services/amapi/v1/catalog/{endpoint_country}/artists/{artist_id}"
|
||||||
"https://www.shazam.com/services/amapi/v1/catalog/{endpoint_country}/artists/{artist_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ class ArtistRelationships(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ArtistViews(BaseModel):
|
class ArtistViews(BaseModel):
|
||||||
top_music_videos: Optional[TopMusicVideosView] = Field(None, alias="top-music-videos")
|
top_music_videos: Optional[TopMusicVideosView] = Field(
|
||||||
|
None, alias="top-music-videos"
|
||||||
|
)
|
||||||
simular_artists: Optional[SimularArtist] = Field(None, alias="similar-artists")
|
simular_artists: Optional[SimularArtist] = Field(None, alias="similar-artists")
|
||||||
latest_release: Optional[LastReleaseModel] = Field(None, alias="latest-release")
|
latest_release: Optional[LastReleaseModel] = Field(None, alias="latest-release")
|
||||||
full_albums: Optional[FullAlbumsModel] = Field(None, alias="full-albums")
|
full_albums: Optional[FullAlbumsModel] = Field(None, alias="full-albums")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class RawSignatureHeader(LittleEndianStructure):
|
|||||||
# field above,
|
# field above,
|
||||||
# it can be inferred and subtracted so that we obtain the number of samples,
|
# it can be inferred and subtracted so that we obtain the number of samples,
|
||||||
# and from the number of samples and sample rate we can obtain the length of the recording
|
# and from the number of samples and sample rate we can obtain the length of the recording
|
||||||
("fixed_value", c_uint32)
|
("fixed_value", c_uint32),
|
||||||
# Calculated as ((15 << 19) + 0x40000) - 0x7c0000 or 00 00 7c 00 - seems pretty constant,
|
# Calculated as ((15 << 19) + 0x40000) - 0x7c0000 or 00 00 7c 00 - seems pretty constant,
|
||||||
# may be different in the "SigType.STREAMING" mode
|
# may be different in the "SigType.STREAMING" mode
|
||||||
]
|
]
|
||||||
@@ -100,7 +100,9 @@ class DecodedMessage:
|
|||||||
assert crc32(check_summable_data) & 0xFFFFFFFF == header.crc32
|
assert crc32(check_summable_data) & 0xFFFFFFFF == header.crc32
|
||||||
assert header.magic2 == 0x94119C00
|
assert header.magic2 == 0x94119C00
|
||||||
|
|
||||||
self.sample_rate_hz = int(SampleRate(header.shifted_sample_rate_id >> 27).name.strip("_"))
|
self.sample_rate_hz = int(
|
||||||
|
SampleRate(header.shifted_sample_rate_id >> 27).name.strip("_")
|
||||||
|
)
|
||||||
|
|
||||||
self.number_samples = int(
|
self.number_samples = int(
|
||||||
header.number_samples_plus_divided_sample_rate - self.sample_rate_hz * 0.24
|
header.number_samples_plus_divided_sample_rate - self.sample_rate_hz * 0.24
|
||||||
@@ -145,13 +147,17 @@ class DecodedMessage:
|
|||||||
|
|
||||||
fft_pass_offset: int = raw_fft_pass[0]
|
fft_pass_offset: int = raw_fft_pass[0]
|
||||||
if fft_pass_offset == 0xFF:
|
if fft_pass_offset == 0xFF:
|
||||||
fft_pass_number = int.from_bytes(frequency_peaks_buf.read(4), "little")
|
fft_pass_number = int.from_bytes(
|
||||||
|
frequency_peaks_buf.read(4), "little"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
fft_pass_number += fft_pass_offset
|
fft_pass_number += fft_pass_offset
|
||||||
|
|
||||||
peak_magnitude = int.from_bytes(frequency_peaks_buf.read(2), "little")
|
peak_magnitude = int.from_bytes(frequency_peaks_buf.read(2), "little")
|
||||||
corrected_peak_frequency_bin = int.from_bytes(frequency_peaks_buf.read(2), "little")
|
corrected_peak_frequency_bin = int.from_bytes(
|
||||||
|
frequency_peaks_buf.read(2), "little"
|
||||||
|
)
|
||||||
|
|
||||||
self.frequency_band_to_sound_peaks[frequency_band].append(
|
self.frequency_band_to_sound_peaks[frequency_band].append(
|
||||||
FrequencyPeak(
|
FrequencyPeak(
|
||||||
@@ -203,7 +209,9 @@ class DecodedMessage:
|
|||||||
|
|
||||||
header.magic1 = 0xCAFE2580
|
header.magic1 = 0xCAFE2580
|
||||||
header.magic2 = 0x94119C00
|
header.magic2 = 0x94119C00
|
||||||
header.shifted_sample_rate_id = int(getattr(SampleRate, "_%s" % self.sample_rate_hz)) << 27
|
header.shifted_sample_rate_id = (
|
||||||
|
int(getattr(SampleRate, "_%s" % self.sample_rate_hz)) << 27
|
||||||
|
)
|
||||||
header.fixed_value = (15 << 19) + 0x40000
|
header.fixed_value = (15 << 19) + 0x40000
|
||||||
header.number_samples_plus_divided_sample_rate = int(
|
header.number_samples_plus_divided_sample_rate = int(
|
||||||
self.number_samples + self.sample_rate_hz * 0.24
|
self.number_samples + self.sample_rate_hz * 0.24
|
||||||
@@ -211,7 +219,9 @@ class DecodedMessage:
|
|||||||
|
|
||||||
contents_buf = BytesIO()
|
contents_buf = BytesIO()
|
||||||
|
|
||||||
for frequency_band, frequency_peaks in sorted(self.frequency_band_to_sound_peaks.items()):
|
for frequency_band, frequency_peaks in sorted(
|
||||||
|
self.frequency_band_to_sound_peaks.items()
|
||||||
|
):
|
||||||
peaks_buf = BytesIO()
|
peaks_buf = BytesIO()
|
||||||
|
|
||||||
fft_pass_number = 0
|
fft_pass_number = 0
|
||||||
@@ -225,13 +235,19 @@ class DecodedMessage:
|
|||||||
|
|
||||||
if frequency_peak.fft_pass_number - fft_pass_number >= 255:
|
if frequency_peak.fft_pass_number - fft_pass_number >= 255:
|
||||||
peaks_buf.write(b"\xff")
|
peaks_buf.write(b"\xff")
|
||||||
peaks_buf.write(frequency_peak.fft_pass_number.to_bytes(4, "little"))
|
peaks_buf.write(
|
||||||
|
frequency_peak.fft_pass_number.to_bytes(4, "little")
|
||||||
|
)
|
||||||
|
|
||||||
fft_pass_number = frequency_peak.fft_pass_number
|
fft_pass_number = frequency_peak.fft_pass_number
|
||||||
|
|
||||||
peaks_buf.write(bytes([frequency_peak.fft_pass_number - fft_pass_number]))
|
peaks_buf.write(
|
||||||
|
bytes([frequency_peak.fft_pass_number - fft_pass_number])
|
||||||
|
)
|
||||||
peaks_buf.write(frequency_peak.peak_magnitude.to_bytes(2, "little"))
|
peaks_buf.write(frequency_peak.peak_magnitude.to_bytes(2, "little"))
|
||||||
peaks_buf.write(frequency_peak.corrected_peak_frequency_bin.to_bytes(2, "little"))
|
peaks_buf.write(
|
||||||
|
frequency_peak.corrected_peak_frequency_bin.to_bytes(2, "little")
|
||||||
|
)
|
||||||
|
|
||||||
fft_pass_number = frequency_peak.fft_pass_number
|
fft_pass_number = frequency_peak.fft_pass_number
|
||||||
|
|
||||||
@@ -245,7 +261,9 @@ class DecodedMessage:
|
|||||||
header.size_minus_header = len(contents_buf.getvalue()) + 8
|
header.size_minus_header = len(contents_buf.getvalue()) + 8
|
||||||
|
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
buf.write(header) # We will rewrite it just after in order to include the final CRC-32
|
buf.write(
|
||||||
|
header
|
||||||
|
) # We will rewrite it just after in order to include the final CRC-32
|
||||||
|
|
||||||
buf.write((0x40000000).to_bytes(4, "little"))
|
buf.write((0x40000000).to_bytes(4, "little"))
|
||||||
buf.write((len(contents_buf.getvalue()) + 8).to_bytes(4, "little"))
|
buf.write((len(contents_buf.getvalue()) + 8).to_bytes(4, "little"))
|
||||||
|
|||||||
Reference in New Issue
Block a user