Add translations

This commit is contained in:
heboba
2026-02-26 20:25:09 +00:00
parent 9ab125b1a6
commit 06add127ff
2 changed files with 407 additions and 61 deletions

View File

@@ -44,7 +44,7 @@ def get_router() -> APIRouter:
chat_id, display_name = await svc.auth.handle_callback(code=code, state=state) chat_id, display_name = await svc.auth.handle_callback(code=code, state=state)
runner = request.app.state.runtime.telegram_runner runner = request.app.state.runtime.telegram_runner
if runner is not None: 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 """ return """
<html><body style="font-family:sans-serif;padding:24px"> <html><body style="font-family:sans-serif;padding:24px">
<h2>Spotify connected</h2> <h2>Spotify connected</h2>

View File

@@ -1,13 +1,172 @@
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
from collections.abc import Awaitable, Callable
from telegram import Update from telegram import ReplyKeyboardMarkup, Update
from telegram.ext import Application, CommandHandler, ContextTypes from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
from app.db.repositories import PlaylistRunRepository, SavedTrackRepository, UserRepository from app.db.repositories import PlaylistRunRepository, SavedTrackRepository, UserRepository
from app.services.app_services import AppServices 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: class TelegramBotRunner:
def __init__(self, token: str, session_factory, services: AppServices, app_base_url: str) -> None: def __init__(self, token: str, session_factory, services: AppServices, app_base_url: str) -> None:
@@ -16,8 +175,9 @@ class TelegramBotRunner:
self.services = services self.services = services
self.app_base_url = app_base_url.rstrip("/") self.app_base_url = app_base_url.rstrip("/")
self.application = Application.builder().token(token).build() self.application = Application.builder().token(token).build()
self._setup_handlers()
self._running = False self._running = False
self._chat_lang: dict[str, Lang] = {}
self._setup_handlers()
def _setup_handlers(self) -> None: def _setup_handlers(self) -> None:
self.application.add_handler(CommandHandler("start", self.start)) 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("setsize", self.set_size))
self.application.add_handler(CommandHandler("setratio", self.set_ratio)) self.application.add_handler(CommandHandler("setratio", self.set_ratio))
self.application.add_handler(CommandHandler("sync", self.sync_likes)) 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: async def start_polling(self) -> None:
await self.application.initialize() await self.application.initialize()
@@ -53,6 +215,10 @@ class TelegramBotRunner:
async def send_message(self, chat_id: str, text: str) -> None: 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) 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): async def _ensure_user(self, update: Update):
chat = update.effective_chat chat = update.effective_chat
user = update.effective_user user = update.effective_user
@@ -67,33 +233,192 @@ class TelegramBotRunner:
await session.commit() await session.commit()
return db_user 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 self._ensure_user(update)
await update.message.reply_text( await self._reply(
"Я бот для Spotify daily vibe playlist.\n" update,
"Команды: /connect /status /generate /latest /setsize 30 /setratio 0.8 /sync" 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: async def help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text( lang = self._detect_lang(update)
"/connect - привязать Spotify\n" await self._reply(update, self._t(lang, "start_text"), lang=lang)
"/status - статус аккаунта и последнего плейлиста\n"
"/generate - сгенерировать плейлист сейчас\n" async def open_settings(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"/latest - показать ссылку на последний плейлист\n" lang = self._detect_lang(update)
"/setsize N - размер плейлиста\n" await self._reply(
"/setratio X - доля новых треков (0.5..1.0)\n" update,
"/sync - обновить лайкнутые треки из Spotify" 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: async def connect(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
lang = self._detect_lang(update)
chat = update.effective_chat chat = update.effective_chat
user = update.effective_user user = update.effective_user
if not chat: if not chat:
return 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) 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: async def status(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
lang = self._detect_lang(update)
chat = update.effective_chat chat = update.effective_chat
if not chat: if not chat:
return return
@@ -103,26 +428,29 @@ class TelegramBotRunner:
saved = SavedTrackRepository(session) saved = SavedTrackRepository(session)
db_user = await users.get_by_chat_id(str(chat.id)) db_user = await users.get_by_chat_id(str(chat.id))
if not db_user: if not db_user:
await update.message.reply_text("Пользователь не найден. Напиши /start") await self._reply(update, self._t(lang, "user_not_found_start"), lang=lang)
return return
latest = await runs.latest_for_user(db_user.id) latest = await runs.latest_for_user(db_user.id)
saved_count = await saved.count_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" connected = self._t(lang, "status_yes") if db_user.spotify_refresh_token else self._t(lang, "status_no")
text = ( text = self._t(
f"Connected: {connected}\n" lang,
f"Spotify user: {db_user.spotify_user_id or '-'}\n" "status_text",
f"Liked tracks cached: {saved_count}\n" connected=connected,
f"Playlist size: {db_user.playlist_size}\n" spotify_user=db_user.spotify_user_id or "-",
f"Min new ratio: {db_user.min_new_ratio:.2f}\n" saved_count=saved_count,
f"Last generated: {db_user.last_generated_date or '-'}" playlist_size=db_user.playlist_size,
min_new_ratio=db_user.min_new_ratio,
last_generated=db_user.last_generated_date or "-",
) )
if latest: 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: if latest.playlist_url:
text += f"\n{latest.playlist_url}" text += "\n" + self._t(lang, "status_last_url", url=latest.playlist_url)
await update.message.reply_text(text, disable_web_page_preview=False) await self._reply(update, text, lang=lang, disable_web_page_preview=False)
async def latest(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def latest(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
lang = self._detect_lang(update)
chat = update.effective_chat chat = update.effective_chat
if not chat: if not chat:
return return
@@ -130,11 +458,12 @@ class TelegramBotRunner:
users = UserRepository(session) users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id)) db_user = await users.get_by_chat_id(str(chat.id))
if not db_user or not db_user.latest_playlist_url: 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 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: async def generate(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
lang = self._detect_lang(update)
chat = update.effective_chat chat = update.effective_chat
if not chat: if not chat:
return return
@@ -142,62 +471,79 @@ class TelegramBotRunner:
users = UserRepository(session) users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id)) db_user = await users.get_by_chat_id(str(chat.id))
if not db_user: if not db_user:
await update.message.reply_text("Пользователь не найден. Напиши /start") await self._reply(update, self._t(lang, "user_not_found_start"), lang=lang)
return return
user_id = db_user.id 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) outcome = await self.services.jobs.generate_for_user(user_id=user_id, force=True, notify=False)
if outcome.ok: if outcome.ok:
msg = outcome.message msg = outcome.message
if outcome.playlist_url: if outcome.playlist_url:
msg += f"\n{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: 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 chat = update.effective_chat
if not chat: if not chat:
return 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: if not context.args:
await update.message.reply_text("Использование: /setsize 30") await self._reply(update, self._t(lang, "size_usage"), lang=lang)
return return
try: try:
value = int(context.args[0]) value = int(context.args[0])
if value < 5 or value > 100:
raise ValueError
except ValueError: except ValueError:
await update.message.reply_text("Размер должен быть числом от 5 до 100.") await self._reply(update, self._t(lang, "size_invalid"), lang=lang)
return return
async with self.session_factory() as session: await self._apply_size(update, value, lang=lang)
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}")
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 chat = update.effective_chat
if not chat: if not chat:
return 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: if value < 0.5 or value > 1.0:
raise ValueError await self._reply(update, self._t(lang, "ratio_invalid"), lang=lang, reply_markup=reply_markup)
except ValueError:
await update.message.reply_text("Значение должно быть от 0.5 до 1.0")
return return
async with self.session_factory() as session: async with self.session_factory() as session:
users = UserRepository(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 db_user.min_new_ratio = value
await session.commit() 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: async def sync_likes(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
lang = self._detect_lang(update)
chat = update.effective_chat chat = update.effective_chat
if not chat: if not chat:
return return
@@ -205,13 +551,13 @@ class TelegramBotRunner:
users = UserRepository(session) users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id)) db_user = await users.get_by_chat_id(str(chat.id))
if not db_user: if not db_user:
await update.message.reply_text("Пользователь не найден. Напиши /start") await self._reply(update, self._t(lang, "user_not_found_start"), lang=lang)
return return
if not db_user.spotify_refresh_token: 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 return
access_token = await self.services.auth.ensure_valid_access_token(session, db_user) 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 self.services.recommendation.sync_saved_tracks(session, db_user, access_token)
await session.commit() await session.commit()
saved_count = await SavedTrackRepository(session).count_for_user(db_user.id) 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)