From dc334ba85429025f21f38ecd02c0803a0018afac Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Mon, 13 Jan 2025 03:25:45 +0300 Subject: [PATCH 1/9] Refactor imports and logging in multiple files for consistency and clarity. Updated README.md to simplify bot token description. Enhanced error logging format across handlers for improved debugging. Consolidated model imports in database and service modules for better organization. --- README.md | 2 +- app/filters/language.py | 2 +- app/handlers/faq.py | 3 +-- app/handlers/get_song.py | 12 +++++------- app/handlers/language.py | 9 ++++----- app/handlers/menu.py | 6 +++--- app/handlers/pages.py | 3 +-- app/handlers/search.py | 14 ++++++-------- app/middlewares/auth_middleware.py | 7 +++---- app/middlewares/i18n_middleware.py | 2 +- app/utils.py | 2 +- database/crud.py | 1 - database/engine.py | 1 - database/models/__init__.py | 5 +++++ service/__init__.py | 5 +++++ service/core.py | 5 ++--- 16 files changed, 39 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fe562d4..3552e8d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ ```env # Bot Configuration - BOT_TOKEN=your_bot_token # Токен бота от BotFather + BOT_TOKEN=your_bot_token # Токен бота TIMEZONE=UTC # Пример: Europe/Moscow # Database Configuration diff --git a/app/filters/language.py b/app/filters/language.py index 30d30d5..373a20a 100644 --- a/app/filters/language.py +++ b/app/filters/language.py @@ -2,7 +2,7 @@ from aiogram import types from aiogram.filters import Filter -from database.models.user import User +from database.models import User from locales import support_languages diff --git a/app/handlers/faq.py b/app/handlers/faq.py index cd234b8..9f11d8b 100644 --- a/app/handlers/faq.py +++ b/app/handlers/faq.py @@ -4,7 +4,6 @@ from aiogram.utils.i18n import gettext from app.keyboards import inline -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -16,7 +15,7 @@ async def faq_handler(callback: types.CallbackQuery) -> None: reply_markup=inline.get_back_keyboard(gettext, "menu") ) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) def register(router: Router) -> None: diff --git a/app/handlers/get_song.py b/app/handlers/get_song.py index e8261c6..7244ff2 100644 --- a/app/handlers/get_song.py +++ b/app/handlers/get_song.py @@ -3,12 +3,10 @@ from aiogram import types, Router, F, Bot from aiogram.types import BufferedInputFile from aiogram.utils.i18n import gettext -from service.core import MusicService -from service.data import Song +from service import Music, Song from app.utils import load_songs_from_db -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -21,7 +19,7 @@ async def send_song( try: await bot.send_chat_action(callback.message.chat.id, "upload_document") - async with MusicService() as service: + async with Music() as service: song_bytes = await service.get_song_bytes(song) thumbnail_bytes = await service.get_thumbnail_bytes(song) @@ -39,7 +37,7 @@ async def send_song( ) except Exception as e: await callback.message.answer(gettext("send_song_error")) - logger.error(f"Failed to send song: {e}") + logger.error("Failed to send song: %s", e) async def get_song_handler(callback: types.CallbackQuery, bot: Bot) -> None: @@ -52,7 +50,7 @@ async def get_song_handler(callback: types.CallbackQuery, bot: Bot) -> None: await send_song(callback, bot, song) except Exception as e: - logger.error(f"Failed get song handler: {e}") + logger.error("Failed get song handler: %s", e) async def get_all_from_page_handler( @@ -68,7 +66,7 @@ async def get_all_from_page_handler( await send_song(callback, bot, song) except Exception as e: - logger.error(f"Failed get all from page handler: {e}") + logger.error("Failed get all from page handler: %s", e) def register(router: Router) -> None: diff --git a/app/handlers/language.py b/app/handlers/language.py index 9b0eaea..a9694a4 100644 --- a/app/handlers/language.py +++ b/app/handlers/language.py @@ -7,11 +7,10 @@ from app.filters import LanguageFilter from app.keyboards import inline, command from database.crud import CRUD -from database.models.user import User +from database.models import User from .menu import menu_callback_handler -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -23,7 +22,7 @@ async def language_handler(message: types.Message) -> None: reply_markup=inline.language_keyboard ) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) async def language_callback_handler(callback: types.CallbackQuery) -> None: @@ -34,7 +33,7 @@ async def language_callback_handler(callback: types.CallbackQuery) -> None: reply_markup=inline.language_keyboard ) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) async def language_set_handler( @@ -53,7 +52,7 @@ async def language_set_handler( await bot.set_my_commands(command.get_commands(gettext)) await menu_callback_handler(callback) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) def register(router: Router) -> None: diff --git a/app/handlers/menu.py b/app/handlers/menu.py index 4b0b427..61edd9c 100644 --- a/app/handlers/menu.py +++ b/app/handlers/menu.py @@ -1,11 +1,11 @@ -"""Start for the bot.""" +"""Menu handler for the bot.""" import logging from aiogram import types, Router, F from aiogram.filters import CommandStart, Command from aiogram.utils.i18n import gettext from app.keyboards import inline -logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ async def menu_handler(message: types.Message) -> None: reply_markup=inline.get_menu_keyboard(gettext) ) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) async def menu_callback_handler(callback: types.CallbackQuery) -> None: diff --git a/app/handlers/pages.py b/app/handlers/pages.py index b6c8b28..c91d1b2 100644 --- a/app/handlers/pages.py +++ b/app/handlers/pages.py @@ -6,7 +6,6 @@ from app.utils import load_songs_from_db -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -22,7 +21,7 @@ async def pages_handler(callback: types.CallbackQuery) -> None: ) ) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) def register(router: Router) -> None: diff --git a/app/handlers/search.py b/app/handlers/search.py index 0cbb6d2..931923c 100644 --- a/app/handlers/search.py +++ b/app/handlers/search.py @@ -2,14 +2,12 @@ import logging from aiogram import types, Router, F from aiogram.utils.i18n import gettext -from service.core import MusicService -from service.data import Song +from service import Music, Song from database.crud import CRUD -from database.models.search_history import SearchHistory -from database.models.user import User +from database.models import SearchHistory, User from app.keyboards import inline -logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) @@ -36,7 +34,7 @@ async def search_handler(message: types.Message, user: User) -> None: search_message = await message.answer( gettext("searching").format(keyword=keyword) ) - async with MusicService() as service: + async with Music() as service: songs = await service.get_songs_list(keyword) search = await update_search(user, keyword, songs) @@ -47,12 +45,12 @@ async def search_handler(message: types.Message, user: User) -> None: ) ) except Exception as e: - logger.error(f"Failed to send message: {e}") + logger.error("Failed to send message: %s", e) async def get_song_list(list_type: str) -> list[Song]: """Gets the song list.""" - async with MusicService() as service: + async with Music() as service: map_list_type = { "top": service.get_top_songs, "novelties": service.get_novelties diff --git a/app/middlewares/auth_middleware.py b/app/middlewares/auth_middleware.py index d5b5f04..0b780bf 100644 --- a/app/middlewares/auth_middleware.py +++ b/app/middlewares/auth_middleware.py @@ -1,16 +1,15 @@ """Auth middleware module for the app.""" import logging +from datetime import datetime from typing import Any, Awaitable, Callable, Dict -from sqlalchemy import func from aiogram import BaseMiddleware from aiogram.types import Update from database.crud import CRUD -from database.models.user import User +from database.models import User -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -71,7 +70,7 @@ async def _get_or_create_user( # Update user data if it exists else: - user_data['updated_at'] = func.now() + user_data['updated_at'] = datetime.now() db_user = await user_crud.update(db_user, **user_data) logger.info("User %s updated in the database.", user.id) diff --git a/app/middlewares/i18n_middleware.py b/app/middlewares/i18n_middleware.py index f2a9642..41e6b8a 100644 --- a/app/middlewares/i18n_middleware.py +++ b/app/middlewares/i18n_middleware.py @@ -1,7 +1,7 @@ """Custom i18n middleware (language selection).""" from aiogram.types import Message from aiogram.utils.i18n.middleware import I18nMiddleware as BaseI18nMiddleware -from database.models.user import User +from database.models import User class I18nMiddleware(BaseI18nMiddleware): diff --git a/app/utils.py b/app/utils.py index 68895f1..6e7fbcd 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,6 +1,6 @@ """Utils for the bot.""" from database.crud import CRUD -from database.models.search_history import SearchHistory +from database.models import SearchHistory from service.data import Song diff --git a/database/crud.py b/database/crud.py index 45924b8..5e1d66b 100644 --- a/database/crud.py +++ b/database/crud.py @@ -12,7 +12,6 @@ T = TypeVar('T') -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/database/engine.py b/database/engine.py index 2f01a82..6955cc7 100644 --- a/database/engine.py +++ b/database/engine.py @@ -6,7 +6,6 @@ from app import app_config -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) engine = create_async_engine(app_config.db_url, future=True) diff --git a/database/models/__init__.py b/database/models/__init__.py index e69de29..b1f971b 100644 --- a/database/models/__init__.py +++ b/database/models/__init__.py @@ -0,0 +1,5 @@ +"""Database models.""" +from .user import User +from .search_history import SearchHistory + +__all__ = ['User', 'SearchHistory'] diff --git a/service/__init__.py b/service/__init__.py index e69de29..55f7912 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -0,0 +1,5 @@ +"""Service module.""" +from .core import Music +from .data import Song + +__all__ = ['Music', 'Song'] diff --git a/service/core.py b/service/core.py index 1b563e3..db6b767 100644 --- a/service/core.py +++ b/service/core.py @@ -8,11 +8,10 @@ from .data import Song, ServiceConfig from .exceptions import MusicServiceError -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -class MusicService: +class Music: """Service for searching and downloading music.""" BASE_URL = "https://mp3wr.com" SONG_DOWNLOAD_URL = "https://cdn.mp3wr.com" @@ -22,7 +21,7 @@ def __init__(self, config: Optional[ServiceConfig] = None) -> None: self._config = config or ServiceConfig() self._session: Optional[aiohttp.ClientSession] = None - async def __aenter__(self) -> 'MusicService': + async def __aenter__(self) -> 'Music': """Context manager entry point.""" await self.connect() return self From 1be794d11e8521169e9f6599c1215d4d9730e293 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Mon, 13 Jan 2025 04:28:14 +0300 Subject: [PATCH 2/9] Refactor song handling to track handling across the bot. Updated README.md for clarity. Replaced 'Song' with 'Track' in multiple files, including handlers, services, and models. Enhanced user interaction by updating inline keyboards and search functionalities to reflect the change from songs to tracks. Improved localization for new track terminology in English and Russian. This update streamlines the bot's functionality and improves user experience. --- README.md | 2 +- app/handlers/__init__.py | 4 +- app/handlers/get_song.py | 79 ------------------------------ app/handlers/get_track.py | 79 ++++++++++++++++++++++++++++++ app/handlers/pages.py | 12 ++--- app/handlers/search.py | 36 +++++++------- app/keyboards/inline.py | 40 +++++++-------- app/utils.py | 8 +-- database/models/search_history.py | 7 +-- database/models/user.py | 7 +-- locales/en/LC_MESSAGES/messages.po | 20 ++++---- locales/ru/LC_MESSAGES/messages.po | 14 +++--- service/__init__.py | 4 +- service/core.py | 56 ++++++++++----------- service/data.py | 8 +-- 15 files changed, 188 insertions(+), 188 deletions(-) delete mode 100644 app/handlers/get_song.py create mode 100644 app/handlers/get_track.py diff --git a/README.md b/README.md index 3552e8d..87923aa 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## 🎥 Демо (Demo) -Демо бота можно посмотреть [здесь](https://t.me/mygoldmusicbot) +Демо можно посмотреть [здесь](https://t.me/mygoldmusicbot) ## 🚀 Быстрый старт (Quickstart) diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py index 98d4a58..67808cc 100644 --- a/app/handlers/__init__.py +++ b/app/handlers/__init__.py @@ -1,11 +1,11 @@ """Setup router for the bot.""" from aiogram import Dispatcher, Router from . import ( + get_track, language, menu, faq, search, - get_song, pages ) @@ -20,6 +20,6 @@ def setup(dp: Dispatcher) -> None: faq.register(router) search.register(router) pages.register(router) - get_song.register(router) + get_track.register(router) dp.include_router(router) diff --git a/app/handlers/get_song.py b/app/handlers/get_song.py deleted file mode 100644 index 7244ff2..0000000 --- a/app/handlers/get_song.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Get song handler for the bot.""" -import logging -from aiogram import types, Router, F, Bot -from aiogram.types import BufferedInputFile -from aiogram.utils.i18n import gettext -from service import Music, Song -from app.utils import load_songs_from_db - - -logger = logging.getLogger(__name__) - - -async def send_song( - callback: types.CallbackQuery, - bot: Bot, - song: Song -) -> None: - """Send song.""" - try: - await bot.send_chat_action(callback.message.chat.id, "upload_document") - - async with Music() as service: - song_bytes = await service.get_song_bytes(song) - thumbnail_bytes = await service.get_thumbnail_bytes(song) - - audio_file = BufferedInputFile(song_bytes, filename=song.name) - thumbnail_file = BufferedInputFile( - thumbnail_bytes, filename=song.name - ) - - await callback.message.answer_audio( - audio_file, - title=song.title, - performer=song.performer, - caption=gettext("promo_caption").format(username=bot._me.username), - thumbnail=thumbnail_file, - ) - except Exception as e: - await callback.message.answer(gettext("send_song_error")) - logger.error("Failed to send song: %s", e) - - -async def get_song_handler(callback: types.CallbackQuery, bot: Bot) -> None: - """Get song handler.""" - try: - _, _, search_id, song_index = callback.data.split(":") - songs: list[Song] = await load_songs_from_db(search_id) - song: Song = songs[int(song_index)] - await callback.answer(gettext("song_sending")) - await send_song(callback, bot, song) - - except Exception as e: - logger.error("Failed get song handler: %s", e) - - -async def get_all_from_page_handler( - callback: types.CallbackQuery, bot: Bot -) -> None: - """Get all songs from page handler.""" - try: - _, _, search_id, start_indx, end_indx = callback.data.split(":") - songs: list[Song] = await load_songs_from_db(search_id) - songs = songs[int(start_indx):int(end_indx)] - await callback.answer(gettext("song_sending")) - for song in songs: - await send_song(callback, bot, song) - - except Exception as e: - logger.error("Failed get all from page handler: %s", e) - - -def register(router: Router) -> None: - """Registers get song handler with the router.""" - router.callback_query.register( - get_song_handler, F.data.startswith("song:get:") - ) - router.callback_query.register( - get_all_from_page_handler, F.data.startswith("song:all:") - ) diff --git a/app/handlers/get_track.py b/app/handlers/get_track.py new file mode 100644 index 0000000..437d602 --- /dev/null +++ b/app/handlers/get_track.py @@ -0,0 +1,79 @@ +"""Get track handler for the bot.""" +import logging +from aiogram import types, Router, F, Bot +from aiogram.types import BufferedInputFile +from aiogram.utils.i18n import gettext +from service import Music, Track +from app.utils import load_tracks_from_db + + +logger = logging.getLogger(__name__) + + +async def send_track( + callback: types.CallbackQuery, + bot: Bot, + track: Track +) -> None: + """Send track.""" + try: + await bot.send_chat_action(callback.message.chat.id, "upload_document") + + async with Music() as service: + audio_bytes = await service.get_audio_bytes(track) + thumbnail_bytes = await service.get_thumbnail_bytes(track) + + audio_file = BufferedInputFile(audio_bytes, filename=track.name) + thumbnail_file = BufferedInputFile( + thumbnail_bytes, filename=track.name + ) + + await callback.message.answer_audio( + audio_file, + title=track.title, + performer=track.performer, + caption=gettext("promo_caption").format(username=bot._me.username), + thumbnail=thumbnail_file, + ) + except Exception as e: + await callback.message.answer(gettext("send_track_error")) + logger.error("Failed to send track: %s", e) + + +async def get_track_handler(callback: types.CallbackQuery, bot: Bot) -> None: + """Get track handler.""" + try: + _, _, search_id, index = callback.data.split(":") + tracks: list[Track] = await load_tracks_from_db(search_id) + track: Track = tracks[int(index)] + await callback.answer(gettext("track_sending")) + await send_track(callback, bot, track) + + except Exception as e: + logger.error("Failed get track handler: %s", e) + + +async def get_all_from_page_handler( + callback: types.CallbackQuery, bot: Bot +) -> None: + """Get all tracks from page handler.""" + try: + _, _, search_id, start_indx, end_indx = callback.data.split(":") + tracks: list[Track] = await load_tracks_from_db(search_id) + tracks = tracks[int(start_indx):int(end_indx)] + await callback.answer(gettext("track_sending")) + for track in tracks: + await send_track(callback, bot, track) + + except Exception as e: + logger.error("Failed get all from page handler: %s", e) + + +def register(router: Router) -> None: + """Registers get track handler with the router.""" + router.callback_query.register( + get_track_handler, F.data.startswith("track:get:") + ) + router.callback_query.register( + get_all_from_page_handler, F.data.startswith("track:all:") + ) diff --git a/app/handlers/pages.py b/app/handlers/pages.py index c91d1b2..4b5305e 100644 --- a/app/handlers/pages.py +++ b/app/handlers/pages.py @@ -1,9 +1,9 @@ """Pages handler for the bot.""" import logging from aiogram import types, Router, F -from service.data import Song +from service.data import Track from app.keyboards import inline -from app.utils import load_songs_from_db +from app.utils import load_tracks_from_db logger = logging.getLogger(__name__) @@ -13,11 +13,11 @@ async def pages_handler(callback: types.CallbackQuery) -> None: """Handles the pages navigation.""" try: _, _, search_id, page = callback.data.split(":") - songs: list[Song] = await load_songs_from_db(search_id) + tracks: list[Track] = await load_tracks_from_db(search_id) await callback.message.edit_reply_markup( - reply_markup=inline.get_keyboard_of_songs( - songs, search_id, int(page) + reply_markup=inline.get_keyboard_of_tracks( + tracks, search_id, int(page) ) ) except Exception as e: @@ -27,5 +27,5 @@ async def pages_handler(callback: types.CallbackQuery) -> None: def register(router: Router) -> None: """Registers pages handler with the router.""" router.callback_query.register( - pages_handler, F.data.startswith("song:page") + pages_handler, F.data.startswith("track:page") ) diff --git a/app/handlers/search.py b/app/handlers/search.py index 931923c..81de3c2 100644 --- a/app/handlers/search.py +++ b/app/handlers/search.py @@ -2,7 +2,7 @@ import logging from aiogram import types, Router, F from aiogram.utils.i18n import gettext -from service import Music, Song +from service import Music, Track from database.crud import CRUD from database.models import SearchHistory, User from app.keyboards import inline @@ -12,7 +12,7 @@ async def update_search( - user: User, keyword: str, songs: list[Song] + user: User, keyword: str, tracks: list[Track] ) -> SearchHistory: """Updates the user in the database.""" user_crud = CRUD(User) @@ -22,7 +22,7 @@ async def update_search( search = await search_history_crud.create( user_id=user.id, keyword=keyword, - songs=[song.__dict__ for song in songs] + tracks=[track.__dict__ for track in tracks] ) return search @@ -35,41 +35,39 @@ async def search_handler(message: types.Message, user: User) -> None: gettext("searching").format(keyword=keyword) ) async with Music() as service: - songs = await service.get_songs_list(keyword) + tracks = await service.search(keyword) - search = await update_search(user, keyword, songs) + search = await update_search(user, keyword, tracks) await search_message.edit_text( gettext("search_result").format(keyword=keyword), - reply_markup=inline.get_keyboard_of_songs( - songs, search.id - ) + reply_markup=inline.get_keyboard_of_tracks(tracks, search.id) ) except Exception as e: logger.error("Failed to send message: %s", e) -async def get_song_list(list_type: str) -> list[Song]: - """Gets the song list.""" +async def get_track_list(list_type: str) -> list[Track]: + """Gets the track list.""" async with Music() as service: map_list_type = { - "top": service.get_top_songs, - "novelties": service.get_novelties + "top_hits": service.get_top_hits, + "new_hits": service.get_new_hits } return await map_list_type[list_type]() -async def song_lists_handler( +async def track_lists_handler( callback: types.CallbackQuery, user: User ) -> None: - """Handles the song lists.""" + """Handles the track lists.""" _, _, list_type = callback.data.split(":") - songs = await get_song_list(list_type) - search = await update_search(user, list_type, songs) + tracks = await get_track_list(list_type) + search = await update_search(user, list_type, tracks) await callback.message.answer( gettext(list_type), - reply_markup=inline.get_keyboard_of_songs( - songs, search.id + reply_markup=inline.get_keyboard_of_tracks( + tracks, search.id ) ) @@ -78,5 +76,5 @@ def register(router: Router) -> None: """Registers search handler with the router.""" router.message.register(search_handler) router.callback_query.register( - song_lists_handler, F.data.startswith("song:list:") + track_lists_handler, F.data.startswith("track:list:") ) diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 36dcf3f..ccc9917 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -2,32 +2,32 @@ from typing import Callable from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton -from service.data import Song +from service.data import Track from locales import support_languages -def get_keyboard_of_songs( - songs: list[Song], +def get_keyboard_of_tracks( + tracks: list[Track], search_id: int, page: int = 0 ) -> InlineKeyboardMarkup: - """Create paginated inline keyboard for song selection.""" - SONGS_PER_PAGE = 10 + """Create paginated inline keyboard for track selection.""" + TRACKS_PER_PAGE = 10 - total_pages = max((len(songs) - 1) // SONGS_PER_PAGE, 0) + total_pages = max((len(tracks) - 1) // TRACKS_PER_PAGE, 0) page = min(max(0, page), total_pages) - start_indx = page * SONGS_PER_PAGE - end_indx = (page + 1) * SONGS_PER_PAGE - current_page_songs = songs[start_indx:end_indx] + start_indx = page * TRACKS_PER_PAGE + end_indx = (page + 1) * TRACKS_PER_PAGE + current_page = tracks[start_indx:end_indx] keyboard = [ [ InlineKeyboardButton( - text=f"{song.performer} - {song.title}", - callback_data=f"song:get:{search_id}:{song.index}" + text=f"{track.performer} - {track.title}", + callback_data=f"track:get:{search_id}:{track.index}" ) - ] for song in current_page_songs + ] for track in current_page ] if total_pages > 0: @@ -41,9 +41,9 @@ def create_navigation_button(is_next: bool) -> InlineKeyboardButton: if is_next and not is_available else "◀️" if not is_next and is_available else "⏺️", callback_data=( - f"song:page:{search_id}:" + f"track:page:{search_id}:" f"{page + 1 if is_next else page - 1}" - if is_available else "song:noop" + if is_available else "track:noop" ) ) @@ -51,14 +51,14 @@ def create_navigation_button(is_next: bool) -> InlineKeyboardButton: create_navigation_button(is_next=False), InlineKeyboardButton( text=f"{page + 1}/{total_pages + 1}", - callback_data="song:noop" + callback_data="track:noop" ), create_navigation_button(is_next=True), ]) keyboard.append([ InlineKeyboardButton( text="🔽", - callback_data=f"song:all:{search_id}:{start_indx}:{end_indx}" + callback_data=f"track:all:{search_id}:{start_indx}:{end_indx}" ) ]) return InlineKeyboardMarkup(inline_keyboard=keyboard) @@ -82,12 +82,12 @@ def get_menu_keyboard(gettext: Callable[[str], str]) -> InlineKeyboardMarkup: inline_keyboard=[ [ InlineKeyboardButton( - text=gettext("top_button"), - callback_data="song:list:top" + text=gettext("top_hits_button"), + callback_data="track:list:top_hits" ), InlineKeyboardButton( - text=gettext("novelties_button"), - callback_data="song:list:novelties" + text=gettext("new_hits_button"), + callback_data="track:list:new_hits" ) ], [ diff --git a/app/utils.py b/app/utils.py index 6e7fbcd..e636bb2 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,11 +1,11 @@ """Utils for the bot.""" from database.crud import CRUD from database.models import SearchHistory -from service.data import Song +from service.data import Track -async def load_songs_from_db(search_id: int) -> list[Song]: - """Get songs from the database.""" +async def load_tracks_from_db(search_id: int) -> list[Track]: + """Get tracks from the database.""" search_history_crud = CRUD(SearchHistory) search: SearchHistory = await search_history_crud.get(id=int(search_id)) - return [Song(**song) for song in search.songs] + return [Track(**track) for track in search.tracks] diff --git a/database/models/search_history.py b/database/models/search_history.py index 4cfe106..07f3ecd 100644 --- a/database/models/search_history.py +++ b/database/models/search_history.py @@ -1,7 +1,8 @@ """Search history database model.""" +from datetime import datetime from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy import ( - Column, String, DateTime, func, Integer, BigInteger, ForeignKey + Column, String, DateTime, Integer, BigInteger, ForeignKey ) from ..engine import Base @@ -14,5 +15,5 @@ class SearchHistory(Base): id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.id')) keyword = Column(String, default=None) - songs = Column(JSONB, default=[]) - created_at = Column(DateTime, default=func.now()) + tracks = Column(JSONB, default=[]) + created_at = Column(DateTime, default=datetime.now()) diff --git a/database/models/user.py b/database/models/user.py index a2a41b1..bbd67d8 100644 --- a/database/models/user.py +++ b/database/models/user.py @@ -1,5 +1,6 @@ """User database model.""" -from sqlalchemy import Column, String, BigInteger, DateTime, func, Integer +from datetime import datetime +from sqlalchemy import Column, String, BigInteger, DateTime, Integer from ..engine import Base @@ -14,5 +15,5 @@ class User(Base): language_code = Column(String, default=None) state = Column(String, default=None) search_queries = Column(Integer, default=0) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now()) + created_at = Column(DateTime, default=datetime.now()) + updated_at = Column(DateTime, default=datetime.now()) diff --git a/locales/en/LC_MESSAGES/messages.po b/locales/en/LC_MESSAGES/messages.po index 409d2fa..3d2b51c 100644 --- a/locales/en/LC_MESSAGES/messages.po +++ b/locales/en/LC_MESSAGES/messages.po @@ -46,17 +46,17 @@ msgstr "❓ Frequently asked questions\n\n" "How to change the language ❓\n" "• Send me the /language command in the chat or click the «🌎 Language» button in the menu." -msgid "top_button" -msgstr "🔥 Top songs" +msgid "top_hits_button" +msgstr "🔥 Top" -msgid "top" +msgid "top_hits" msgstr "🔥 Popular music" -msgid "novelties_button" -msgstr "🆕 Novelties" +msgid "new_hits_button" +msgstr "🆕 New tracks" -msgid "novelties" -msgstr "🆕 New music" +msgid "new_hits" +msgstr "🆕 New tracks" msgid "searching" msgstr "🔍 Searching: {keyword}" @@ -64,10 +64,10 @@ msgstr "🔍 Searching: {keyword}" msgid "search_result" msgstr "🎧 {keyword}:" -msgid "song_sending" -msgstr "🎧 Sending song..." +msgid "track_sending" +msgstr "🎧 Sending track..." -msgid "send_song_error" +msgid "send_track_error" msgstr "❌ Oops! Something went wrong. Audio file is not available." msgid "promo_caption" diff --git a/locales/ru/LC_MESSAGES/messages.po b/locales/ru/LC_MESSAGES/messages.po index ccb77f1..11aa628 100644 --- a/locales/ru/LC_MESSAGES/messages.po +++ b/locales/ru/LC_MESSAGES/messages.po @@ -47,16 +47,16 @@ msgstr "❓ Часто задаваемые вопросы\n\n" "Как изменить язык ❓\n" "• Отправьте мне команду /language в чат или нажмите на кнопку «🌎 Язык» в меню." -msgid "top_button" +msgid "top_hits_button" msgstr "🔥 Топ песен" -msgid "top" +msgid "top_hits" msgstr "🔥 Популярная музыка" -msgid "novelties_button" +msgid "new_hits_button" msgstr "🆕 Новинки" -msgid "novelties" +msgid "new_hits" msgstr "🆕 Новинки музыки" msgid "searching" @@ -65,10 +65,10 @@ msgstr "🔍 Поиск: {keyword}" msgid "search_result" msgstr "🎧 {keyword}" -msgid "song_sending" -msgstr "🎧 Отправка песни..." +msgid "track_sending" +msgstr "🎧 Отправка трека..." -msgid "send_song_error" +msgid "send_track_error" msgstr "❌ Ой! Что-то пошло не так. Аудиофайл не доступен." msgid "promo_caption" diff --git a/service/__init__.py b/service/__init__.py index 55f7912..f629b2a 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -1,5 +1,5 @@ """Service module.""" from .core import Music -from .data import Song +from .data import Track -__all__ = ['Music', 'Song'] +__all__ = ['Music', 'Track'] diff --git a/service/core.py b/service/core.py index db6b767..ebf0e07 100644 --- a/service/core.py +++ b/service/core.py @@ -5,7 +5,7 @@ import aiohttp from bs4 import BeautifulSoup -from .data import Song, ServiceConfig +from .data import Track, ServiceConfig from .exceptions import MusicServiceError logger = logging.getLogger(__name__) @@ -14,7 +14,7 @@ class Music: """Service for searching and downloading music.""" BASE_URL = "https://mp3wr.com" - SONG_DOWNLOAD_URL = "https://cdn.mp3wr.com" + TRACK_DOWNLOAD_URL = "https://cdn.mp3wr.com" def __init__(self, config: Optional[ServiceConfig] = None) -> None: """Initialize music service with optional configuration.""" @@ -43,7 +43,7 @@ async def disconnect(self) -> None: await self._session.close() self._session = None - async def get_songs_list(self, keyword: str) -> list[Song]: + async def search(self, keyword: str) -> list[Track]: """Search for music by keyword.""" if not self._session: await self.connect() @@ -51,50 +51,50 @@ async def get_songs_list(self, keyword: str) -> list[Song]: url = f"{self.BASE_URL}/search/{keyword}" logger.info("Searching music with keyword: %s", keyword) - return await self._parse_songs(url, is_search=True) + return await self._parse_tracks(url, is_search=True) - async def get_top_songs(self) -> list[Song]: - """Get top songs.""" + async def get_top_hits(self) -> list[Track]: + """Get top tracks.""" url = f"{self.BASE_URL}/besthit" - return await self._parse_songs(url) + return await self._parse_tracks(url) - async def get_novelties(self) -> list[Song]: - """Get novelties.""" + async def get_new_hits(self) -> list[Track]: + """Get new hits.""" url = f"{self.BASE_URL}/newhit" - return await self._parse_songs(url) + return await self._parse_tracks(url) - async def _parse_songs( + async def _parse_tracks( self, url: str, is_search: bool = False - ) -> list[Song]: - """Parse songs from the given URL.""" + ) -> list[Track]: + """Parse tracks from the given URL.""" try: async with self._session.get( url, timeout=self._config.timeout ) as response: response.raise_for_status() soup = BeautifulSoup(await response.text(), "html.parser") - songs = [ - Song.from_element(song_data, index, is_search) - for index, song_data in enumerate( + tracks = [ + Track.from_element(track_data, index, is_search) + for index, track_data in enumerate( soup.find_all("item") if is_search else soup.find_all("li", class_="sarki-liste") ) ] - logger.info("Found %d songs", len(songs)) - return songs + logger.info("Found %d tracks", len(tracks)) + return tracks except (aiohttp.ClientError, TimeoutError) as e: raise MusicServiceError(f"Failed to search music: {str(e)}") from e async def _download_data( - self, url: str, resource_type: str, song_name: str + self, url: str, resource_type: str, track_name: str ) -> bytes: """Generic method for downloading data.""" if not self._session: await self.connect() - logger.info("Downloading %s for song: %s", resource_type, song_name) + logger.info("Downloading %s for track: %s", resource_type, track_name) try: async with self._session.get( @@ -108,16 +108,16 @@ async def _download_data( raise MusicServiceError( f"Failed to download {resource_type}: {str(e)}") from e - async def get_song_bytes(self, song: Song) -> bytes: + async def get_audio_bytes(self, track: Track) -> bytes: """Download music file.""" - url = song.audio_url + url = track.audio_url if url.startswith("/"): - url = f"{self.BASE_URL}{song.audio_url}" - return await self._download_data(url, "song", song.name) + url = f"{self.BASE_URL}{track.audio_url}" + return await self._download_data(url, "audio", track.name) - async def get_thumbnail_bytes(self, song: Song) -> bytes: + async def get_thumbnail_bytes(self, track: Track) -> bytes: """Download thumbnail image.""" - url = song.thumbnail_url + url = track.thumbnail_url if url.startswith("/"): - url = f"{self.BASE_URL}{song.thumbnail_url}" - return await self._download_data(url, "thumbnail", song.name) + url = f"{self.BASE_URL}{track.thumbnail_url}" + return await self._download_data(url, "thumbnail", track.name) diff --git a/service/data.py b/service/data.py index 506e0e9..63a603c 100644 --- a/service/data.py +++ b/service/data.py @@ -17,8 +17,8 @@ class ServiceConfig: @dataclass -class Song: - """Song data class""" +class Track: + """Track data class""" index: int name: str @@ -33,8 +33,8 @@ def from_element( element: BeautifulSoup, index: int, is_search: bool = False, - ) -> "Song": - """Create Song from BeautifulSoup element""" + ) -> "Track": + """Create Track from BeautifulSoup element""" full_name = element.find(class_="artist_name").text.strip() performer, title = full_name.split(" - ", 1) audio_url = element.find(class_="right").get("data-id") From 1a2f82c92fd80457638ab9ff11adfb675cce1908 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Mon, 13 Jan 2025 05:05:38 +0300 Subject: [PATCH 3/9] Add configuration management and restructure bot modules - Introduced a new `configs.py` file for centralized configuration management, replacing the previous `app/configs.py`. - Updated `main.py` to utilize the new configuration structure for bot token and database settings. - Removed the old `app/__init__.py` and `app/configs.py` files to streamline the codebase. - Created new modules in the `bot` directory, including handlers, filters, and utilities, to enhance modularity and organization. - Implemented various bot handlers for menu, language selection, search functionality, and track management, improving user interaction and experience. - Added middleware for authentication and internationalization, ensuring user data is managed effectively and language preferences are respected. This update enhances the bot's architecture, making it more maintainable and scalable for future features. --- app/__init__.py | 5 ---- app/configs.py | 28 ------------------- {app/keyboards => bot}/__init__.py | 0 {app => bot}/filters/__init__.py | 2 +- {app => bot}/filters/language.py | 0 {app => bot}/handlers/__init__.py | 0 {app => bot}/handlers/faq.py | 2 +- {app => bot}/handlers/get_track.py | 2 +- {app => bot}/handlers/language.py | 4 +-- {app => bot}/handlers/menu.py | 2 +- {app => bot}/handlers/pages.py | 4 +-- {app => bot}/handlers/search.py | 2 +- bot/keyboards/__init__.py | 0 {app => bot}/keyboards/command.py | 0 {app => bot}/keyboards/inline.py | 0 {app => bot}/middlewares/__init__.py | 2 +- {app => bot}/middlewares/auth_middleware.py | 2 +- {app => bot}/middlewares/i18n_middleware.py | 0 {app => bot}/utils.py | 0 configs.py | 30 +++++++++++++++++++++ database/engine.py | 4 +-- main.py | 10 ++++--- 22 files changed, 49 insertions(+), 50 deletions(-) delete mode 100644 app/__init__.py delete mode 100644 app/configs.py rename {app/keyboards => bot}/__init__.py (100%) rename {app => bot}/filters/__init__.py (71%) rename {app => bot}/filters/language.py (100%) rename {app => bot}/handlers/__init__.py (100%) rename {app => bot}/handlers/faq.py (95%) rename {app => bot}/handlers/get_track.py (98%) rename {app => bot}/handlers/language.py (96%) rename {app => bot}/handlers/menu.py (96%) rename {app => bot}/handlers/pages.py (91%) rename {app => bot}/handlers/search.py (98%) create mode 100644 bot/keyboards/__init__.py rename {app => bot}/keyboards/command.py (100%) rename {app => bot}/keyboards/inline.py (100%) rename {app => bot}/middlewares/__init__.py (91%) rename {app => bot}/middlewares/auth_middleware.py (98%) rename {app => bot}/middlewares/i18n_middleware.py (100%) rename {app => bot}/utils.py (100%) create mode 100644 configs.py diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index a957775..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""App init module.""" -from .configs import app_config - - -__all__ = ['app_config'] diff --git a/app/configs.py b/app/configs.py deleted file mode 100644 index f1d8675..0000000 --- a/app/configs.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Configurations for the app.""" -import os -from dataclasses import dataclass - - -os.environ['TZ'] = os.getenv('TIMEZONE', 'UTC') - - -@dataclass -class APPConfig: - """Configuration class for the bot.""" - - # Bot token - bot_token: str = os.getenv('BOT_TOKEN') - - # Database Postgres - db_host: str = os.getenv('POSTGRES_HOST', 'db') - db_port: str = os.getenv('POSTGRES_PORT', '5432') - db_user: str = os.getenv('POSTGRES_USER') - db_password: str = os.getenv('POSTGRES_PASSWORD') - db_name: str = os.getenv('POSTGRES_DB') - db_url: str = ( - "postgresql+asyncpg://" - f"{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" - ) - - -app_config = APPConfig() diff --git a/app/keyboards/__init__.py b/bot/__init__.py similarity index 100% rename from app/keyboards/__init__.py rename to bot/__init__.py diff --git a/app/filters/__init__.py b/bot/filters/__init__.py similarity index 71% rename from app/filters/__init__.py rename to bot/filters/__init__.py index 434dd37..8518d43 100644 --- a/app/filters/__init__.py +++ b/bot/filters/__init__.py @@ -1,4 +1,4 @@ -"""Filters for the app.""" +"""Filters for the bot.""" from .language import LanguageFilter __all__ = ['LanguageFilter'] diff --git a/app/filters/language.py b/bot/filters/language.py similarity index 100% rename from app/filters/language.py rename to bot/filters/language.py diff --git a/app/handlers/__init__.py b/bot/handlers/__init__.py similarity index 100% rename from app/handlers/__init__.py rename to bot/handlers/__init__.py diff --git a/app/handlers/faq.py b/bot/handlers/faq.py similarity index 95% rename from app/handlers/faq.py rename to bot/handlers/faq.py index 9f11d8b..3baa00e 100644 --- a/app/handlers/faq.py +++ b/bot/handlers/faq.py @@ -2,7 +2,7 @@ import logging from aiogram import types, Router, F from aiogram.utils.i18n import gettext -from app.keyboards import inline +from bot.keyboards import inline logger = logging.getLogger(__name__) diff --git a/app/handlers/get_track.py b/bot/handlers/get_track.py similarity index 98% rename from app/handlers/get_track.py rename to bot/handlers/get_track.py index 437d602..7581fe1 100644 --- a/app/handlers/get_track.py +++ b/bot/handlers/get_track.py @@ -4,7 +4,7 @@ from aiogram.types import BufferedInputFile from aiogram.utils.i18n import gettext from service import Music, Track -from app.utils import load_tracks_from_db +from bot.utils import load_tracks_from_db logger = logging.getLogger(__name__) diff --git a/app/handlers/language.py b/bot/handlers/language.py similarity index 96% rename from app/handlers/language.py rename to bot/handlers/language.py index a9694a4..ae0de8b 100644 --- a/app/handlers/language.py +++ b/bot/handlers/language.py @@ -4,8 +4,8 @@ from aiogram.utils import i18n from aiogram.utils.i18n import gettext from aiogram.filters import Command -from app.filters import LanguageFilter -from app.keyboards import inline, command +from bot.filters import LanguageFilter +from bot.keyboards import inline, command from database.crud import CRUD from database.models import User from .menu import menu_callback_handler diff --git a/app/handlers/menu.py b/bot/handlers/menu.py similarity index 96% rename from app/handlers/menu.py rename to bot/handlers/menu.py index 61edd9c..ec6eef3 100644 --- a/app/handlers/menu.py +++ b/bot/handlers/menu.py @@ -3,7 +3,7 @@ from aiogram import types, Router, F from aiogram.filters import CommandStart, Command from aiogram.utils.i18n import gettext -from app.keyboards import inline +from bot.keyboards import inline logger = logging.getLogger(__name__) diff --git a/app/handlers/pages.py b/bot/handlers/pages.py similarity index 91% rename from app/handlers/pages.py rename to bot/handlers/pages.py index 4b5305e..31d6c4c 100644 --- a/app/handlers/pages.py +++ b/bot/handlers/pages.py @@ -2,8 +2,8 @@ import logging from aiogram import types, Router, F from service.data import Track -from app.keyboards import inline -from app.utils import load_tracks_from_db +from bot.keyboards import inline +from bot.utils import load_tracks_from_db logger = logging.getLogger(__name__) diff --git a/app/handlers/search.py b/bot/handlers/search.py similarity index 98% rename from app/handlers/search.py rename to bot/handlers/search.py index 81de3c2..2f17a86 100644 --- a/app/handlers/search.py +++ b/bot/handlers/search.py @@ -5,7 +5,7 @@ from service import Music, Track from database.crud import CRUD from database.models import SearchHistory, User -from app.keyboards import inline +from bot.keyboards import inline logger = logging.getLogger(__name__) diff --git a/bot/keyboards/__init__.py b/bot/keyboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/keyboards/command.py b/bot/keyboards/command.py similarity index 100% rename from app/keyboards/command.py rename to bot/keyboards/command.py diff --git a/app/keyboards/inline.py b/bot/keyboards/inline.py similarity index 100% rename from app/keyboards/inline.py rename to bot/keyboards/inline.py diff --git a/app/middlewares/__init__.py b/bot/middlewares/__init__.py similarity index 91% rename from app/middlewares/__init__.py rename to bot/middlewares/__init__.py index 8551236..bc2ee51 100644 --- a/app/middlewares/__init__.py +++ b/bot/middlewares/__init__.py @@ -1,4 +1,4 @@ -"""Middlewares for the app.""" +"""Middlewares for the bot.""" from aiogram import Dispatcher from aiogram.utils.i18n import I18n from .auth_middleware import AuthMiddleware diff --git a/app/middlewares/auth_middleware.py b/bot/middlewares/auth_middleware.py similarity index 98% rename from app/middlewares/auth_middleware.py rename to bot/middlewares/auth_middleware.py index 0b780bf..0649498 100644 --- a/app/middlewares/auth_middleware.py +++ b/bot/middlewares/auth_middleware.py @@ -1,4 +1,4 @@ -"""Auth middleware module for the app.""" +"""Auth middleware module for the bot.""" import logging from datetime import datetime from typing import Any, Awaitable, Callable, Dict diff --git a/app/middlewares/i18n_middleware.py b/bot/middlewares/i18n_middleware.py similarity index 100% rename from app/middlewares/i18n_middleware.py rename to bot/middlewares/i18n_middleware.py diff --git a/app/utils.py b/bot/utils.py similarity index 100% rename from app/utils.py rename to bot/utils.py diff --git a/configs.py b/configs.py new file mode 100644 index 0000000..8150909 --- /dev/null +++ b/configs.py @@ -0,0 +1,30 @@ +"""Configurations for the app.""" +import os +from dataclasses import dataclass + + +os.environ['TZ'] = os.getenv('TIMEZONE', 'UTC') + + +@dataclass +class BotConfig: + """Configuration class for the bot.""" + token: str = os.getenv('BOT_TOKEN') + + +@dataclass +class DBConfig: + """Configuration class for the database.""" + host: str = os.getenv('POSTGRES_HOST', 'db') + port: str = os.getenv('POSTGRES_PORT', '5432') + user: str = os.getenv('POSTGRES_USER') + password: str = os.getenv('POSTGRES_PASSWORD') + db: str = os.getenv('POSTGRES_DB') + url: str = ( + "postgresql+asyncpg://" + f"{user}:{password}@{host}:{port}/{db}" + ) + + +bot_config = BotConfig() +db_config = DBConfig() diff --git a/database/engine.py b/database/engine.py index 6955cc7..4497a48 100644 --- a/database/engine.py +++ b/database/engine.py @@ -3,12 +3,12 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker -from app import app_config +from configs import db_config logger = logging.getLogger(__name__) -engine = create_async_engine(app_config.db_url, future=True) +engine = create_async_engine(db_config.url, future=True) Base = declarative_base() async_session_factory = sessionmaker( diff --git a/main.py b/main.py index de39f16..aa43231 100644 --- a/main.py +++ b/main.py @@ -2,14 +2,16 @@ """Entry point of the bot application.""" import logging import asyncio + from aiogram import Bot, Dispatcher from aiogram.client.bot import DefaultBotProperties from aiogram.utils.token import TokenValidationError from aiogram.utils.i18n import I18n from aiogram.fsm.storage.memory import MemoryStorage -from app import handlers, middlewares, app_config -from database.engine import init_db +from bot import handlers, middlewares +from database.engine import init_db +from configs import bot_config logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -20,13 +22,13 @@ async def create_bot() -> Bot: Create and return a Bot instance.""" try: bot = Bot( - token=app_config.bot_token, + token=bot_config.token, default=DefaultBotProperties(parse_mode='HTML') ) logger.info('Successfully created bot instance.') return bot except TokenValidationError as e: - logger.error('Invalid token provided: %s', app_config.bot_token) + logger.error('Invalid token provided: %s', bot_config.token) raise e except Exception as e: logger.error('Failed to create bot instance: %s', str(e)) From 6196b00cdea593a00cee259e05dd6996b77ebcc7 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Mon, 13 Jan 2025 05:42:31 +0300 Subject: [PATCH 4/9] Enhance language and menu handlers for improved user interaction - Updated language and menu handlers to support both message and callback query events, streamlining the handling of user inputs. - Refactored error handling to provide clearer logging for debugging purposes. - Revised README.md to include a new feature for auto-detecting user language and clarified demo link description. --- README.md | 4 +++- bot/handlers/language.py | 35 ++++++++++++++--------------------- bot/handlers/menu.py | 28 +++++++++++++--------------- bot/handlers/search.py | 2 +- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 87923aa..e25d5e7 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,15 @@ - 🌅 Аудио с обложкой - 📱 Интуитивный интерфейс - 🌍 Поддержка нескольких языков (английский, русский) +- 🌍 Автоопределение языка пользователя - 🐳 Легкое развертывание через Docker - 🛡️ Безопасное хранение данных в PostgreSQL +- 📊 Управление базой данных (Adminer) - 📝 Лицензия: Apache License 2.0 ## 🎥 Демо (Demo) -Демо можно посмотреть [здесь](https://t.me/mygoldmusicbot) +Демо бота можно посмотреть [здесь](https://t.me/mygoldmusicbot) ## 🚀 Быстрый старт (Quickstart) diff --git a/bot/handlers/language.py b/bot/handlers/language.py index ae0de8b..cf937f4 100644 --- a/bot/handlers/language.py +++ b/bot/handlers/language.py @@ -1,5 +1,6 @@ """Language handler for the bot.""" import logging +from typing import Union from aiogram import types, Router, Bot, F from aiogram.utils import i18n from aiogram.utils.i18n import gettext @@ -8,30 +9,24 @@ from bot.keyboards import inline, command from database.crud import CRUD from database.models import User -from .menu import menu_callback_handler +from .menu import menu_handler logger = logging.getLogger(__name__) -async def language_handler(message: types.Message) -> None: +async def language_handler( + event: Union[types.Message, types.CallbackQuery] +) -> None: """Language handler.""" try: - await message.answer( - "🌎 Choose language:", - reply_markup=inline.language_keyboard - ) - except Exception as e: - logger.error("Failed to send message: %s", e) - + text = "🌎 Choose language:" + keyboard = inline.language_keyboard -async def language_callback_handler(callback: types.CallbackQuery) -> None: - """Language callback handler.""" - try: - await callback.message.edit_text( - "🌎 Choose language:", - reply_markup=inline.language_keyboard - ) + if isinstance(event, types.CallbackQuery): + await event.message.edit_text(text, reply_markup=keyboard) + else: + await event.answer(text, reply_markup=keyboard) except Exception as e: logger.error("Failed to send message: %s", e) @@ -50,18 +45,16 @@ async def language_set_handler( i18n.get_i18n().ctx_locale.set(language_code) await bot.set_my_commands(command.get_commands(gettext)) - await menu_callback_handler(callback) + await menu_handler(callback) except Exception as e: - logger.error("Failed to send message: %s", e) + logger.error("Failed to set language: %s", e) def register(router: Router) -> None: """Registers language handler with the router.""" router.message.register(language_handler, LanguageFilter()) router.message.register(language_handler, Command("language")) - router.callback_query.register( - language_callback_handler, F.data == "language" - ) + router.callback_query.register(language_handler, F.data == "language") router.callback_query.register( language_set_handler, F.data.startswith("language:set:") ) diff --git a/bot/handlers/menu.py b/bot/handlers/menu.py index ec6eef3..0992752 100644 --- a/bot/handlers/menu.py +++ b/bot/handlers/menu.py @@ -1,5 +1,6 @@ """Menu handler for the bot.""" import logging +from typing import Union from aiogram import types, Router, F from aiogram.filters import CommandStart, Command from aiogram.utils.i18n import gettext @@ -9,27 +10,24 @@ logger = logging.getLogger(__name__) -async def menu_handler(message: types.Message) -> None: +async def menu_handler( + event: Union[types.Message, types.CallbackQuery] +) -> None: """Menu handler.""" try: - await message.answer( - gettext("menu"), - reply_markup=inline.get_menu_keyboard(gettext) - ) - except Exception as e: - logger.error("Failed to send message: %s", e) - + text = gettext("menu") + keyboard = inline.get_menu_keyboard(gettext) -async def menu_callback_handler(callback: types.CallbackQuery) -> None: - """Menu callback handler.""" - await callback.message.edit_text( - gettext("menu"), - reply_markup=inline.get_menu_keyboard(gettext) - ) + if isinstance(event, types.CallbackQuery): + await event.message.edit_text(text, reply_markup=keyboard) + else: + await event.answer(text, reply_markup=keyboard) + except Exception as e: + logger.error("Failed to handle menu event: %s", e) def register(router: Router) -> None: """Registers start handler with the router.""" router.message.register(menu_handler, CommandStart()) router.message.register(menu_handler, Command("menu")) - router.callback_query.register(menu_callback_handler, F.data == "menu") + router.callback_query.register(menu_handler, F.data == "menu") diff --git a/bot/handlers/search.py b/bot/handlers/search.py index 2f17a86..2721990 100644 --- a/bot/handlers/search.py +++ b/bot/handlers/search.py @@ -30,7 +30,7 @@ async def update_search( async def search_handler(message: types.Message, user: User) -> None: """Handles the search.""" try: - keyword = message.text.replace(":", "").strip() + keyword = message.text search_message = await message.answer( gettext("searching").format(keyword=keyword) ) From 89ef06fab3e01392aef3be14c0434b6ab080993d Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Mon, 13 Jan 2025 06:02:49 +0300 Subject: [PATCH 5/9] Enhance localization and user language selection features - Updated README.md to clarify multi-language support with flag icons and improved formatting for deployment and database management sections. - Modified language handler to utilize gettext for language selection prompts, enhancing user experience based on language preferences. - Added new localization entries for language selection prompts in both English and Russian, ensuring consistent messaging across languages. --- README.md | 10 +++++----- bot/handlers/language.py | 10 ++++++++-- locales/en/LC_MESSAGES/messages.po | 4 ++++ locales/ru/LC_MESSAGES/messages.po | 4 ++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e25d5e7..7dd3f37 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ - 🎧 Высокое качество звука - 🌅 Аудио с обложкой - 📱 Интуитивный интерфейс -- 🌍 Поддержка нескольких языков (английский, русский) +- 🌍 Поддержка нескольких языков (🇬🇧 English, 🇷🇺 Русский) - 🌍 Автоопределение языка пользователя -- 🐳 Легкое развертывание через Docker -- 🛡️ Безопасное хранение данных в PostgreSQL -- 📊 Управление базой данных (Adminer) -- 📝 Лицензия: Apache License 2.0 +- 🐳 Легкое развертывание через **Docker** +- 🛡️ Безопасное хранение данных в **PostgreSQL** +- 📊 Управление базой данных через **Adminer** +- 📝 Лицензия: **Apache License 2.0** ## 🎥 Демо (Demo) diff --git a/bot/handlers/language.py b/bot/handlers/language.py index cf937f4..fe9522e 100644 --- a/bot/handlers/language.py +++ b/bot/handlers/language.py @@ -9,6 +9,7 @@ from bot.keyboards import inline, command from database.crud import CRUD from database.models import User +from locales import support_languages from .menu import menu_handler @@ -16,11 +17,16 @@ async def language_handler( - event: Union[types.Message, types.CallbackQuery] + event: Union[types.Message, types.CallbackQuery], + user: User, ) -> None: """Language handler.""" try: - text = "🌎 Choose language:" + text = ( + gettext("language_choose") + if support_languages.is_supported(user.language_code) + else "🌎 Choose language:" + ) keyboard = inline.language_keyboard if isinstance(event, types.CallbackQuery): diff --git a/locales/en/LC_MESSAGES/messages.po b/locales/en/LC_MESSAGES/messages.po index 3d2b51c..aa260cd 100644 --- a/locales/en/LC_MESSAGES/messages.po +++ b/locales/en/LC_MESSAGES/messages.po @@ -18,6 +18,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" + +msgid "language_choose" +msgstr "🌎 Choose language:" + msgid "language_command" msgstr "🌎 Change language" diff --git a/locales/ru/LC_MESSAGES/messages.po b/locales/ru/LC_MESSAGES/messages.po index 11aa628..7e10d1e 100644 --- a/locales/ru/LC_MESSAGES/messages.po +++ b/locales/ru/LC_MESSAGES/messages.po @@ -19,6 +19,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" + +msgid "language_choose" +msgstr "🌎 Выберите язык:" + msgid "language_command" msgstr "🌎 Изменить язык" From aa1868a73def7a4db9acef294f903321ec457f12 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Mon, 13 Jan 2025 07:21:43 +0300 Subject: [PATCH 6/9] Refactor database configuration and search handling for improved functionality - Changed the DBConfig class to use a property for constructing the database URL, enhancing encapsulation. - Updated the get_all_from_page_handler to clarify variable names and improve readability. - Added input validation in the search_handler to ensure search queries are trimmed and within a valid length. - Enhanced error handling in the CRUD class to rollback sessions on exceptions, improving database reliability. - Added new localization entries for search query errors in both English and Russian, improving user feedback. --- bot/handlers/get_track.py | 6 +++--- bot/handlers/search.py | 6 +++++- bot/middlewares/auth_middleware.py | 1 - configs.py | 12 ++++++++---- database/crud.py | 4 ++++ database/models/search_history.py | 5 ++--- database/models/user.py | 7 +++---- locales/en/LC_MESSAGES/messages.po | 3 +++ locales/ru/LC_MESSAGES/messages.po | 4 +++- service/core.py | 11 ++++++----- 10 files changed, 37 insertions(+), 22 deletions(-) diff --git a/bot/handlers/get_track.py b/bot/handlers/get_track.py index 7581fe1..b70b7cd 100644 --- a/bot/handlers/get_track.py +++ b/bot/handlers/get_track.py @@ -59,10 +59,10 @@ async def get_all_from_page_handler( """Get all tracks from page handler.""" try: _, _, search_id, start_indx, end_indx = callback.data.split(":") - tracks: list[Track] = await load_tracks_from_db(search_id) - tracks = tracks[int(start_indx):int(end_indx)] + all_tracks: list[Track] = await load_tracks_from_db(search_id) + page_tracks = all_tracks[int(start_indx):int(end_indx)] await callback.answer(gettext("track_sending")) - for track in tracks: + for track in page_tracks: await send_track(callback, bot, track) except Exception as e: diff --git a/bot/handlers/search.py b/bot/handlers/search.py index 2721990..e6fa900 100644 --- a/bot/handlers/search.py +++ b/bot/handlers/search.py @@ -30,7 +30,11 @@ async def update_search( async def search_handler(message: types.Message, user: User) -> None: """Handles the search.""" try: - keyword = message.text + keyword = message.text.strip() + if not keyword or len(keyword) > 100: + await message.answer(gettext("search_query_error")) + return + search_message = await message.answer( gettext("searching").format(keyword=keyword) ) diff --git a/bot/middlewares/auth_middleware.py b/bot/middlewares/auth_middleware.py index 0649498..723ac56 100644 --- a/bot/middlewares/auth_middleware.py +++ b/bot/middlewares/auth_middleware.py @@ -68,7 +68,6 @@ async def _get_or_create_user( db_user = await user_crud.create(**user_data) logger.info("User %s registered in the database.", user.id) - # Update user data if it exists else: user_data['updated_at'] = datetime.now() db_user = await user_crud.update(db_user, **user_data) diff --git a/configs.py b/configs.py index 8150909..4076d19 100644 --- a/configs.py +++ b/configs.py @@ -20,10 +20,14 @@ class DBConfig: user: str = os.getenv('POSTGRES_USER') password: str = os.getenv('POSTGRES_PASSWORD') db: str = os.getenv('POSTGRES_DB') - url: str = ( - "postgresql+asyncpg://" - f"{user}:{password}@{host}:{port}/{db}" - ) + + @property + def url(self) -> str: + """Construct and return the database URL using instance attributes.""" + return ( + "postgresql+asyncpg://" + f"{self.user}:{self.password}@{self.host}:{self.port}/{self.db}" + ) bot_config = BotConfig() diff --git a/database/crud.py b/database/crud.py index 5e1d66b..d35ab8f 100644 --- a/database/crud.py +++ b/database/crud.py @@ -33,6 +33,10 @@ async def get_session(self) -> AsyncGenerator[AsyncSession, None]: session = self.session_factory() try: yield session + except Exception as e: + await session.rollback() + logger.error("Failed to get session: %s", e) + raise finally: await session.close() diff --git a/database/models/search_history.py b/database/models/search_history.py index 07f3ecd..f96b2fa 100644 --- a/database/models/search_history.py +++ b/database/models/search_history.py @@ -1,8 +1,7 @@ """Search history database model.""" -from datetime import datetime from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy import ( - Column, String, DateTime, Integer, BigInteger, ForeignKey + Column, String, DateTime, Integer, BigInteger, ForeignKey, func ) from ..engine import Base @@ -16,4 +15,4 @@ class SearchHistory(Base): user_id = Column(BigInteger, ForeignKey('users.id')) keyword = Column(String, default=None) tracks = Column(JSONB, default=[]) - created_at = Column(DateTime, default=datetime.now()) + created_at = Column(DateTime, default=func.now()) diff --git a/database/models/user.py b/database/models/user.py index bbd67d8..dd860c7 100644 --- a/database/models/user.py +++ b/database/models/user.py @@ -1,6 +1,5 @@ """User database model.""" -from datetime import datetime -from sqlalchemy import Column, String, BigInteger, DateTime, Integer +from sqlalchemy import Column, String, BigInteger, DateTime, Integer, func from ..engine import Base @@ -15,5 +14,5 @@ class User(Base): language_code = Column(String, default=None) state = Column(String, default=None) search_queries = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.now()) - updated_at = Column(DateTime, default=datetime.now()) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now()) diff --git a/locales/en/LC_MESSAGES/messages.po b/locales/en/LC_MESSAGES/messages.po index aa260cd..3eb275a 100644 --- a/locales/en/LC_MESSAGES/messages.po +++ b/locales/en/LC_MESSAGES/messages.po @@ -68,6 +68,9 @@ msgstr "🔍 Searching: {keyword}" msgid "search_result" msgstr "🎧 {keyword}:" +msgid "search_query_error" +msgstr "❌ Incorrect search query." + msgid "track_sending" msgstr "🎧 Sending track..." diff --git a/locales/ru/LC_MESSAGES/messages.po b/locales/ru/LC_MESSAGES/messages.po index 7e10d1e..ffc6ffa 100644 --- a/locales/ru/LC_MESSAGES/messages.po +++ b/locales/ru/LC_MESSAGES/messages.po @@ -72,6 +72,9 @@ msgstr "🎧 {keyword}" msgid "track_sending" msgstr "🎧 Отправка трека..." +msgid "search_query_error" +msgstr "❌ Некорректный поисковый запрос." + msgid "send_track_error" msgstr "❌ Ой! Что-то пошло не так. Аудиофайл не доступен." @@ -80,4 +83,3 @@ msgstr "🎵 Найди больше музы msgid "back_button" msgstr "⬅️ Назад" - diff --git a/service/core.py b/service/core.py index ebf0e07..44e7f04 100644 --- a/service/core.py +++ b/service/core.py @@ -1,5 +1,6 @@ """Music service core module for downloading and searching music.""" import logging +import urllib.parse from typing import Optional import aiohttp @@ -48,19 +49,19 @@ async def search(self, keyword: str) -> list[Track]: if not self._session: await self.connect() - url = f"{self.BASE_URL}/search/{keyword}" + url = urllib.parse.urljoin(self.BASE_URL, f"search/{keyword}") logger.info("Searching music with keyword: %s", keyword) return await self._parse_tracks(url, is_search=True) async def get_top_hits(self) -> list[Track]: """Get top tracks.""" - url = f"{self.BASE_URL}/besthit" + url = urllib.parse.urljoin(self.BASE_URL, "besthit") return await self._parse_tracks(url) async def get_new_hits(self) -> list[Track]: """Get new hits.""" - url = f"{self.BASE_URL}/newhit" + url = urllib.parse.urljoin(self.BASE_URL, "newhit") return await self._parse_tracks(url) async def _parse_tracks( @@ -112,12 +113,12 @@ async def get_audio_bytes(self, track: Track) -> bytes: """Download music file.""" url = track.audio_url if url.startswith("/"): - url = f"{self.BASE_URL}{track.audio_url}" + url = urllib.parse.urljoin(self.BASE_URL, track.audio_url) return await self._download_data(url, "audio", track.name) async def get_thumbnail_bytes(self, track: Track) -> bytes: """Download thumbnail image.""" url = track.thumbnail_url if url.startswith("/"): - url = f"{self.BASE_URL}{track.thumbnail_url}" + url = urllib.parse.urljoin(self.BASE_URL, track.thumbnail_url) return await self._download_data(url, "thumbnail", track.name) From e1942d312c9c03a399173514b0d7d6aa883dfaf0 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Tue, 14 Jan 2025 03:39:17 +0300 Subject: [PATCH 7/9] Implement subscription management features in the bot - Added a new NotSubbedFilter to check if users are subscribed to required channels. - Created a subscription handler to prompt users for subscription if not already subscribed. - Introduced a SubscriptionRequired model to manage required subscriptions in the database. - Updated inline keyboard functionality to include subscription prompts. - Enhanced logging and error handling in subscription-related operations. - Added localization entries for subscription messages in English and Russian. --- bot/filters/__init__.py | 4 ++- bot/filters/not_subbed.py | 51 ++++++++++++++++++++++++++++++ bot/handlers/__init__.py | 4 ++- bot/handlers/subscribe.py | 46 +++++++++++++++++++++++++++ bot/keyboards/inline.py | 24 ++++++++++++++ database/crud.py | 6 ++++ database/models/__init__.py | 3 +- database/models/sub_required.py | 15 +++++++++ locales/en/LC_MESSAGES/messages.po | 6 ++++ locales/ru/LC_MESSAGES/messages.po | 6 ++++ main.py | 4 ++- 11 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 bot/filters/not_subbed.py create mode 100644 bot/handlers/subscribe.py create mode 100644 database/models/sub_required.py diff --git a/bot/filters/__init__.py b/bot/filters/__init__.py index 8518d43..9a7b82c 100644 --- a/bot/filters/__init__.py +++ b/bot/filters/__init__.py @@ -1,4 +1,6 @@ """Filters for the bot.""" from .language import LanguageFilter +from .not_subbed import NotSubbedFilter -__all__ = ['LanguageFilter'] + +__all__ = ['LanguageFilter', 'NotSubbedFilter'] diff --git a/bot/filters/not_subbed.py b/bot/filters/not_subbed.py new file mode 100644 index 0000000..dff3b26 --- /dev/null +++ b/bot/filters/not_subbed.py @@ -0,0 +1,51 @@ +"""Check if user is subbed to the channel.""" + +from aiogram import types +from aiogram.filters import Filter +from aiogram import Bot +from aiogram.enums import ChatMemberStatus +from database.models import SubscriptionRequired, User +from database.crud import CRUD + + +class NotSubbedFilter(Filter): + """ + A filter that checks if a user is subbed to the channel. + """ + + ALLOWED_STATUSES = { + ChatMemberStatus.MEMBER, + ChatMemberStatus.ADMINISTRATOR, + ChatMemberStatus.CREATOR, + } + + async def __call__( + self, update: types.Message | types.CallbackQuery, user: User, bot: Bot + ) -> bool: + """ + Check if the user is not subscribed to any required channels. + """ + chats = await CRUD(SubscriptionRequired).get_all() + for chat in chats: + if await self._check_subscription(chat, user, bot): + return True + return False + + async def _check_subscription( + self, sub: SubscriptionRequired, user: User, bot: Bot + ) -> bool: + """ + Check if the user is subscribed to the channel. + Returns True if user is NOT subscribed. + Returns False if user is subscribed or if channel is not accessible. + """ + try: + chat = await bot.get_chat(sub.chat_id) + except Exception: + return False + + try: + member = await chat.get_member(user.id) + return member.status not in self.ALLOWED_STATUSES + except Exception: + return True diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 67808cc..82d0393 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -6,7 +6,8 @@ menu, faq, search, - pages + pages, + subscribe ) @@ -18,6 +19,7 @@ def setup(dp: Dispatcher) -> None: language.register(router) menu.register(router) faq.register(router) + subscribe.register(router) search.register(router) pages.register(router) get_track.register(router) diff --git a/bot/handlers/subscribe.py b/bot/handlers/subscribe.py new file mode 100644 index 0000000..448b3b3 --- /dev/null +++ b/bot/handlers/subscribe.py @@ -0,0 +1,46 @@ +"""Subscription required handler for the bot.""" +import logging +from aiogram import types, Router, F, Bot +from aiogram.utils.i18n import gettext +from bot.keyboards import inline +from bot.filters import NotSubbedFilter +from database.models import SubscriptionRequired, User +from database.crud import CRUD + +logger = logging.getLogger(__name__) + + +async def sub_required_handler( + event: types.Message | types.CallbackQuery, +) -> None: + """Subscription required handler.""" + try: + required_chats = await CRUD(SubscriptionRequired).get_all() + text = gettext("not_subscribed") + keyboard = inline.get_subscribe_keyboard(gettext, required_chats) + + if isinstance(event, types.Message): + await event.answer(text, reply_markup=keyboard) + else: + await event.message.answer(text, reply_markup=keyboard) + except Exception as e: + logger.error("Failed to send message: %s", e) + + +async def sub_check_handler( + callback: types.CallbackQuery, user: User, bot: Bot, +) -> None: + """Subscription check handler.""" + sub_check = NotSubbedFilter() + + if await sub_check(callback, user, bot): + await callback.message.answer(gettext("not_subscribed")) + else: + await callback.message.delete() + + +def register(router: Router) -> None: + """Registers FAQ handler with the router.""" + router.callback_query.register(sub_check_handler, F.data == "sub_check") + router.callback_query.register(sub_required_handler, NotSubbedFilter()) + router.message.register(sub_required_handler, NotSubbedFilter()) diff --git a/bot/keyboards/inline.py b/bot/keyboards/inline.py index ccc9917..ceed154 100644 --- a/bot/keyboards/inline.py +++ b/bot/keyboards/inline.py @@ -4,6 +4,7 @@ from service.data import Track from locales import support_languages +from database.models import SubscriptionRequired def get_keyboard_of_tracks( @@ -102,6 +103,29 @@ def get_menu_keyboard(gettext: Callable[[str], str]) -> InlineKeyboardMarkup: ) +def get_subscribe_keyboard( + gettext: Callable[[str], str], + sub_required: list[SubscriptionRequired], +) -> InlineKeyboardMarkup: + """Get subscribe keyboard.""" + chats = [ + [ + InlineKeyboardButton( + text=f"➕ {sub.chat_title}", url=sub.chat_link + ) + ] + for sub in sub_required + ] + chats.append([ + InlineKeyboardButton( + text=gettext("sub_check_button"), callback_data="sub_check" + ) + ]) + return InlineKeyboardMarkup( + inline_keyboard=chats + ) + + def get_back_keyboard( gettext: Callable[[str], str], callback_data: str ) -> InlineKeyboardMarkup: diff --git a/database/crud.py b/database/crud.py index d35ab8f..e01eb4b 100644 --- a/database/crud.py +++ b/database/crud.py @@ -65,6 +65,12 @@ async def get(self, **kwargs) -> T: instance = query.scalar_one_or_none() return instance + async def get_all(self) -> list[T]: + """Get all records.""" + async with self.get_session() as session: + query = await session.execute(select(self.model)) + return query.scalars().all() + async def update(self, instance: T, **kwargs) -> T: """Update a record's information.""" async with self.get_session() as session: diff --git a/database/models/__init__.py b/database/models/__init__.py index b1f971b..e5d530f 100644 --- a/database/models/__init__.py +++ b/database/models/__init__.py @@ -1,5 +1,6 @@ """Database models.""" from .user import User from .search_history import SearchHistory +from .sub_required import SubscriptionRequired -__all__ = ['User', 'SearchHistory'] +__all__ = ['User', 'SearchHistory', 'SubscriptionRequired'] diff --git a/database/models/sub_required.py b/database/models/sub_required.py new file mode 100644 index 0000000..508dda2 --- /dev/null +++ b/database/models/sub_required.py @@ -0,0 +1,15 @@ +"""Subscription required database model.""" +from sqlalchemy import Column, String, DateTime, Integer, BigInteger, func +from ..engine import Base + + +class SubscriptionRequired(Base): + """Subscription required model.""" + + __tablename__ = "subscription_required" + + id = Column(Integer, primary_key=True, autoincrement=True) + chat_id = Column(BigInteger) + chat_title = Column(String) + chat_link = Column(String) + created_at = Column(DateTime, default=func.now()) diff --git a/locales/en/LC_MESSAGES/messages.po b/locales/en/LC_MESSAGES/messages.po index 3eb275a..4811466 100644 --- a/locales/en/LC_MESSAGES/messages.po +++ b/locales/en/LC_MESSAGES/messages.po @@ -82,3 +82,9 @@ msgstr "🎵 Discover More Music" msgid "back_button" msgstr "⬅️ Back" + +msgid "not_subscribed" +msgstr "❗️ You are not subscribed to the required channels." + +msgid "sub_check_button" +msgstr "✅ I'm subscribed" diff --git a/locales/ru/LC_MESSAGES/messages.po b/locales/ru/LC_MESSAGES/messages.po index ffc6ffa..725058f 100644 --- a/locales/ru/LC_MESSAGES/messages.po +++ b/locales/ru/LC_MESSAGES/messages.po @@ -83,3 +83,9 @@ msgstr "🎵 Найди больше музы msgid "back_button" msgstr "⬅️ Назад" + +msgid "not_subscribed" +msgstr "❗️ Вы не подписаны на каналы, необходимые для использования бота." + +msgid "sub_check_button" +msgstr "✅ Я подписан" diff --git a/main.py b/main.py index aa43231..c9c9ddf 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,9 @@ from database.engine import init_db from configs import bot_config -logging.basicConfig(level=logging.INFO) +logging.basicConfig( + level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' +) logger = logging.getLogger(__name__) From 4d62de12b79d2885093a5ec3f53d58285f885189 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Tue, 14 Jan 2025 04:32:06 +0300 Subject: [PATCH 8/9] Enhance configuration management and localization support - Added timezone configuration in `configs.py` and updated Docker setup to utilize the TIMEZONE environment variable. - Refactored `NotSubbedFilter` to clarify subscription checks and updated localization messages for user prompts in English and Russian. - Improved database session handling with new timeout and recycle settings in `engine.py`. - Updated `auth_middleware.py` to use SQLAlchemy's `func.now()` for timestamp updates. - Introduced a maximum file size limit for downloads in the `Music` service, enhancing error handling for large files. --- Dockerfile | 5 +++-- bot/filters/not_subbed.py | 10 ++++++++-- bot/middlewares/auth_middleware.py | 4 ++-- configs.py | 3 ++- database/engine.py | 4 +++- docker-compose.yml | 1 + locales/en/LC_MESSAGES/messages.po | 2 +- locales/ru/LC_MESSAGES/messages.po | 2 +- main.py | 1 - service/core.py | 15 ++++++++++++--- 10 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index ede7b4a..97a8b13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,9 @@ RUN addgroup -S appgroup && adduser -S appuser -G appgroup ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=1 - + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + WORKDIR /app COPY requirements.txt . diff --git a/bot/filters/not_subbed.py b/bot/filters/not_subbed.py index dff3b26..17a9be1 100644 --- a/bot/filters/not_subbed.py +++ b/bot/filters/not_subbed.py @@ -24,14 +24,20 @@ async def __call__( ) -> bool: """ Check if the user is not subscribed to any required channels. + Returns True if user is NOT subscribed to ANY required channel. + Returns False if user is subscribed to ALL required channels. """ chats = await CRUD(SubscriptionRequired).get_all() + + if not chats: + return False + for chat in chats: - if await self._check_subscription(chat, user, bot): + if await self._not_subscribe(chat, user, bot): return True return False - async def _check_subscription( + async def _not_subscribe( self, sub: SubscriptionRequired, user: User, bot: Bot ) -> bool: """ diff --git a/bot/middlewares/auth_middleware.py b/bot/middlewares/auth_middleware.py index 723ac56..d271cfb 100644 --- a/bot/middlewares/auth_middleware.py +++ b/bot/middlewares/auth_middleware.py @@ -1,10 +1,10 @@ """Auth middleware module for the bot.""" import logging -from datetime import datetime from typing import Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware from aiogram.types import Update +from sqlalchemy import func from database.crud import CRUD from database.models import User @@ -69,7 +69,7 @@ async def _get_or_create_user( logger.info("User %s registered in the database.", user.id) else: - user_data['updated_at'] = datetime.now() + user_data['updated_at'] = func.now() db_user = await user_crud.update(db_user, **user_data) logger.info("User %s updated in the database.", user.id) diff --git a/configs.py b/configs.py index 4076d19..78ae17c 100644 --- a/configs.py +++ b/configs.py @@ -1,9 +1,10 @@ """Configurations for the app.""" import os +import time from dataclasses import dataclass - os.environ['TZ'] = os.getenv('TIMEZONE', 'UTC') +time.tzset() @dataclass diff --git a/database/engine.py b/database/engine.py index 4497a48..28cada1 100644 --- a/database/engine.py +++ b/database/engine.py @@ -15,7 +15,9 @@ bind=engine, class_=AsyncSession, expire_on_commit=False, - autoflush=False + autoflush=False, + pool_timeout=30, + pool_recycle=1800 ) diff --git a/docker-compose.yml b/docker-compose.yml index 537009d..af863e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} + TZ: ${TIMEZONE} volumes: - telegram-music-bot-db:/var/lib/postgresql/data diff --git a/locales/en/LC_MESSAGES/messages.po b/locales/en/LC_MESSAGES/messages.po index 4811466..7859678 100644 --- a/locales/en/LC_MESSAGES/messages.po +++ b/locales/en/LC_MESSAGES/messages.po @@ -84,7 +84,7 @@ msgid "back_button" msgstr "⬅️ Back" msgid "not_subscribed" -msgstr "❗️ You are not subscribed to the required channels." +msgstr "❗️ Please subscribe to the channels for using the bot." msgid "sub_check_button" msgstr "✅ I'm subscribed" diff --git a/locales/ru/LC_MESSAGES/messages.po b/locales/ru/LC_MESSAGES/messages.po index 725058f..7de5e46 100644 --- a/locales/ru/LC_MESSAGES/messages.po +++ b/locales/ru/LC_MESSAGES/messages.po @@ -85,7 +85,7 @@ msgid "back_button" msgstr "⬅️ Назад" msgid "not_subscribed" -msgstr "❗️ Вы не подписаны на каналы, необходимые для использования бота." +msgstr "❗️ Для использования бота, пожалуйста, подпишитесь на каналы." msgid "sub_check_button" msgstr "✅ Я подписан" diff --git a/main.py b/main.py index c9c9ddf..7076091 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ - """Entry point of the bot application.""" import logging import asyncio diff --git a/service/core.py b/service/core.py index 44e7f04..ece45ae 100644 --- a/service/core.py +++ b/service/core.py @@ -92,6 +92,8 @@ async def _download_data( self, url: str, resource_type: str, track_name: str ) -> bytes: """Generic method for downloading data.""" + MAX_SIZE = 50 * 1024 * 1024 # 50MB + if not self._session: await self.connect() @@ -103,11 +105,18 @@ async def _download_data( timeout=self._config.timeout ) as response: response.raise_for_status() - return await response.read() + content_length = response.content_length - except (aiohttp.ClientError, TimeoutError) as e: + if content_length and content_length > MAX_SIZE: + raise MusicServiceError( + f"File too large: {content_length} bytes" + ) + + return await response.read() + except Exception as e: raise MusicServiceError( - f"Failed to download {resource_type}: {str(e)}") from e + f"Failed to download {resource_type}: {str(e)}" + ) from e async def get_audio_bytes(self, track: Track) -> bytes: """Download music file.""" From 8247424f8a777bcad2353d4df8cbcbc69c6e8a4a Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Tue, 14 Jan 2025 05:00:36 +0300 Subject: [PATCH 9/9] Refactor subscription management and improve logging - Updated the subscription management system by renaming the SubscriptionRequired model to RequiredSubscriptions for clarity. - Enhanced the NotSubbedFilter and subscription handler to utilize the new model, ensuring accurate subscription checks. - Improved logging in the NotSubbedFilter to capture errors when fetching chat information. - Refined logging configuration in main.py for better message formatting. - Updated localization messages for subscription prompts in both English and Russian to enhance user experience. --- bot/filters/not_subbed.py | 13 ++++++++----- bot/handlers/subscribe.py | 4 ++-- bot/keyboards/inline.py | 4 ++-- database/engine.py | 2 -- database/models/__init__.py | 4 ++-- .../models/{sub_required.py => required_subs.py} | 8 ++++---- locales/en/LC_MESSAGES/messages.po | 2 +- locales/ru/LC_MESSAGES/messages.po | 2 +- main.py | 7 +++---- 9 files changed, 23 insertions(+), 23 deletions(-) rename database/models/{sub_required.py => required_subs.py} (65%) diff --git a/bot/filters/not_subbed.py b/bot/filters/not_subbed.py index 17a9be1..bd9d70d 100644 --- a/bot/filters/not_subbed.py +++ b/bot/filters/not_subbed.py @@ -1,12 +1,14 @@ """Check if user is subbed to the channel.""" - +import logging from aiogram import types from aiogram.filters import Filter from aiogram import Bot from aiogram.enums import ChatMemberStatus -from database.models import SubscriptionRequired, User +from database.models import RequiredSubscriptions, User from database.crud import CRUD +logger = logging.getLogger(__name__) + class NotSubbedFilter(Filter): """ @@ -27,7 +29,7 @@ async def __call__( Returns True if user is NOT subscribed to ANY required channel. Returns False if user is subscribed to ALL required channels. """ - chats = await CRUD(SubscriptionRequired).get_all() + chats = await CRUD(RequiredSubscriptions).get_all() if not chats: return False @@ -38,7 +40,7 @@ async def __call__( return False async def _not_subscribe( - self, sub: SubscriptionRequired, user: User, bot: Bot + self, sub: RequiredSubscriptions, user: User, bot: Bot ) -> bool: """ Check if the user is subscribed to the channel. @@ -47,7 +49,8 @@ async def _not_subscribe( """ try: chat = await bot.get_chat(sub.chat_id) - except Exception: + except Exception as e: + logger.error(f"Failed to get chat {sub.chat_id}: {e}") return False try: diff --git a/bot/handlers/subscribe.py b/bot/handlers/subscribe.py index 448b3b3..1e00c78 100644 --- a/bot/handlers/subscribe.py +++ b/bot/handlers/subscribe.py @@ -4,7 +4,7 @@ from aiogram.utils.i18n import gettext from bot.keyboards import inline from bot.filters import NotSubbedFilter -from database.models import SubscriptionRequired, User +from database.models import RequiredSubscriptions, User from database.crud import CRUD logger = logging.getLogger(__name__) @@ -15,7 +15,7 @@ async def sub_required_handler( ) -> None: """Subscription required handler.""" try: - required_chats = await CRUD(SubscriptionRequired).get_all() + required_chats = await CRUD(RequiredSubscriptions).get_all() text = gettext("not_subscribed") keyboard = inline.get_subscribe_keyboard(gettext, required_chats) diff --git a/bot/keyboards/inline.py b/bot/keyboards/inline.py index ceed154..fbc05e7 100644 --- a/bot/keyboards/inline.py +++ b/bot/keyboards/inline.py @@ -4,7 +4,7 @@ from service.data import Track from locales import support_languages -from database.models import SubscriptionRequired +from database.models import RequiredSubscriptions def get_keyboard_of_tracks( @@ -105,7 +105,7 @@ def get_menu_keyboard(gettext: Callable[[str], str]) -> InlineKeyboardMarkup: def get_subscribe_keyboard( gettext: Callable[[str], str], - sub_required: list[SubscriptionRequired], + sub_required: list[RequiredSubscriptions], ) -> InlineKeyboardMarkup: """Get subscribe keyboard.""" chats = [ diff --git a/database/engine.py b/database/engine.py index 28cada1..ef63bcd 100644 --- a/database/engine.py +++ b/database/engine.py @@ -16,8 +16,6 @@ class_=AsyncSession, expire_on_commit=False, autoflush=False, - pool_timeout=30, - pool_recycle=1800 ) diff --git a/database/models/__init__.py b/database/models/__init__.py index e5d530f..6e9562f 100644 --- a/database/models/__init__.py +++ b/database/models/__init__.py @@ -1,6 +1,6 @@ """Database models.""" from .user import User from .search_history import SearchHistory -from .sub_required import SubscriptionRequired +from .required_subs import RequiredSubscriptions -__all__ = ['User', 'SearchHistory', 'SubscriptionRequired'] +__all__ = ['User', 'SearchHistory', 'RequiredSubscriptions'] diff --git a/database/models/sub_required.py b/database/models/required_subs.py similarity index 65% rename from database/models/sub_required.py rename to database/models/required_subs.py index 508dda2..483b371 100644 --- a/database/models/sub_required.py +++ b/database/models/required_subs.py @@ -1,12 +1,12 @@ -"""Subscription required database model.""" +"""Required subscriptions database model.""" from sqlalchemy import Column, String, DateTime, Integer, BigInteger, func from ..engine import Base -class SubscriptionRequired(Base): - """Subscription required model.""" +class RequiredSubscriptions(Base): + """Required subscriptions model.""" - __tablename__ = "subscription_required" + __tablename__ = "required_subscriptions" id = Column(Integer, primary_key=True, autoincrement=True) chat_id = Column(BigInteger) diff --git a/locales/en/LC_MESSAGES/messages.po b/locales/en/LC_MESSAGES/messages.po index 7859678..1b22ecf 100644 --- a/locales/en/LC_MESSAGES/messages.po +++ b/locales/en/LC_MESSAGES/messages.po @@ -84,7 +84,7 @@ msgid "back_button" msgstr "⬅️ Back" msgid "not_subscribed" -msgstr "❗️ Please subscribe to the channels for using the bot." +msgstr "❗️ To use the bot, you need to subscribe to the channels." msgid "sub_check_button" msgstr "✅ I'm subscribed" diff --git a/locales/ru/LC_MESSAGES/messages.po b/locales/ru/LC_MESSAGES/messages.po index 7de5e46..8aa3ce2 100644 --- a/locales/ru/LC_MESSAGES/messages.po +++ b/locales/ru/LC_MESSAGES/messages.po @@ -85,7 +85,7 @@ msgid "back_button" msgstr "⬅️ Назад" msgid "not_subscribed" -msgstr "❗️ Для использования бота, пожалуйста, подпишитесь на каналы." +msgstr "❗️ Для использования бота необходимо подписаться на каналы." msgid "sub_check_button" msgstr "✅ Я подписан" diff --git a/main.py b/main.py index 7076091..f71e921 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,8 @@ from configs import bot_config logging.basicConfig( - level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' + level=logging.INFO, + format='%(asctime)s - %(levelname)s:%(name)s - %(message)s' ) logger = logging.getLogger(__name__) @@ -37,9 +38,7 @@ async def create_bot() -> Bot: async def main() -> None: - """ - The entry point of the bot application. - """ + """The entry point of the bot application.""" bot = await create_bot() storage = MemoryStorage()