Add uk and nl
This commit is contained in:
@@ -118,7 +118,7 @@ Notes:
|
||||
- `/generate` calls `PlaylistJobService.generate_for_user(..., force=True, notify=False)`
|
||||
- `/sync` only refreshes liked tracks cache
|
||||
- each command uses a short-lived DB session from `session_factory`
|
||||
- bot UI supports `ru` and `en` (localized text/buttons)
|
||||
- bot UI supports `ru`, `en`, `uk`, and `nl` (localized text/buttons)
|
||||
|
||||
### 3. Service layer
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Recommended:
|
||||
- `/setsize 30` - playlist size (5..100)
|
||||
- `/setratio 0.8` - target new-track ratio (0.5..1.0)
|
||||
- `/sync` - force sync liked tracks
|
||||
- `/lang ru|en` - switch bot language
|
||||
- `/lang ru|en|uk|nl` - switch bot language
|
||||
|
||||
## Recommendation Algorithm
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"button_ratio_100": "🆕 Новые 100%",
|
||||
"button_lang_ru": "🇺🇦 Русский",
|
||||
"button_lang_en": "🇺🇸 English",
|
||||
"button_lang_uk": "🇺🇦 Українська",
|
||||
"button_lang_nl": "🇳🇱 Nederlands",
|
||||
"button_back": "⬅️ Назад",
|
||||
"start_text": (
|
||||
"Я бот для Spotify daily vibe playlist.\n"
|
||||
@@ -41,7 +43,7 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"/setsize N - размер плейлиста\n"
|
||||
"/setratio X - доля новых треков (0.5..1.0)\n"
|
||||
"/sync - обновить лайкнутые треки из Spotify\n"
|
||||
"/lang [ru|en] - сменить язык"
|
||||
"/lang [ru|en|uk|nl] - сменить язык"
|
||||
),
|
||||
"help_text": (
|
||||
"Я бот для Spotify daily vibe playlist.\n"
|
||||
@@ -53,7 +55,7 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"/setsize N - размер плейлиста\n"
|
||||
"/setratio X - доля новых треков (0.5..1.0)\n"
|
||||
"/sync - обновить лайкнутые треки из Spotify\n"
|
||||
"/lang [ru|en] - сменить язык"
|
||||
"/lang [ru|en|uk|nl] - сменить язык"
|
||||
),
|
||||
"connect_open_link": "Открой ссылку и авторизуй Spotify:\n{url}",
|
||||
"spotify_connected_notice": "Spotify подключен: {display_name}\nТеперь можно /generate",
|
||||
@@ -71,7 +73,7 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"sync_done": "Лайкнутые треки обновлены: {count}",
|
||||
"lang_choose": "Выбери язык интерфейса:",
|
||||
"lang_set": "Язык интерфейса: {lang_name}",
|
||||
"lang_invalid": "Поддерживаются только `ru` и `en`.",
|
||||
"lang_invalid": "Поддерживаются только `ru`, `en`, `uk` и `nl`.",
|
||||
"unknown_action": "Не понял команду. Используйте кнопки ниже или /help.",
|
||||
"main_menu_hint": "Главное меню",
|
||||
"settings_menu_hint": "Настройки",
|
||||
@@ -105,6 +107,8 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"button_ratio_100": "🆕 New 100%",
|
||||
"button_lang_ru": "🇺🇦 Русский",
|
||||
"button_lang_en": "🇺🇸 English",
|
||||
"button_lang_uk": "🇺🇦 Українська",
|
||||
"button_lang_nl": "🇳🇱 Nederlands",
|
||||
"button_back": "⬅️ Back",
|
||||
"start_text": (
|
||||
"I am a Spotify daily vibe playlist bot.\n"
|
||||
@@ -116,7 +120,7 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"/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"
|
||||
"/lang [ru|en|uk|nl] - change language"
|
||||
),
|
||||
"help_text": (
|
||||
"I am a Spotify daily vibe playlist bot.\n"
|
||||
@@ -128,7 +132,7 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"/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"
|
||||
"/lang [ru|en|uk|nl] - 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",
|
||||
@@ -146,7 +150,7 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"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.",
|
||||
"lang_invalid": "Only `ru`, `en`, `uk`, and `nl` are supported.",
|
||||
"unknown_action": "I did not understand that. Use the buttons below or /help.",
|
||||
"main_menu_hint": "Main menu",
|
||||
"settings_menu_hint": "Settings",
|
||||
@@ -163,9 +167,164 @@ I18N: dict[str, dict[str, str]] = {
|
||||
"status_last_run": "Last run: {status}, tracks={tracks}",
|
||||
"status_last_url": "{url}",
|
||||
},
|
||||
"uk": {
|
||||
"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_lang_uk": "🇺🇦 Українська",
|
||||
"button_lang_nl": "🇳🇱 Nederlands",
|
||||
"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|uk|nl] - змінити мову"
|
||||
),
|
||||
"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|uk|nl] - змінити мову"
|
||||
),
|
||||
"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`, `uk` та `nl`.",
|
||||
"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}",
|
||||
},
|
||||
"nl": {
|
||||
"button_connect": "🔗 Spotify koppelen",
|
||||
"button_generate": "✨ Genereren",
|
||||
"button_status": "📊 Status",
|
||||
"button_latest": "🎵 Laatste playlist",
|
||||
"button_sync": "🔄 Likes syncen",
|
||||
"button_help": "❓ Help",
|
||||
"button_settings": "⚙️ Instellingen",
|
||||
"button_language": "🌐 Taal / Language",
|
||||
"button_size_20": "📏 Grootte 20",
|
||||
"button_size_30": "📏 Grootte 30",
|
||||
"button_size_50": "📏 Grootte 50",
|
||||
"button_ratio_60": "🆕 Nieuw 60%",
|
||||
"button_ratio_80": "🆕 Nieuw 80%",
|
||||
"button_ratio_100": "🆕 Nieuw 100%",
|
||||
"button_lang_ru": "🇺🇦 Русский",
|
||||
"button_lang_en": "🇺🇸 English",
|
||||
"button_lang_uk": "🇺🇦 Українська",
|
||||
"button_lang_nl": "🇳🇱 Nederlands",
|
||||
"button_back": "⬅️ Terug",
|
||||
"start_text": (
|
||||
"Ik ben een Spotify daily vibe playlist-bot.\n"
|
||||
"Ik maak playlists op basis van je recent listening + liked tracks en probeer ze fris te houden met minder herhalingen.\n\n"
|
||||
"/connect - Spotify koppelen\n"
|
||||
"/status - accountstatus en laatste playlist\n"
|
||||
"/generate - nu een playlist genereren\n"
|
||||
"/latest - link naar de laatste playlist tonen\n"
|
||||
"/setsize N - playlistgrootte\n"
|
||||
"/setratio X - minimale nieuwe ratio (0.5..1.0)\n"
|
||||
"/sync - liked tracks uit Spotify synchroniseren\n"
|
||||
"/lang [ru|en|uk|nl] - taal wijzigen"
|
||||
),
|
||||
"help_text": (
|
||||
"Ik ben een Spotify daily vibe playlist-bot.\n"
|
||||
"Ik maak playlists op basis van je recent listening + liked tracks en probeer ze fris te houden met minder herhalingen.\n\n"
|
||||
"/connect - Spotify koppelen\n"
|
||||
"/status - accountstatus en laatste playlist\n"
|
||||
"/generate - nu een playlist genereren\n"
|
||||
"/latest - link naar de laatste playlist tonen\n"
|
||||
"/setsize N - playlistgrootte\n"
|
||||
"/setratio X - minimale nieuwe ratio (0.5..1.0)\n"
|
||||
"/sync - liked tracks uit Spotify synchroniseren\n"
|
||||
"/lang [ru|en|uk|nl] - taal wijzigen"
|
||||
),
|
||||
"connect_open_link": "Open de link en autoriseer Spotify:\n{url}",
|
||||
"spotify_connected_notice": "Spotify gekoppeld: {display_name}\nJe kunt nu /generate gebruiken",
|
||||
"user_not_found_start": "Gebruiker niet gevonden. Stuur /start",
|
||||
"no_playlist_yet": "Nog geen gegenereerde playlist.",
|
||||
"generate_wait": "Playlist wordt gegenereerd, dit kan 20-60 seconden duren...",
|
||||
"error_prefix": "Fout: {message}",
|
||||
"size_usage": "Gebruik: /setsize 30",
|
||||
"size_invalid": "Grootte moet een getal zijn van 5 t/m 100.",
|
||||
"size_set": "Playlistgrootte ingesteld op: {value}",
|
||||
"ratio_usage": "Gebruik: /setratio 0.8",
|
||||
"ratio_invalid": "Waarde moet tussen 0.5 en 1.0 zijn",
|
||||
"ratio_set": "Minimale nieuwe ratio: {value:.2f}",
|
||||
"sync_need_connect": "Gebruik eerst /connect",
|
||||
"sync_done": "Liked tracks gesynchroniseerd: {count}",
|
||||
"lang_choose": "Kies de interfacetaal:",
|
||||
"lang_set": "Interfacetaal: {lang_name}",
|
||||
"lang_invalid": "Alleen `ru`, `en`, `uk` en `nl` worden ondersteund.",
|
||||
"unknown_action": "Ik begreep dat niet. Gebruik de knoppen hieronder of /help.",
|
||||
"main_menu_hint": "Hoofdmenu",
|
||||
"settings_menu_hint": "Instellingen",
|
||||
"status_yes": "ja",
|
||||
"status_no": "nee",
|
||||
"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"}
|
||||
LANG_NAMES = {"ru": "Русский", "en": "English", "uk": "Українська", "nl": "Nederlands"}
|
||||
SUPPORTED_LANGS = frozenset(LANG_NAMES.keys())
|
||||
|
||||
|
||||
class TelegramBotRunner:
|
||||
@@ -216,7 +375,7 @@ class TelegramBotRunner:
|
||||
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")
|
||||
lang = self._chat_lang.get(str(chat_id), "en")
|
||||
await self.send_message(chat_id, self._t(lang, "spotify_connected_notice", display_name=display_name))
|
||||
|
||||
async def _ensure_user(self, update: Update):
|
||||
@@ -241,17 +400,24 @@ class TelegramBotRunner:
|
||||
chat = update.effective_chat
|
||||
if chat:
|
||||
cached = self._chat_lang.get(str(chat.id))
|
||||
if cached in {"ru", "en"}:
|
||||
if cached in SUPPORTED_LANGS:
|
||||
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 tg_lang.startswith("uk") or tg_lang.startswith("ua"):
|
||||
lang = "uk"
|
||||
elif tg_lang.startswith("ru"):
|
||||
lang = "ru"
|
||||
elif tg_lang.startswith("nl"):
|
||||
lang = "nl"
|
||||
else:
|
||||
lang = "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"}:
|
||||
if lang in SUPPORTED_LANGS:
|
||||
self._chat_lang[str(chat_id)] = lang
|
||||
|
||||
async def _reply(
|
||||
@@ -299,12 +465,13 @@ class TelegramBotRunner:
|
||||
def _language_menu(self, lang: Lang) -> ReplyKeyboardMarkup:
|
||||
kb = [
|
||||
[self._t(lang, "button_lang_ru"), self._t(lang, "button_lang_en")],
|
||||
[self._t(lang, "button_lang_uk"), self._t(lang, "button_lang_nl")],
|
||||
[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]}
|
||||
return {I18N[code][key] for code in SUPPORTED_LANGS if key in I18N.get(code, {})}
|
||||
|
||||
async def on_text_button(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
text = (update.effective_message.text if update.effective_message else "") or ""
|
||||
@@ -331,6 +498,12 @@ class TelegramBotRunner:
|
||||
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_lang_uk"):
|
||||
await self._set_language_from_ui(update, "uk", reply_markup=self._settings_menu("uk"))
|
||||
return
|
||||
if text in self._button_labels("button_lang_nl"):
|
||||
await self._set_language_from_ui(update, "nl", reply_markup=self._settings_menu("nl"))
|
||||
return
|
||||
if text in self._button_labels("button_back"):
|
||||
await self._reply(update, self._t(lang, "main_menu_hint"), lang=lang)
|
||||
return
|
||||
@@ -398,6 +571,12 @@ class TelegramBotRunner:
|
||||
if requested.startswith("en"):
|
||||
await self._set_language_from_ui(update, "en")
|
||||
return
|
||||
if requested.startswith("uk") or requested.startswith("ua"):
|
||||
await self._set_language_from_ui(update, "uk")
|
||||
return
|
||||
if requested.startswith("nl"):
|
||||
await self._set_language_from_ui(update, "nl")
|
||||
return
|
||||
await self._reply(update, self._t(lang, "lang_invalid"), lang=lang)
|
||||
return
|
||||
await self._reply(
|
||||
|
||||
Reference in New Issue
Block a user