diff --git a/app/api/routes.py b/app/api/routes.py
index 76688cb..282a0b4 100644
--- a/app/api/routes.py
+++ b/app/api/routes.py
@@ -44,7 +44,7 @@ def get_router() -> APIRouter:
chat_id, display_name = await svc.auth.handle_callback(code=code, state=state)
runner = request.app.state.runtime.telegram_runner
if runner is not None:
- await runner.send_message(chat_id, f"Spotify подключен: {display_name}\nТеперь можно /generate")
+ await runner.send_spotify_connected_notice(chat_id, display_name)
return """
Spotify connected
diff --git a/app/bot/telegram_bot.py b/app/bot/telegram_bot.py
index 2e93536..bc85cd5 100644
--- a/app/bot/telegram_bot.py
+++ b/app/bot/telegram_bot.py
@@ -1,13 +1,172 @@
from __future__ import annotations
import contextlib
+from collections.abc import Awaitable, Callable
-from telegram import Update
-from telegram.ext import Application, CommandHandler, ContextTypes
+from telegram import ReplyKeyboardMarkup, Update
+from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
from app.db.repositories import PlaylistRunRepository, SavedTrackRepository, UserRepository
from app.services.app_services import AppServices
+Lang = str
+
+
+I18N: dict[str, dict[str, str]] = {
+ "ru": {
+ "button_connect": "🔗 Подключить Spotify",
+ "button_generate": "✨ Сгенерировать",
+ "button_status": "📊 Статус",
+ "button_latest": "🎵 Последний плейлист",
+ "button_sync": "🔄 Обновить лайки",
+ "button_help": "❓ Помощь",
+ "button_settings": "⚙️ Настройки",
+ "button_language": "🌐 Язык / Language",
+ "button_size_20": "📏 Размер 20",
+ "button_size_30": "📏 Размер 30",
+ "button_size_50": "📏 Размер 50",
+ "button_ratio_60": "🆕 Новые 60%",
+ "button_ratio_80": "🆕 Новые 80%",
+ "button_ratio_100": "🆕 Новые 100%",
+ "button_lang_ru": "🇺🇦 Русский",
+ "button_lang_en": "🇺🇸 English",
+ "button_back": "⬅️ Назад",
+ "start_text": (
+ "Я бот для Spotify daily vibe playlist.\n"
+ "Делаю плейлисты по вашему recent listening + liked tracks, стараюсь давать больше новых треков и меньше повторов.\n\n"
+ "/connect - привязать Spotify\n"
+ "/status - статус аккаунта и последнего плейлиста\n"
+ "/generate - сгенерировать плейлист сейчас\n"
+ "/latest - показать ссылку на последний плейлист\n"
+ "/setsize N - размер плейлиста\n"
+ "/setratio X - доля новых треков (0.5..1.0)\n"
+ "/sync - обновить лайкнутые треки из Spotify\n"
+ "/lang [ru|en] - сменить язык"
+ ),
+ "help_text": (
+ "Я бот для Spotify daily vibe playlist.\n"
+ "Делаю плейлисты по вашему recent listening + liked tracks, стараюсь давать больше новых треков и меньше повторов.\n\n"
+ "/connect - привязать Spotify\n"
+ "/status - статус аккаунта и последнего плейлиста\n"
+ "/generate - сгенерировать плейлист сейчас\n"
+ "/latest - показать ссылку на последний плейлист\n"
+ "/setsize N - размер плейлиста\n"
+ "/setratio X - доля новых треков (0.5..1.0)\n"
+ "/sync - обновить лайкнутые треки из Spotify\n"
+ "/lang [ru|en] - сменить язык"
+ ),
+ "connect_open_link": "Открой ссылку и авторизуй Spotify:\n{url}",
+ "spotify_connected_notice": "Spotify подключен: {display_name}\nТеперь можно /generate",
+ "user_not_found_start": "Пользователь не найден. Напиши /start",
+ "no_playlist_yet": "Пока нет сгенерированного плейлиста.",
+ "generate_wait": "Генерирую плейлист, это может занять 20-60 секунд...",
+ "error_prefix": "Ошибка: {message}",
+ "size_usage": "Использование: /setsize 30",
+ "size_invalid": "Размер должен быть числом от 5 до 100.",
+ "size_set": "Размер плейлиста установлен: {value}",
+ "ratio_usage": "Использование: /setratio 0.8",
+ "ratio_invalid": "Значение должно быть от 0.5 до 1.0",
+ "ratio_set": "Минимальная доля новых треков: {value:.2f}",
+ "sync_need_connect": "Сначала /connect",
+ "sync_done": "Лайкнутые треки обновлены: {count}",
+ "lang_choose": "Выбери язык интерфейса:",
+ "lang_set": "Язык интерфейса: {lang_name}",
+ "lang_invalid": "Поддерживаются только `ru` и `en`.",
+ "unknown_action": "Не понял команду. Используйте кнопки ниже или /help.",
+ "main_menu_hint": "Главное меню",
+ "settings_menu_hint": "Настройки",
+ "status_yes": "да",
+ "status_no": "нет",
+ "status_text": (
+ "Connected: {connected}\n"
+ "Spotify user: {spotify_user}\n"
+ "Liked tracks cached: {saved_count}\n"
+ "Playlist size: {playlist_size}\n"
+ "Min new ratio: {min_new_ratio:.2f}\n"
+ "Last generated: {last_generated}"
+ ),
+ "status_last_run": "Last run: {status}, tracks={tracks}",
+ "status_last_url": "{url}",
+ },
+ "en": {
+ "button_connect": "🔗 Connect Spotify",
+ "button_generate": "✨ Generate",
+ "button_status": "📊 Status",
+ "button_latest": "🎵 Latest Playlist",
+ "button_sync": "🔄 Sync Likes",
+ "button_help": "❓ Help",
+ "button_settings": "⚙️ Settings",
+ "button_language": "🌐 Language / Язык",
+ "button_size_20": "📏 Size 20",
+ "button_size_30": "📏 Size 30",
+ "button_size_50": "📏 Size 50",
+ "button_ratio_60": "🆕 New 60%",
+ "button_ratio_80": "🆕 New 80%",
+ "button_ratio_100": "🆕 New 100%",
+ "button_lang_ru": "🇺🇦 Русский",
+ "button_lang_en": "🇺🇸 English",
+ "button_back": "⬅️ Back",
+ "start_text": (
+ "I am a Spotify daily vibe playlist bot.\n"
+ "I build playlists from your recent listening + liked tracks and try to keep them fresh with fewer repeats.\n\n"
+ "/connect - connect Spotify\n"
+ "/status - account status and latest playlist\n"
+ "/generate - generate playlist now\n"
+ "/latest - show latest playlist link\n"
+ "/setsize N - playlist size\n"
+ "/setratio X - min new ratio (0.5..1.0)\n"
+ "/sync - sync liked tracks from Spotify\n"
+ "/lang [ru|en] - change language"
+ ),
+ "help_text": (
+ "I am a Spotify daily vibe playlist bot.\n"
+ "I build playlists from your recent listening + liked tracks and try to keep them fresh with fewer repeats.\n\n"
+ "/connect - connect Spotify\n"
+ "/status - account status and latest playlist\n"
+ "/generate - generate playlist now\n"
+ "/latest - show latest playlist link\n"
+ "/setsize N - playlist size\n"
+ "/setratio X - min new ratio (0.5..1.0)\n"
+ "/sync - sync liked tracks from Spotify\n"
+ "/lang [ru|en] - change language"
+ ),
+ "connect_open_link": "Open the link and authorize Spotify:\n{url}",
+ "spotify_connected_notice": "Spotify connected: {display_name}\nNow you can use /generate",
+ "user_not_found_start": "User not found. Send /start first",
+ "no_playlist_yet": "No generated playlist yet.",
+ "generate_wait": "Generating playlist, this can take 20-60 seconds...",
+ "error_prefix": "Error: {message}",
+ "size_usage": "Usage: /setsize 30",
+ "size_invalid": "Size must be a number from 5 to 100.",
+ "size_set": "Playlist size set to: {value}",
+ "ratio_usage": "Usage: /setratio 0.8",
+ "ratio_invalid": "Value must be between 0.5 and 1.0",
+ "ratio_set": "Minimum new ratio: {value:.2f}",
+ "sync_need_connect": "Use /connect first",
+ "sync_done": "Liked tracks synced: {count}",
+ "lang_choose": "Choose interface language:",
+ "lang_set": "Interface language: {lang_name}",
+ "lang_invalid": "Only `ru` and `en` are supported.",
+ "unknown_action": "I did not understand that. Use the buttons below or /help.",
+ "main_menu_hint": "Main menu",
+ "settings_menu_hint": "Settings",
+ "status_yes": "yes",
+ "status_no": "no",
+ "status_text": (
+ "Connected: {connected}\n"
+ "Spotify user: {spotify_user}\n"
+ "Liked tracks cached: {saved_count}\n"
+ "Playlist size: {playlist_size}\n"
+ "Min new ratio: {min_new_ratio:.2f}\n"
+ "Last generated: {last_generated}"
+ ),
+ "status_last_run": "Last run: {status}, tracks={tracks}",
+ "status_last_url": "{url}",
+ },
+}
+
+LANG_NAMES = {"ru": "Русский", "en": "English"}
+
class TelegramBotRunner:
def __init__(self, token: str, session_factory, services: AppServices, app_base_url: str) -> None:
@@ -16,8 +175,9 @@ class TelegramBotRunner:
self.services = services
self.app_base_url = app_base_url.rstrip("/")
self.application = Application.builder().token(token).build()
- self._setup_handlers()
self._running = False
+ self._chat_lang: dict[str, Lang] = {}
+ self._setup_handlers()
def _setup_handlers(self) -> None:
self.application.add_handler(CommandHandler("start", self.start))
@@ -29,6 +189,8 @@ class TelegramBotRunner:
self.application.add_handler(CommandHandler("setsize", self.set_size))
self.application.add_handler(CommandHandler("setratio", self.set_ratio))
self.application.add_handler(CommandHandler("sync", self.sync_likes))
+ self.application.add_handler(CommandHandler("lang", self.lang))
+ self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.on_text_button))
async def start_polling(self) -> None:
await self.application.initialize()
@@ -53,6 +215,10 @@ class TelegramBotRunner:
async def send_message(self, chat_id: str, text: str) -> None:
await self.application.bot.send_message(chat_id=int(chat_id), text=text, disable_web_page_preview=False)
+ async def send_spotify_connected_notice(self, chat_id: str, display_name: str) -> None:
+ lang = self._chat_lang.get(str(chat_id), "ru")
+ await self.send_message(chat_id, self._t(lang, "spotify_connected_notice", display_name=display_name))
+
async def _ensure_user(self, update: Update):
chat = update.effective_chat
user = update.effective_user
@@ -67,33 +233,192 @@ class TelegramBotRunner:
await session.commit()
return db_user
- async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ def _t(self, lang: Lang, key: str, **kwargs) -> str:
+ template = I18N.get(lang, I18N["en"]).get(key, I18N["en"].get(key, key))
+ return template.format(**kwargs)
+
+ def _detect_lang(self, update: Update) -> Lang:
+ chat = update.effective_chat
+ if chat:
+ cached = self._chat_lang.get(str(chat.id))
+ if cached in {"ru", "en"}:
+ return cached
+ user = update.effective_user
+ tg_lang = (getattr(user, "language_code", None) or "").lower()
+ lang = "ru" if tg_lang.startswith("ru") else "en"
+ if chat:
+ self._chat_lang[str(chat.id)] = lang
+ return lang
+
+ def _set_chat_lang(self, chat_id: str, lang: Lang) -> None:
+ if lang in {"ru", "en"}:
+ self._chat_lang[str(chat_id)] = lang
+
+ async def _reply(
+ self,
+ update: Update,
+ text: str,
+ *,
+ lang: Lang | None = None,
+ disable_web_page_preview: bool = False,
+ reply_markup=None,
+ ) -> None:
+ msg = update.effective_message
+ if msg is None:
+ return
+ if reply_markup is None:
+ lang = lang or self._detect_lang(update)
+ reply_markup = self._main_menu(lang)
+ await msg.reply_text(text, disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup)
+
+ def _main_menu(self, lang: Lang) -> ReplyKeyboardMarkup:
+ kb = [
+ [self._t(lang, "button_connect"), self._t(lang, "button_generate")],
+ [self._t(lang, "button_status"), self._t(lang, "button_sync")],
+ [self._t(lang, "button_help"), self._t(lang, "button_settings")],
+ ]
+ return ReplyKeyboardMarkup(kb, resize_keyboard=True, is_persistent=True)
+
+ def _settings_menu(self, lang: Lang) -> ReplyKeyboardMarkup:
+ kb = [
+ [
+ self._t(lang, "button_size_20"),
+ self._t(lang, "button_size_30"),
+ self._t(lang, "button_size_50"),
+ ],
+ [
+ self._t(lang, "button_ratio_60"),
+ self._t(lang, "button_ratio_80"),
+ self._t(lang, "button_ratio_100"),
+ ],
+ [self._t(lang, "button_language")],
+ [self._t(lang, "button_back")],
+ ]
+ return ReplyKeyboardMarkup(kb, resize_keyboard=True, is_persistent=True)
+
+ def _language_menu(self, lang: Lang) -> ReplyKeyboardMarkup:
+ kb = [
+ [self._t(lang, "button_lang_ru"), self._t(lang, "button_lang_en")],
+ [self._t(lang, "button_back")],
+ ]
+ return ReplyKeyboardMarkup(kb, resize_keyboard=True, is_persistent=True)
+
+ def _button_labels(self, key: str) -> set[str]:
+ return {I18N["ru"][key], I18N["en"][key]}
+
+ async def on_text_button(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ text = (update.effective_message.text if update.effective_message else "") or ""
+ text = text.strip()
+ lang = self._detect_lang(update)
+
+ action_handlers: list[tuple[str, Callable[[Update, ContextTypes.DEFAULT_TYPE], Awaitable[None]]]] = [
+ ("button_connect", self.connect),
+ ("button_generate", self.generate),
+ ("button_status", self.status),
+ ("button_sync", self.sync_likes),
+ ("button_help", self.help),
+ ("button_settings", self.open_settings),
+ ("button_language", self.lang),
+ ]
+ for key, handler in action_handlers:
+ if text in self._button_labels(key):
+ await handler(update, context)
+ return
+
+ if text in self._button_labels("button_lang_ru"):
+ await self._set_language_from_ui(update, "ru", reply_markup=self._settings_menu("ru"))
+ return
+ if text in self._button_labels("button_lang_en"):
+ await self._set_language_from_ui(update, "en", reply_markup=self._settings_menu("en"))
+ return
+ if text in self._button_labels("button_back"):
+ await self._reply(update, self._t(lang, "main_menu_hint"), lang=lang)
+ return
+
+ size_map = {
+ "button_size_20": 20,
+ "button_size_30": 30,
+ "button_size_50": 50,
+ }
+ for key, value in size_map.items():
+ if text in self._button_labels(key):
+ await self._apply_size(update, value, lang=lang, reply_markup=self._settings_menu(lang))
+ return
+
+ ratio_map = {
+ "button_ratio_60": 0.6,
+ "button_ratio_80": 0.8,
+ "button_ratio_100": 1.0,
+ }
+ for key, value in ratio_map.items():
+ if text in self._button_labels(key):
+ await self._apply_ratio(update, value, lang=lang, reply_markup=self._settings_menu(lang))
+ return
+
+ await self._reply(update, self._t(lang, "unknown_action"), lang=lang)
+
+ async def _set_language_from_ui(self, update: Update, lang: Lang, reply_markup=None) -> None:
+ chat = update.effective_chat
+ if not chat:
+ return
+ self._set_chat_lang(str(chat.id), lang)
await self._ensure_user(update)
- await update.message.reply_text(
- "Я бот для Spotify daily vibe playlist.\n"
- "Команды: /connect /status /generate /latest /setsize 30 /setratio 0.8 /sync"
+ await self._reply(
+ update,
+ self._t(lang, "lang_set", lang_name=LANG_NAMES[lang]),
+ lang=lang,
+ reply_markup=reply_markup or self._main_menu(lang),
)
+ async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ await self._ensure_user(update)
+ await self.help(update, context)
+
async def help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- await update.message.reply_text(
- "/connect - привязать Spotify\n"
- "/status - статус аккаунта и последнего плейлиста\n"
- "/generate - сгенерировать плейлист сейчас\n"
- "/latest - показать ссылку на последний плейлист\n"
- "/setsize N - размер плейлиста\n"
- "/setratio X - доля новых треков (0.5..1.0)\n"
- "/sync - обновить лайкнутые треки из Spotify"
+ lang = self._detect_lang(update)
+ await self._reply(update, self._t(lang, "start_text"), lang=lang)
+
+ async def open_settings(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
+ await self._reply(
+ update,
+ self._t(lang, "settings_menu_hint"),
+ lang=lang,
+ reply_markup=self._settings_menu(lang),
+ )
+
+ async def lang(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
+ args = list(getattr(context, "args", []) or [])
+ if args:
+ requested = args[0].strip().lower()
+ if requested.startswith("ru"):
+ await self._set_language_from_ui(update, "ru")
+ return
+ if requested.startswith("en"):
+ await self._set_language_from_ui(update, "en")
+ return
+ await self._reply(update, self._t(lang, "lang_invalid"), lang=lang)
+ return
+ await self._reply(
+ update,
+ self._t(lang, "lang_choose"),
+ lang=lang,
+ reply_markup=self._language_menu(lang),
)
async def connect(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
chat = update.effective_chat
user = update.effective_user
if not chat:
return
+ self._set_chat_lang(str(chat.id), lang)
url = await self.services.auth.create_connect_url(str(chat.id), user.username if user else None)
- await update.message.reply_text(f"Открой ссылку и авторизуй Spotify:\n{url}", disable_web_page_preview=True)
+ await self._reply(update, self._t(lang, "connect_open_link", url=url), lang=lang, disable_web_page_preview=True)
async def status(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
chat = update.effective_chat
if not chat:
return
@@ -103,26 +428,29 @@ class TelegramBotRunner:
saved = SavedTrackRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user:
- await update.message.reply_text("Пользователь не найден. Напиши /start")
+ await self._reply(update, self._t(lang, "user_not_found_start"), lang=lang)
return
latest = await runs.latest_for_user(db_user.id)
saved_count = await saved.count_for_user(db_user.id)
- connected = "yes" if db_user.spotify_refresh_token else "no"
- text = (
- f"Connected: {connected}\n"
- f"Spotify user: {db_user.spotify_user_id or '-'}\n"
- f"Liked tracks cached: {saved_count}\n"
- f"Playlist size: {db_user.playlist_size}\n"
- f"Min new ratio: {db_user.min_new_ratio:.2f}\n"
- f"Last generated: {db_user.last_generated_date or '-'}"
+ connected = self._t(lang, "status_yes") if db_user.spotify_refresh_token else self._t(lang, "status_no")
+ text = self._t(
+ lang,
+ "status_text",
+ connected=connected,
+ spotify_user=db_user.spotify_user_id or "-",
+ saved_count=saved_count,
+ playlist_size=db_user.playlist_size,
+ min_new_ratio=db_user.min_new_ratio,
+ last_generated=db_user.last_generated_date or "-",
)
if latest:
- text += f"\nLast run: {latest.status}, tracks={latest.total_tracks}"
+ text += "\n" + self._t(lang, "status_last_run", status=latest.status, tracks=latest.total_tracks)
if latest.playlist_url:
- text += f"\n{latest.playlist_url}"
- await update.message.reply_text(text, disable_web_page_preview=False)
+ text += "\n" + self._t(lang, "status_last_url", url=latest.playlist_url)
+ await self._reply(update, text, lang=lang, disable_web_page_preview=False)
async def latest(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
chat = update.effective_chat
if not chat:
return
@@ -130,11 +458,12 @@ class TelegramBotRunner:
users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user or not db_user.latest_playlist_url:
- await update.message.reply_text("Пока нет сгенерированного плейлиста.")
+ await self._reply(update, self._t(lang, "no_playlist_yet"), lang=lang)
return
- await update.message.reply_text(db_user.latest_playlist_url, disable_web_page_preview=False)
+ await self._reply(update, db_user.latest_playlist_url, lang=lang, disable_web_page_preview=False)
async def generate(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
chat = update.effective_chat
if not chat:
return
@@ -142,62 +471,79 @@ class TelegramBotRunner:
users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user:
- await update.message.reply_text("Пользователь не найден. Напиши /start")
+ await self._reply(update, self._t(lang, "user_not_found_start"), lang=lang)
return
user_id = db_user.id
- await update.message.reply_text("Генерирую плейлист, это может занять 20-60 секунд...")
+ await self._reply(update, self._t(lang, "generate_wait"), lang=lang)
outcome = await self.services.jobs.generate_for_user(user_id=user_id, force=True, notify=False)
if outcome.ok:
msg = outcome.message
if outcome.playlist_url:
msg += f"\n{outcome.playlist_url}"
- await update.message.reply_text(msg, disable_web_page_preview=False)
+ await self._reply(update, msg, lang=lang, disable_web_page_preview=False)
else:
- await update.message.reply_text(f"Ошибка: {outcome.message}")
+ await self._reply(update, self._t(lang, "error_prefix", message=outcome.message), lang=lang)
- async def set_size(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ async def _apply_size(self, update: Update, value: int, *, lang: Lang, reply_markup=None) -> None:
chat = update.effective_chat
if not chat:
return
+ if value < 5 or value > 100:
+ await self._reply(update, self._t(lang, "size_invalid"), lang=lang, reply_markup=reply_markup)
+ return
+ async with self.session_factory() as session:
+ users = UserRepository(session)
+ db_user = await users.get_or_create_by_chat(
+ str(chat.id),
+ update.effective_user.username if update.effective_user else None,
+ )
+ db_user.playlist_size = value
+ await session.commit()
+ await self._reply(update, self._t(lang, "size_set", value=value), lang=lang, reply_markup=reply_markup)
+
+ async def set_size(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
if not context.args:
- await update.message.reply_text("Использование: /setsize 30")
+ await self._reply(update, self._t(lang, "size_usage"), lang=lang)
return
try:
value = int(context.args[0])
- if value < 5 or value > 100:
- raise ValueError
except ValueError:
- await update.message.reply_text("Размер должен быть числом от 5 до 100.")
+ await self._reply(update, self._t(lang, "size_invalid"), lang=lang)
return
- async with self.session_factory() as session:
- users = UserRepository(session)
- db_user = await users.get_or_create_by_chat(str(chat.id), update.effective_user.username if update.effective_user else None)
- db_user.playlist_size = value
- await session.commit()
- await update.message.reply_text(f"Размер плейлиста установлен: {value}")
+ await self._apply_size(update, value, lang=lang)
- async def set_ratio(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ async def _apply_ratio(self, update: Update, value: float, *, lang: Lang, reply_markup=None) -> None:
chat = update.effective_chat
if not chat:
return
- if not context.args:
- await update.message.reply_text("Использование: /setratio 0.8")
- return
- try:
- value = float(context.args[0])
- if value < 0.5 or value > 1.0:
- raise ValueError
- except ValueError:
- await update.message.reply_text("Значение должно быть от 0.5 до 1.0")
+ if value < 0.5 or value > 1.0:
+ await self._reply(update, self._t(lang, "ratio_invalid"), lang=lang, reply_markup=reply_markup)
return
async with self.session_factory() as session:
users = UserRepository(session)
- db_user = await users.get_or_create_by_chat(str(chat.id), update.effective_user.username if update.effective_user else None)
+ db_user = await users.get_or_create_by_chat(
+ str(chat.id),
+ update.effective_user.username if update.effective_user else None,
+ )
db_user.min_new_ratio = value
await session.commit()
- await update.message.reply_text(f"Минимальная доля новых треков: {value:.2f}")
+ await self._reply(update, self._t(lang, "ratio_set", value=value), lang=lang, reply_markup=reply_markup)
+
+ async def set_ratio(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
+ if not context.args:
+ await self._reply(update, self._t(lang, "ratio_usage"), lang=lang)
+ return
+ try:
+ value = float(context.args[0])
+ except ValueError:
+ await self._reply(update, self._t(lang, "ratio_invalid"), lang=lang)
+ return
+ await self._apply_ratio(update, value, lang=lang)
async def sync_likes(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ lang = self._detect_lang(update)
chat = update.effective_chat
if not chat:
return
@@ -205,13 +551,13 @@ class TelegramBotRunner:
users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user:
- await update.message.reply_text("Пользователь не найден. Напиши /start")
+ await self._reply(update, self._t(lang, "user_not_found_start"), lang=lang)
return
if not db_user.spotify_refresh_token:
- await update.message.reply_text("Сначала /connect")
+ await self._reply(update, self._t(lang, "sync_need_connect"), lang=lang)
return
access_token = await self.services.auth.ensure_valid_access_token(session, db_user)
await self.services.recommendation.sync_saved_tracks(session, db_user, access_token)
await session.commit()
saved_count = await SavedTrackRepository(session).count_for_user(db_user.id)
- await update.message.reply_text(f"Лайкнутые треки обновлены: {saved_count}")
+ await self._reply(update, self._t(lang, "sync_done", count=saved_count), lang=lang)