used black

This commit is contained in:
hhh
2024-11-02 00:10:24 +02:00
parent 1b1f217b75
commit e0a3d256d5
79 changed files with 658 additions and 733 deletions

View File

@@ -1,4 +1,4 @@
from bot import main from bot import main
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -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.[/]")

View File

@@ -1,4 +1,4 @@
from . import main from . import main
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -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(),
) )

View File

@@ -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(),
) )

View File

@@ -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

View File

@@ -8,4 +8,4 @@ dp = Dispatcher(storage=InDbStorage())
console = Console() console = Console()
__all__ = ['bot', 'dp', 'config', 'console'] __all__ = ["bot", "dp", "config", "console"]

View File

@@ -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"]

View File

@@ -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

View File

@@ -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
) )

View File

@@ -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' )
]
)
)
) )

View File

@@ -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}")

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -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,
) )

View File

@@ -12,4 +12,4 @@ router.include_routers(
suppress_verify.router, suppress_verify.router,
) )
__all__ = ['router'] __all__ = ["router"]

View File

@@ -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,
) )

View File

@@ -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

View File

@@ -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,
) )

View File

@@ -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

View File

@@ -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
) )

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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):

View File

@@ -3,4 +3,4 @@ from .db import Db
db = Db() db = Db()
__all__ = ['db'] __all__ = ["db"]

View File

@@ -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")

View File

@@ -7,4 +7,4 @@ deezer = Deezer(
arl=config.tokens.deezer.arl, arl=config.tokens.deezer.arl,
) )
__all__ = ['deezer', 'DeezerBytestream'] __all__ = ["deezer", "DeezerBytestream"]

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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,
) )

View File

@@ -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"
)
} }

View File

@@ -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]))

View File

@@ -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-")

View File

@@ -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()
] ]
) )

View File

@@ -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):

View File

@@ -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):

View File

@@ -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"]

View File

@@ -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
)

View File

@@ -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
}
)

View File

@@ -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()

View File

@@ -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

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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())

View File

@@ -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

View File

@@ -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"])

View File

@@ -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
)

View File

@@ -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=".")]
] ]
), ),
) )

View File

@@ -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"]

View File

@@ -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)
] ]

View File

@@ -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",
),
)
) )
)] ]

View File

@@ -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'
]

View File

@@ -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)
] ]

View File

@@ -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'
]

View File

@@ -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)
] ]

View File

@@ -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,
) )
] ]

View File

@@ -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'
]

View File

@@ -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)
] ]

View File

@@ -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:

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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")

View File

@@ -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"))