Add translations
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user