diff --git a/.env.example b/.env.example index 2806723..a1e1a97 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,8 @@ TELEGRAM_BOT_TOKEN= TMDB_API_KEY= # Language for TMDB results (default: it-IT for Italian) TMDB_LANGUAGE=it-IT +# language for ReplayKeyboard (default: it for Italian) +DEFAULT_LANGUAGE=it # Storage Paths # These paths are used inside the container if using Docker diff --git a/README.md b/README.md index d92d714..0edf42b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Modular Telegram bot for automatic organization of your media library

+

+ 🇺🇸 English | 🇨🇳 简体中文 +

+ ## ✨ Features - 🎬 **Smart Organization** - Automatically creates folders for movies and TV series diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..92535d7 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,357 @@ +# 🎬 MediaButler - Telegram 媒体整理机器人 + +
+ MediaButler +
+ +

+ Python 3.8+ + Docker Ready + MIT License + Telegram Bot +

+ +

+ 模块化 Telegram 机器人,自动整理你的媒体库 +

+ +

+ 🇺🇸 English | 🇨🇳 简体中文 +

+ +## ✨ 功能亮点 + +- 🎬 **智能整理** - 自动为电影和电视剧创建文件夹 +- 📺 **剧集识别** - 识别季/集模式(S01E01、1x01 等) +- 🎯 **TMDB 集成** - 元数据、海报和自动重命名 +- 📁 **结构清晰** - 电影独立文件夹,剧集按季整理 +- ⏳ **队列管理** - 多任务下载,支持配置并发数 +- 💾 **空间监控** - 实时磁盘空间管理,自动队列 +- 👥 **多用户** - 白名单授权系统 +- 🔄 **高可用** - 支持断点续传和自动重试 +- 🐳 **Docker 支持** - 一键 Docker Compose 部署 + +## 🏗️ 架构 + +本项目采用模块化架构,便于维护和扩展: + +``` +mediabutler/ +├── main.py # 主入口 +├── core/ # 核心系统模块 +│ ├── __init__.py +│ ├── config.py # 配置管理 +│ ├── auth.py # 授权管理 +│ ├── downloader.py # 下载与队列管理 +│ ├── space_manager.py # 磁盘空间监控 +│ └── tmdb_client.py # TMDB API 客户端 +├── handlers/ # Telegram 事件处理 +│ ├── __init__.py +│ ├── commands.py # 命令处理(/start, /status 等) +│ ├── callbacks.py # 按钮回调处理 +│ └── files.py # 文件接收与识别 +├── models/ # 数据模型 +│ ├── __init__.py +│ └── download.py # 下载信息数据类 +├── utils/ # 工具与辅助 +│ ├── __init__.py +│ ├── naming.py # 文件名解析与管理 +│ ├── formatters.py # 消息格式化 +│ └── helpers.py # 通用辅助 +└── requirements.txt # 依赖列表 +``` + +### 📦 主要模块说明 + +#### Core +- **`config`**: 配置管理与校验 +- **`auth`**: 多用户授权与管理员 +- **`downloader`**: 下载队列、重试与错误处理 +- **`space_manager`**: 空间监控与智能清理 +- **`tmdb_client`**: TMDB 集成与限流 + +#### Handlers +- **`commands`**: 所有机器人命令(`/start`、`/status`、`/space` 等) +- **`callbacks`**: 按钮与交互管理 +- **`files`**: 文件识别与处理 + +#### Utils +- **`naming`**: 智能文件名解析与结构生成 +- **`formatters`**: Telegram 消息格式化与进度条 +- **`helpers`**: 重试、校验、限流、异步辅助 + +## 🚀 快速开始 + +### 前置条件 + +- Python 3.8+ 或 Docker +- Telegram API 凭证([my.telegram.org](https://my.telegram.org)) +- BotFather 机器人令牌([@BotFather](https://t.me/botfather)) +- (可选)TMDB API Key + +### 推荐:Docker 部署 + +1. **克隆仓库**: +```bash +git clone https://github.com/yourusername/mediabutler.git +cd mediabutler +``` + +2. **配置环境变量**: +```bash +cp .env.example .env +nano .env # 填写你的凭证 +``` + +3. **Docker Compose 启动**: +```bash +docker-compose up -d +``` + +### 手动安装 + +1. **创建 Python 虚拟环境**: +```bash +python -m venv venv +source venv/bin/activate # Linux/Mac +# 或 +venv\Scripts\activate # Windows +``` + +2. **安装依赖**: +```bash +pip install -r requirements.txt +``` + +3. **配置并启动**: +```bash +cp .env.example .env +nano .env # 配置凭证 +python main.py +``` + +## 📖 配置说明 + +### 主要环境变量 + +```env +# Telegram(必填) +TELEGRAM_API_ID=123456 +TELEGRAM_API_HASH=abcdef123456 +TELEGRAM_BOT_TOKEN=123456:ABC-DEF + +# TMDB(可选) +TMDB_API_KEY=your_tmdb_api_key +TMDB_LANGUAGE=en-US + +# 路径 +MOVIES_PATH=/media/movies +TV_PATH=/media/tv +TEMP_PATH=/media/temp + +# 授权用户 +AUTHORIZED_USERS=123456789,987654321 + +# 限制 +MAX_CONCURRENT_DOWNLOADS=3 +MIN_FREE_SPACE_GB=5 +WARNING_THRESHOLD_GB=10 +``` + +详见 .env.example 获取全部配置项。 + +## 🎯 使用方法 + +### 机器人命令 + +| 命令 | 说明 | 权限 | +|----------------|----------------------------|---------| +| `/start` | 启动机器人并显示信息 | 所有用户 | +| `/status` | 显示活跃下载和队列 | 所有用户 | +| `/space` | 显示磁盘空间详情 | 所有用户 | +| `/waiting` | 显示等待空间的文件 | 所有用户 | +| `/cancel_all` | 取消所有下载 | 所有用户 | +| `/help` | 显示命令帮助 | 所有用户 | +| `/users` | 列出授权用户 | 管理员 | +| `/stop` | 停止机器人 | 管理员 | + +### 下载流程 + +1. **发送/转发** 视频文件到机器人 +2. 机器人**分析**文件名并搜索 TMDB +3. **确认**或选择电影/剧集 +4. 剧集可**选择季数** +5. **自动下载**或进入队列 + +### 文件夹结构示例 + +``` +/media/ +├── movies/ +│ ├── Avatar (2009)/ +│ │ └── Avatar (2009).mp4 +│ └── Inception (2010)/ +│ └── Inception (2010).mp4 +└── tv/ + ├── Breaking Bad [EN]/ + │ ├── Season 01/ + │ │ ├── Breaking Bad - S01E01 - Pilot.mp4 + │ │ └── Breaking Bad - S01E02 - Cat's in the Bag.mp4 + │ └── Season 02/ + └── The Office/ + └── Season 04/ +``` + +## 🔧 开发 + +### 扩展机器人 + +模块化设计,便于添加新功能: + +#### 新增命令 + +1. 在 commands.py 中添加方法: +```python +async def mycommand_handler(self, event): + """Handler for /mycommand""" + if not await self.auth.check_authorized(event): + return + + # 命令逻辑 + await event.reply("Command response") +``` + +2. 在 `register()` 注册: +```python +self.client.on(events.NewMessage(pattern='/mycommand'))(self.mycommand_handler) +``` + +#### 新增元数据源 + +1. 在 core 新建模块: +```python +# core/metadata_provider.py +class MetadataProvider: + async def search(self, query: str): + # 实现搜索 + pass +``` + +2. 在 `FileHandlers` 或相关位置集成 + +### 测试 + +```bash +# 单元测试 +python -m pytest tests/ + +# 覆盖率测试 +python -m pytest --cov=core --cov=handlers --cov=utils tests/ +``` + +### 代码风格 + +遵循 PEP 8: +```bash +# 格式化 +black . + +# 代码检查 +flake8 . --max-line-length=100 + +# 类型检查 +mypy . +``` + +## 🐳 Docker + +### 构建镜像 + +```bash +docker build -t mediabutler:latest . +``` + +### 自定义 Docker Compose + +```yaml +version: '3.8' + +services: + mediabutler: + image: mediabutler:latest + container_name: mediabutler + restart: unless-stopped + env_file: .env + volumes: + - ${MOVIES_PATH}:/media/movies + - ${TV_PATH}:/media/tv + - ./session:/app/session + networks: + - media_network + +networks: + media_network: + external: true +``` + +## 📊 监控 + +### 日志 + +```bash +# Docker 日志 +docker logs -f mediabutler + +# 日志文件(如配置) +tail -f logs/mediabutler.log +``` + +### 指标 + +通过 `/status` 命令可查看: +- 活跃下载 +- 队列文件 +- 可用空间 +- 下载速度 + +## 🚧 路线图 + +- [ ] Web 管理界面 +- [ ] Jellyfin/Plex 集成 +- [ ] 字幕支持 +- [ ] 播放列表/频道下载 +- [ ] 通知自定义 +- [ ] 配置备份/恢复 +- [ ] REST API 集成 +- [ ] 完整多语言支持 + +## 🤝 贡献 + +欢迎贡献!详见 CONTRIBUTING.md。 + +1. Fork 本项目 +2. 创建分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add AmazingFeature'`) +4. 推送分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +## 📝 许可证 + +本项目采用 MIT 许可证,详见 LICENSE。 + +## 🙏 鸣谢 + +- [Telethon](https://github.com/LonamiWebs/Telethon) - Telegram MTProto 客户端 +- [TMDB](https://www.themoviedb.org) - 元数据库 +- [aiohttp](https://github.com/aio-libs/aiohttp) - 异步 HTTP 客户端 +- 自托管社区 ❤️ + +## ⚠️ 免责声明 + +本机器人仅供个人使用。请遵守版权法规,仅下载您有权获取的内容。 + +--- + +

+ 为自托管社区开发 ❤️ +

\ No newline at end of file diff --git a/core/config.py b/core/config.py index 8530d60..ec9e10a 100644 --- a/core/config.py +++ b/core/config.py @@ -95,6 +95,17 @@ def is_opensubtitles_configured(self) -> bool: return bool(self.opensubtitles_username and self.opensubtitles_password) +@dataclass +class I18nConfig: + """Configurazione internazionalizzazione""" + default_language: str = 'it' + supported_languages: List[str] = None + + def __post_init__(self): + if self.supported_languages is None: + self.supported_languages = ['it', 'en', 'es', 'fr', 'de', 'pt', 'ru', 'zh', 'ja', 'ar'] + + class Config: """Configurazione principale MediaButler""" @@ -105,6 +116,7 @@ def __init__(self): self.limits = self._load_limits_config() self.auth = self._load_auth_config() self.subtitles = self._load_subtitle_config() + self.i18n = self._load_i18n_config() self.logger = self._setup_logging() # Validazione @@ -189,6 +201,12 @@ def _load_subtitle_config(self) -> SubtitleConfig: preferred_format=os.getenv('SUBTITLE_FORMAT', 'srt') ) + def _load_i18n_config(self) -> I18nConfig: + """Carica configurazione internazionalizzazione""" + return I18nConfig( + default_language=os.getenv('DEFAULT_LANGUAGE', 'it') + ) + def _setup_logging(self) -> logging.Logger: """Configura logging""" logging.basicConfig( @@ -220,6 +238,8 @@ def log_config(self): self.logger.info(f"Subtitle auto-download: {self.subtitles.auto_download}") self.logger.info(f"Max concurrent downloads: {self.limits.max_concurrent_downloads}") self.logger.info(f"Min free space: {self.limits.min_free_space_gb} GB") + self.logger.info(f"Default language: {self.i18n.default_language}") + self.logger.info(f"Supported languages: {len(self.i18n.supported_languages)}") # Singleton per configurazione globale diff --git a/core/i18n.py b/core/i18n.py new file mode 100644 index 0000000..10818ff --- /dev/null +++ b/core/i18n.py @@ -0,0 +1,263 @@ +""" +Sistema di internazionalizzazione per MediaButler +Supporta più lingue con fallback automatico +""" +import os +import json +from pathlib import Path +from typing import Dict, Any, Optional +from dataclasses import dataclass + +@dataclass +class LocaleConfig: + """Configurazione locale""" + code: str + name: str + emoji: str + rtl: bool = False + +class I18nManager: + """Gestore internazionalizzazione""" + + # Lingue supportate + SUPPORTED_LOCALES = { + 'it': LocaleConfig('it', 'Italiano', '🇮🇹'), + 'en': LocaleConfig('en', 'English', '🇺🇸'), + 'es': LocaleConfig('es', 'Español', '🇪🇸'), + 'fr': LocaleConfig('fr', 'Français', '🇫🇷'), + 'de': LocaleConfig('de', 'Deutsch', '🇩🇪'), + 'pt': LocaleConfig('pt', 'Português', '🇵🇹'), + 'ru': LocaleConfig('ru', 'Русский', '🇷🇺'), + 'zh': LocaleConfig('zh', '中文', '🇨🇳'), + 'ja': LocaleConfig('ja', '日本語', '🇯🇵'), + 'ar': LocaleConfig('ar', 'العربية', '🇸🇦', rtl=True) + } + + def __init__(self, default_locale: str = 'it'): + """ + Inizializza il gestore i18n + + Args: + default_locale: Lingua predefinita + """ + self.default_locale = default_locale + self.current_locale = default_locale + self.translations: Dict[str, Dict[str, Any]] = {} + self.user_locales: Dict[int, str] = {} # user_id -> locale + + # Directory delle traduzioni + self.locales_dir = Path(__file__).parent.parent / 'locales' + self.locales_dir.mkdir(exist_ok=True) + + # Carica tutte le traduzioni + self._load_all_translations() + + def _load_all_translations(self): + """Carica tutte le traduzioni disponibili""" + for locale_code in self.SUPPORTED_LOCALES: + self._load_translation(locale_code) + + def _load_translation(self, locale: str): + """ + Carica traduzione per una lingua specifica + + Args: + locale: Codice lingua (es: 'it', 'en') + """ + locale_file = self.locales_dir / f'{locale}.json' + + if locale_file.exists(): + try: + with open(locale_file, 'r', encoding='utf-8') as f: + self.translations[locale] = json.load(f) + except Exception as e: + print(f"Errore caricamento {locale}.json: {e}") + self.translations[locale] = {} + else: + self.translations[locale] = {} + + def set_user_locale(self, user_id: int, locale: str): + """ + Imposta lingua per un utente specifico + + Args: + user_id: ID utente Telegram + locale: Codice lingua + """ + if locale in self.SUPPORTED_LOCALES: + self.user_locales[user_id] = locale + else: + raise ValueError(f"Lingua non supportata: {locale}") + + def get_user_locale(self, user_id: int) -> str: + """ + Ottieni lingua per un utente + + Args: + user_id: ID utente Telegram + + Returns: + Codice lingua dell'utente + """ + return self.user_locales.get(user_id, self.default_locale) + + def t(self, key: str, user_id: Optional[int] = None, **kwargs) -> str: + """ + Traduce una chiave per un utente specifico + + Args: + key: Chiave di traduzione (es: 'commands.start.welcome') + user_id: ID utente (opzionale) + **kwargs: Parametri per interpolazione + + Returns: + Testo tradotto + """ + if user_id is not None and not kwargs.get('user_id'): + kwargs['user_id'] = user_id # Assicurati che user_id sia sempre passato per l'interpolazione + locale = self.get_user_locale(user_id) if user_id else self.current_locale + return self._get_translation(key, locale, **kwargs) + + def _get_translation(self, key: str, locale: str, **kwargs) -> str: + """ + Ottieni traduzione per chiave e lingua specifiche + + Args: + key: Chiave traduzione + locale: Codice lingua + **kwargs: Parametri interpolazione + + Returns: + Testo tradotto + """ + + # Cerca nella lingua richiesta + translation = self._find_nested_key(self.translations.get(locale, {}), key) + + # Fallback alla lingua predefinita + if translation is None: + translation = self._find_nested_key( + self.translations.get(self.default_locale, {}), key + ) + + # Fallback alla chiave stessa se non trovata + if translation is None: + translation = key + + # Interpolazione parametri + if kwargs: + try: + translation = translation.format(**kwargs) + except (KeyError, ValueError): + print(f"Errore interpolazione per chiave: {key} con params: {kwargs}") + + return translation + + def _find_nested_key(self, data: Dict[str, Any], key: str) -> Optional[str]: + """ + Trova chiave annidata nel dizionario traduzioni + + Args: + data: Dizionario traduzioni + key: Chiave con notazione punto (es: 'menu.main.title') + + Returns: + Valore traduzione o None + """ + keys = key.split('.') + current = data + + for k in keys: + if isinstance(current, dict) and k in current: + current = current[k] + else: + return None + + return current if isinstance(current, str) else None + + def get_locale_info(self, locale: str) -> Optional[LocaleConfig]: + """ + Ottieni informazioni su una lingua + + Args: + locale: Codice lingua + + Returns: + Configurazione lingua o None + """ + return self.SUPPORTED_LOCALES.get(locale) + + def get_available_locales(self) -> Dict[str, LocaleConfig]: + """ + Ottieni tutte le lingue disponibili + + Returns: + Dizionario lingue supportate + """ + return self.SUPPORTED_LOCALES.copy() + + def create_language_menu_buttons(self, user_id: int): + """ + Crea bottoni per selezione lingua + + Args: + user_id: ID utente + + Returns: + Lista bottoni per Telegram + """ + from telethon import Button + + current_locale = self.get_user_locale(user_id) + buttons = [] + + # Crea righe di 2 bottoni + locale_items = list(self.SUPPORTED_LOCALES.items()) + for i in range(0, len(locale_items), 2): + row = [] + for j in range(2): + if i + j < len(locale_items): + code, config = locale_items[i + j] + marker = "✅" if code == current_locale else "" + text = f"{config.emoji} {config.name} {marker}" + row.append(Button.inline(text, f"lang_{code}")) + buttons.append(row) + + # Bottone indietro + buttons.append([Button.inline( + self.t('buttons.back', user_id), + "menu_back" + )]) + + return buttons + +# Istanza globale +_i18n_manager = None + +def get_i18n() -> I18nManager: + """ + Ottieni istanza globale del gestore i18n + + Returns: + Gestore internazionalizzazione + """ + global _i18n_manager + if _i18n_manager is None: + # Leggi lingua predefinita dalle variabili ambiente + default_lang = os.getenv('DEFAULT_LANGUAGE', 'it') + _i18n_manager = I18nManager(default_lang) + return _i18n_manager + +def t(key: str, user_id: Optional[int] = None, **kwargs) -> str: + """ + Funzione di convenienza per traduzione + + Args: + key: Chiave traduzione + user_id: ID utente + **kwargs: Parametri interpolazione + + Returns: + Testo tradotto + """ + return get_i18n().t(key, user_id, **kwargs) \ No newline at end of file diff --git a/handlers/commands.py b/handlers/commands.py index 4a1d64e..671b3d4 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -8,6 +8,7 @@ from core.space_manager import SpaceManager from core.downloader import DownloadManager from core.config import get_config +from core.i18n import get_i18n, t from utils.helpers import human_readable_size, FileHelpers @@ -27,6 +28,7 @@ def __init__( self.downloads = download_manager self.config = get_config() self.logger = self.config.logger + self.i18n = get_i18n() def register(self): """Registra tutti gli handler comandi""" @@ -46,51 +48,54 @@ def register(self): self.client.on(events.NewMessage(pattern='/subtitles'))(self.subtitles_handler) self.client.on(events.NewMessage(pattern='/sub_toggle'))(self.subtitle_toggle_handler) self.client.on(events.NewMessage(pattern='/sub_auto'))(self.subtitle_auto_handler) + self.client.on(events.NewMessage(pattern='/language'))(self.language_handler) # Callback handler per bottoni self.client.on(events.CallbackQuery(pattern='menu_'))(self.menu_callback_handler) self.client.on(events.CallbackQuery(pattern='cancel_'))(self.cancel_callback_handler) self.client.on(events.CallbackQuery(pattern='stop_'))(self.stop_callback_handler) self.client.on(events.CallbackQuery(pattern='sub_'))(self.subtitle_callback_handler) + self.client.on(events.CallbackQuery(pattern='lang_'))(self.language_callback_handler) self.logger.info("Handler comandi registrati con menu inline") - def _create_main_menu(self, is_admin: bool = False): + def _create_main_menu(self, is_admin: bool = False, user_id: int = None): """Crea menu principale con bottoni inline""" buttons = [ [ - Button.inline("📊 Stato", "menu_status"), - Button.inline("💾 Spazio", "menu_space"), - Button.inline("📥 Downloads", "menu_downloads") + Button.inline(t("commands.menu.main_buttons.status", user_id), "menu_status"), + Button.inline(t("commands.menu.main_buttons.space", user_id), "menu_space"), + Button.inline(t("commands.menu.main_buttons.downloads", user_id), "menu_downloads") ], [ - Button.inline("⏳ In Attesa", "menu_waiting"), - Button.inline("📝 Sottotitoli", "menu_subtitles"), - Button.inline("⚙️ Impostazioni", "menu_settings") + Button.inline(t("commands.menu.main_buttons.waiting", user_id), "menu_waiting"), + Button.inline(t("commands.menu.main_buttons.subtitles", user_id), "menu_subtitles"), + Button.inline(t("commands.menu.main_buttons.settings", user_id), "menu_settings") ], [ - Button.inline("❓ Aiuto", "menu_help"), - Button.inline("❌ Cancella Tutto", "menu_cancel_all") + Button.inline(t("commands.menu.main_buttons.help", user_id), "menu_help"), + Button.inline(t("commands.menu.main_buttons.language", user_id), "menu_language"), + Button.inline(t("commands.menu.main_buttons.cancel_all", user_id), "menu_cancel_all") ] ] if is_admin: buttons.append([ - Button.inline("👥 Utenti", "menu_users"), - Button.inline("🛑 Stop Bot", "menu_stop") + Button.inline(t("commands.menu.main_buttons.users", user_id), "menu_users"), + Button.inline(t("commands.menu.main_buttons.stop", user_id), "menu_stop") ]) return buttons - def _create_quick_menu(self): + def _create_quick_menu(self, user_id: int = None): """Crea menu rapido con azioni principali""" return [ [ - Button.inline("📊 Stato", "menu_status"), - Button.inline("📥 Downloads", "menu_downloads") + Button.inline(t("commands.menu.quick_buttons.status", user_id), "menu_status"), + Button.inline(t("commands.menu.quick_buttons.downloads", user_id), "menu_downloads") ], [ - Button.inline("📱 Menu Completo", "menu_full") + Button.inline(t("commands.menu.quick_buttons.full_menu", user_id), "menu_full") ] ] @@ -110,7 +115,7 @@ async def start_handler(self, event: events.NewMessage.Event): # Invia con menu inline await event.reply( welcome_text, - buttons=self._create_main_menu(is_admin), + buttons=self._create_main_menu(is_admin, user.id), link_preview=False ) @@ -119,12 +124,12 @@ async def menu_handler(self, event: events.NewMessage.Event): if not await self.auth.check_authorized(event): return - is_admin = self.auth.is_admin(event.sender_id) + user_id = event.sender_id + is_admin = self.auth.is_admin(user_id) await event.reply( - "🎬 **MediaButler - Menu Principale**\n\n" - "Seleziona un'opzione:", - buttons=self._create_main_menu(is_admin) + t("commands.menu.title", user_id), + buttons=self._create_main_menu(is_admin, user_id) ) async def status_handler(self, event: events.NewMessage.Event): @@ -132,12 +137,13 @@ async def status_handler(self, event: events.NewMessage.Event): if not await self.auth.check_authorized(event): return - status_text = self._get_status_text() + user_id = event.sender_id + status_text = self._get_status_text(user_id) buttons = [ [ - Button.inline("🔄 Aggiorna", "menu_status"), - Button.inline("📱 Menu", "menu_back") + Button.inline(t("buttons.refresh", user_id), "menu_status"), + Button.inline(t("buttons.menu", user_id), "menu_back") ] ] @@ -164,14 +170,15 @@ async def downloads_handler(self, event: events.NewMessage.Event): if not await self.auth.check_authorized(event): return - downloads_text = self._get_downloads_detailed() + user_id = event.sender_id + downloads_text = self._get_downloads_detailed(user_id) buttons = [ [ - Button.inline("🔄 Aggiorna", "menu_downloads"), - Button.inline("❌ Cancella Tutti", "menu_cancel_all") + Button.inline(t("buttons.refresh", user_id), "menu_downloads"), + Button.inline(t("commands.menu.main_buttons.cancel_all", user_id), "menu_cancel_all") ], - [Button.inline("📱 Menu", "menu_back")] + [Button.inline(t("buttons.menu", user_id), "menu_back")] ] await event.reply(downloads_text, buttons=buttons) @@ -181,12 +188,13 @@ async def waiting_handler(self, event: events.NewMessage.Event): if not await self.auth.check_authorized(event): return - waiting_text = self._get_waiting_text() + user_id = event.sender_id + waiting_text = self._get_waiting_text(user_id) buttons = [ [ - Button.inline("🔄 Aggiorna", "menu_waiting"), - Button.inline("📱 Menu", "menu_back") + Button.inline(t("buttons.refresh", user_id), "menu_waiting"), + Button.inline(t("buttons.menu", user_id), "menu_back") ] ] @@ -265,9 +273,10 @@ async def settings_handler(self, event: events.NewMessage.Event): if not await self.auth.check_authorized(event): return - settings_text = self._get_settings_text() + user_id = event.sender_id + settings_text = self._get_settings_text(user_id) - buttons = [[Button.inline("📱 Menu", "menu_back")]] + buttons = [[Button.inline(t("buttons.menu", user_id), "menu_back")]] await event.reply(settings_text, buttons=buttons) @@ -276,11 +285,12 @@ async def help_handler(self, event: events.NewMessage.Event): if not await self.auth.check_authorized(event): return - help_text = self._get_help_text() + user_id = event.sender_id + help_text = self._get_help_text(user_id) await event.reply( help_text, - buttons=self._create_quick_menu() + buttons=self._create_quick_menu(user_id) ) async def users_handler(self, event: events.NewMessage.Event): @@ -291,9 +301,10 @@ async def users_handler(self, event: events.NewMessage.Event): if not await self.auth.require_admin(event): return - users_text = self._get_users_text() + user_id = event.sender_id + users_text = self._get_users_text(user_id) - buttons = [[Button.inline("📱 Menu", "menu_back")]] + buttons = [[Button.inline(t("buttons.menu", user_id), "menu_back")]] await event.reply(users_text, buttons=buttons) @@ -397,16 +408,40 @@ async def stop_callback_handler(self, event: events.CallbackQuery.Event): async def _handle_menu_action(self, event, action: str): """Gestisce azioni menu""" + user_id = event.sender_id + + # Gestione azione lingua + if action == 'language': + current_lang = self.i18n.get_locale_info(self.i18n.get_user_locale(user_id)) + title = t("commands.language.title", user_id) + current_info = t("commands.language.current", user_id, + language=f"{current_lang.emoji} {current_lang.name}") + + await event.edit( + f"{title}\n\n{current_info}", + buttons=self.i18n.create_language_menu_buttons(user_id) + ) + return + + # Gestione menu principale + if action == 'back' or action == 'full': + is_admin = self.auth.is_admin(user_id) + await event.edit( + t("commands.menu.title", user_id), + buttons=self._create_main_menu(is_admin, user_id) + ) + return + content_map = { - 'status': self._get_status_text, + 'status': lambda: self._get_status_text(user_id), 'space': self.space.format_disk_status, - 'downloads': self._get_downloads_detailed, - 'waiting': self._get_waiting_text, - 'subtitles': self._get_subtitle_status, - 'settings': self._get_settings_text, - 'help': self._get_help_text, - 'users': self._get_users_text, - 'cancel_all': self._get_cancel_confirmation + 'downloads': lambda: self._get_downloads_detailed(user_id), + 'waiting': lambda: self._get_waiting_text(user_id), + 'subtitles': lambda: self._get_subtitle_status(user_id), + 'settings': lambda: self._get_settings_text(user_id), + 'help': lambda: self._get_help_text(user_id), + 'users': lambda: self._get_users_text(user_id), + 'cancel_all': lambda: self._get_cancel_confirmation(user_id) } if action in content_map: @@ -417,31 +452,31 @@ async def _handle_menu_action(self, event, action: str): # Bottoni specifici per ogni azione if action in ['status', 'space', 'downloads', 'waiting']: buttons.append([ - Button.inline("🔄 Aggiorna", f"menu_{action}"), - Button.inline("📱 Menu", "menu_back") + Button.inline(t("buttons.refresh", user_id), f"menu_{action}"), + Button.inline(t("buttons.menu", user_id), "menu_back") ]) elif action == 'subtitles': - buttons = self._create_subtitle_menu() + buttons = self._create_subtitle_menu(user_id) elif action == 'cancel_all': buttons = [ [ - Button.inline("✅ Conferma", "cancel_confirm"), - Button.inline("❌ Annulla", "menu_back") + Button.inline(t("buttons.confirm", user_id), "cancel_confirm"), + Button.inline(t("buttons.cancel", user_id), "menu_back") ] ] elif action == 'users': if not self.auth.is_admin(event.sender_id): - await event.answer("❌ Solo amministratori", alert=True) + await event.answer(t("messages.admin_only_action", user_id), alert=True) return - buttons = [[Button.inline("📱 Menu", "menu_back")]] + buttons = [[Button.inline(t("buttons.menu", user_id), "menu_back")]] elif action == 'stop': if not self.auth.is_admin(event.sender_id): - await event.answer("❌ Solo amministratori", alert=True) + await event.answer(t("messages.admin_only_action", user_id), alert=True) return buttons = [ [ - Button.inline("✅ Conferma Arresto", "stop_confirm"), - Button.inline("❌ Annulla", "menu_back") + Button.inline(t("confirmations.stop_bot.button_confirm", user_id), "stop_confirm"), + Button.inline(t("buttons.cancel", user_id), "menu_back") ] ] content = "🛑 **Conferma Arresto Bot**\n\n⚠️ Questa azione:\n• Cancellerà tutti i download\n• Fermerà il bot\n• Richiederà riavvio manuale\n\nConfermi?" @@ -460,63 +495,51 @@ def _format_welcome_message(self, user_id: int, is_admin: bool) -> str: queued = self.downloads.get_queued_count() tmdb_emoji = "🎯" if self.config.tmdb.is_enabled else "⚠️" - tmdb_status = "TMDB Attivo" if self.config.tmdb.is_enabled else "TMDB Non configurato" + tmdb_status = t("messages.tmdb_active", user_id) if self.config.tmdb.is_enabled else t("messages.tmdb_disabled", user_id) - role = "👑 Amministratore" if is_admin else "👤 Utente" + role = t("commands.start.role_admin", user_id) if is_admin else t("commands.start.role_user", user_id) # Lista comandi per accesso rapido - commands_list = ( - "**📝 Comandi rapidi:**\n" - "`/status` - Stato sistema\n" - "`/downloads` - Download attivi\n" - "`/space` - Spazio disco\n" - "`/menu` - Menu completo\n" - "`/help` - Aiuto" - ) + commands_list = t("commands.start.commands_rapid", user_id) if is_admin: - commands_list += "\n`/users` - Gestione utenti\n`/stop` - Arresta bot" - - return ( - f"🎬 **MediaButler - Organizzatore Media**\n\n" - f"Benvenuto! {role}\n" - f"ID: `{user_id}`\n\n" - f"**📊 Stato Sistema:**\n" - f"• 💾 Spazio: {total_free:.1f} GB liberi\n" - f"• 📥 Attivi: {active} download\n" - f"• ⏳ In coda: {queued} file\n" - f"• {tmdb_emoji} {tmdb_status}\n\n" - f"**📤 Per iniziare:** Invia un file video\n\n" - f"{commands_list}\n\n" - f"**💡 Usa il menu sotto per navigare facilmente!**" - ) + commands_list += t("commands.start.commands_admin", user_id) + + return t("commands.start.welcome", user_id, + role=role, + total_free=f"{total_free:.1f}", + active=active, + queued=queued, + tmdb_emoji=tmdb_emoji, + tmdb_status=tmdb_status, + commands_list=commands_list) - def _get_status_text(self) -> str: + def _get_status_text(self, user_id: int = None) -> str: """Genera testo stato sistema""" - status_text = "📊 **Stato Sistema**\n\n" + status_text = t("commands.status.title", user_id) + "\n\n" active = self.downloads.get_active_downloads() if active: - status_text += f"**📥 Download attivi ({len(active)}):**\n" + status_text += t("commands.status.downloads_active", user_id, count=len(active)) + "\n" for info in active[:5]: status_text += f"• `{info.filename[:30]}{'...' if len(info.filename) > 30 else ''}`\n" if info.progress > 0: status_text += f" {info.progress:.1f}% - {info.speed_mbps:.1f} MB/s\n" if len(active) > 5: - status_text += f" ...e altri {len(active) - 5}\n" + status_text += t("commands.status.downloads_more", user_id, count=len(active) - 5) + "\n" status_text += "\n" else: - status_text += "📭 Nessun download attivo\n\n" + status_text += t("commands.status.downloads_none", user_id) + "\n\n" queue_count = self.downloads.get_queued_count() space_waiting = self.downloads.get_space_waiting_count() if queue_count > 0: - status_text += f"⏳ **In coda:** {queue_count} file\n" + status_text += t("commands.status.queue_pending", user_id, count=queue_count) + "\n" if space_waiting > 0: - status_text += f"⏸️ **In attesa spazio:** {space_waiting} file\n" + status_text += t("commands.status.queue_waiting_space", user_id, count=space_waiting) + "\n" - status_text += "\n💾 **Spazio:**\n" + status_text += "\n" + t("commands.status.disk_space", user_id) + "\n" disk_usage = self.space.get_all_disk_usage() for name, usage in disk_usage.items(): @@ -524,17 +547,17 @@ def _get_status_text(self) -> str: return status_text - def _get_downloads_detailed(self) -> str: + def _get_downloads_detailed(self, user_id: int = None) -> str: """Dettagli download attivi""" active = self.downloads.get_active_downloads() if not active: return ( - "📭 **Nessun download attivo**\n\n" - "Invia un file video per iniziare." + t("commands.downloads.none", user_id) + "\n\n" + + t("messages.send_video", user_id) ) - text = f"📥 **Download Attivi ({len(active)})**\n\n" + text = t("commands.downloads.title", user_id) + f" ({len(active)})\n\n" for idx, info in enumerate(active, 1): text += f"**{idx}. {info.filename[:35]}{'...' if len(info.filename) > 35 else ''}**\n" @@ -559,98 +582,91 @@ def _get_downloads_detailed(self) -> str: return text - def _get_waiting_text(self) -> str: + def _get_waiting_text(self, user_id: int = None) -> str: """Testo file in attesa""" waiting_count = self.downloads.get_space_waiting_count() if waiting_count == 0: - return ( - "✅ **Nessun file in attesa**\n\n" - "Tutti i download hanno spazio sufficiente." - ) + return t("commands.waiting.none", user_id) - text = f"⏳ **File in attesa di spazio ({waiting_count})**\n\n" + text = t("commands.waiting.title", user_id) + f" ({waiting_count})\n\n" for idx, item in enumerate(self.downloads.space_waiting_queue[:10], 1): info = item.download_info - text += f"**{idx}.** `{info.filename[:35]}{'...' if len(info.filename) > 35 else ''}`\n" + filename_display = info.filename[:35] + '...' if len(info.filename) > 35 else info.filename + text += f"**{idx}.** `{filename_display}`\n" text += f" 📏 {info.size_gb:.1f} GB | 📂 {info.media_type.value}\n" if waiting_count > 10: - text += f"\n...e altri {waiting_count - 10} file" + text += "\n" + t("commands.waiting.more", user_id, count=waiting_count - 10) return text - def _get_settings_text(self) -> str: + def _get_settings_text(self, user_id: int = None) -> str: """Testo impostazioni""" - tmdb_status = "✅ Attivo" if self.config.tmdb.is_enabled else "❌ Non configurato" + tmdb_status = t("commands.settings.tmdb_status_active", user_id) if self.config.tmdb.is_enabled else t("commands.settings.tmdb_status_disabled", user_id) return ( - "⚙️ **Impostazioni Correnti**\n\n" - f"**Download:**\n" - f"• Simultanei: {self.config.limits.max_concurrent_downloads}\n" - f"• Max dimensione: {self.config.limits.max_file_size_gb} GB\n\n" - f"**Spazio:**\n" - f"• Minimo riservato: {self.config.limits.min_free_space_gb} GB\n" - f"• Soglia avviso: {self.config.limits.warning_threshold_gb} GB\n" - f"• Controllo ogni: {self.config.limits.space_check_interval}s\n\n" - f"**TMDB:**\n" - f"• Stato: {tmdb_status}\n" - f"• Lingua: {self.config.tmdb.language}\n\n" - f"**Percorsi:**\n" - f"• Film: `{self.config.paths.movies}`\n" - f"• Serie: `{self.config.paths.tv}`\n" - f"• Temp: `{self.config.paths.temp}`\n\n" - f"ℹ️ Modifica `.env` per cambiare." + t("commands.settings.title", user_id) + "\n\n" + + t("commands.settings.downloads", user_id) + "\n" + + t("commands.settings.concurrent", user_id, count=self.config.limits.max_concurrent_downloads) + "\n" + + t("commands.settings.max_size", user_id, size=self.config.limits.max_file_size_gb) + "\n\n" + + t("commands.settings.space_section", user_id) + "\n" + + t("commands.settings.min_free", user_id, size=self.config.limits.min_free_space_gb) + "\n" + + t("commands.settings.warning_threshold", user_id, size=self.config.limits.warning_threshold_gb) + "\n" + + t("commands.settings.check_interval", user_id, seconds=self.config.limits.space_check_interval) + "\n\n" + + t("commands.settings.tmdb_section", user_id) + "\n" + + f"• {t('commands.settings.tmdb_status_active' if self.config.tmdb.is_enabled else 'commands.settings.tmdb_status_disabled', user_id)}\n" + + t("commands.settings.tmdb_language", user_id, language=self.config.tmdb.language) + "\n\n" + + t("commands.settings.paths_section", user_id) + "\n" + + t("commands.settings.path_movies", user_id, path=str(self.config.paths.movies)) + "\n" + + t("commands.settings.path_tv", user_id, path=str(self.config.paths.tv)) + "\n" + + t("commands.settings.path_temp", user_id, path=str(self.config.paths.temp)) + "\n\n" + + t("commands.settings.note", user_id) ) - def _get_help_text(self) -> str: + def _get_help_text(self, user_id: int = None) -> str: """Testo aiuto""" return ( - "❓ **Guida MediaButler**\n\n" - "**📥 Come usare:**\n" - "1️⃣ Invia un file video\n" - "2️⃣ Il bot riconosce il contenuto\n" - "3️⃣ Conferma o scegli tipo\n" - "4️⃣ Download automatico\n\n" - "**📝 Comandi principali:**\n" - "• `/menu` - Menu interattivo\n" - "• `/status` - Stato rapido\n" - "• `/downloads` - Download attivi\n" - "• `/space` - Spazio disco\n" - "• `/cancel` - Cancella download\n" - "• `/help` - Questo aiuto\n\n" - "**📁 Organizzazione:**\n" - "• Film: `/movies/Nome (Anno)/`\n" - "• Serie: `/tv/Serie/Season XX/`\n\n" - "**💡 Suggerimenti:**\n" - "• Nomi descrittivi = migliori risultati\n" - "• Max 10GB per file\n" - "• I download riprendono dopo riavvio\n\n" - "Per assistenza, contatta l'admin." + t("commands.help.title", user_id) + "\n\n" + + t("commands.help.usage", user_id) + "\n" + + t("commands.help.step1", user_id) + "\n" + + t("commands.help.step2", user_id) + "\n" + + t("commands.help.step3", user_id) + "\n" + + t("commands.help.step4", user_id) + "\n\n" + + t("commands.help.commands", user_id) + "\n" + + t("commands.help.cmd_menu", user_id) + "\n" + + t("commands.help.cmd_status", user_id) + "\n" + + t("commands.help.cmd_downloads", user_id) + "\n" + + t("commands.help.cmd_space", user_id) + "\n" + + t("commands.help.cmd_cancel", user_id) + "\n" + + t("commands.help.cmd_help", user_id) + "\n\n" + + t("commands.help.organization", user_id) + "\n" + + t("commands.help.org_movies", user_id) + "\n" + + t("commands.help.org_tv", user_id) + "\n\n" + + t("commands.help.tips", user_id) + "\n" + + t("commands.help.tip1", user_id) + "\n" + + t("commands.help.tip2", user_id) + "\n" + + t("commands.help.tip3", user_id) + "\n\n" + + t("commands.help.support", user_id) ) - def _get_users_text(self) -> str: + def _get_users_text(self, user_id: int = None) -> str: """Testo gestione utenti""" users = self.auth.get_authorized_users() admin_id = self.auth.get_admin_id() - text = f"👥 **Utenti Autorizzati ({len(users)})**\n\n" + text = t("commands.users.title", user_id, count=len(users)) + "\n\n" - for idx, user_id in enumerate(users, 1): - is_admin = " 👑 Admin" if user_id == admin_id else "" - text += f"**{idx}.** `{user_id}`{is_admin}\n" + for idx, user_id_item in enumerate(users, 1): + is_admin = t("commands.users.admin_marker", user_id) if user_id_item == admin_id else "" + text += f"**{idx}.** `{user_id_item}`{is_admin}\n" - text += ( - "\n📝 **Per modificare:**\n" - "1. Modifica `AUTHORIZED_USERS` in `.env`\n" - "2. Riavvia il bot\n\n" - "Il primo utente è sempre admin." - ) + text += "\n" + t("commands.users.modify_instructions", user_id) return text - def _get_cancel_confirmation(self) -> str: + def _get_cancel_confirmation(self, user_id: int = None) -> str: """Testo conferma cancellazione""" active = len(self.downloads.get_active_downloads()) queued = self.downloads.get_queued_count() @@ -658,17 +674,10 @@ def _get_cancel_confirmation(self) -> str: total = active + queued + waiting if total == 0: - return "✅ **Nessun download da cancellare**" + return t("confirmations.cancel_all.no_downloads", user_id) - return ( - f"⚠️ **Conferma Cancellazione**\n\n" - f"Stai per cancellare:\n" - f"• Download attivi: {active}\n" - f"• In coda: {queued}\n" - f"• In attesa: {waiting}\n\n" - f"**Totale: {total} operazioni**\n\n" - f"Sei sicuro?" - ) + return t("confirmations.cancel_all.message", user_id, + active=active, queued=queued, total=total) async def subtitles_handler(self, event): """Handler comando /subtitles""" @@ -676,8 +685,8 @@ async def subtitles_handler(self, event): return await event.reply( - self._get_subtitle_status(), - buttons=self._create_subtitle_menu() + self._get_subtitle_status(event.sender_id), + buttons=self._create_subtitle_menu(event.sender_id) ) async def subtitle_toggle_handler(self, event): @@ -685,13 +694,9 @@ async def subtitle_toggle_handler(self, event): if not await self.auth.check_authorized(event): return + user_id = event.sender_id await event.reply( - "⚙️ **Configurazione Sottotitoli**\n\n" - "Per modificare le impostazioni sottotitoli, aggiorna il file .env:\n\n" - "• `SUBTITLE_ENABLED=true/false`\n" - "• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n" - "• `SUBTITLE_LANGUAGES=it,en`\n\n" - "Riavvia il bot per applicare le modifiche." + t("commands.subtitles.config.toggle_instructions", user_id) ) async def subtitle_auto_handler(self, event): @@ -699,83 +704,120 @@ async def subtitle_auto_handler(self, event): if not await self.auth.check_authorized(event): return + user_id = event.sender_id await event.reply( - "⚙️ **Download Automatico Sottotitoli**\n\n" - "Per abilitare/disabilitare il download automatico, " - "modifica `SUBTITLE_AUTO_DOWNLOAD=true/false` nel file .env\n\n" - "Riavvia il bot per applicare le modifiche." + t("commands.subtitles.config.auto_instructions", user_id) ) async def subtitle_callback_handler(self, event): """Handler callback bottoni sottotitoli""" if not await self.auth.check_authorized(event): - await event.answer("❌ Non autorizzato") + await event.answer(t("messages.unauthorized", event.sender_id)) return try: + user_id = event.sender_id data = event.data.decode('utf-8') if data == "sub_status": await event.edit( - self._get_subtitle_status(), - buttons=self._create_subtitle_menu() + self._get_subtitle_status(user_id), + buttons=self._create_subtitle_menu(user_id) ) elif data == "sub_config": await event.edit( - "⚙️ **Configurazione Sottotitoli**\n\n" - "Per modificare le impostazioni, edita il file .env:\n\n" - "• `SUBTITLE_ENABLED=true/false`\n" - "• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n" - "• `SUBTITLE_LANGUAGES=it,en,es`\n" - "• `OPENSUBTITLES_USERNAME=username`\n" - "• `OPENSUBTITLES_PASSWORD=password`\n\n" - "Riavvia il bot per applicare le modifiche.", - buttons=[[Button.inline("🔙 Indietro", "sub_status")]] + t("commands.subtitles.config.full_instructions", user_id), + buttons=[[Button.inline(t("buttons.back", user_id), "sub_status")]] ) elif data == "sub_back_main": - user_id = event.sender_id is_admin = self.auth.is_admin(user_id) await event.edit( - "🎬 **MediaButler - Menu Principale**\n\n" - "Seleziona un'opzione:", - buttons=self._create_main_menu(is_admin) + t("commands.menu.title", user_id), + buttons=self._create_main_menu(is_admin, user_id) ) await event.answer() except Exception as e: self.logger.error(f"Errore callback sottotitoli: {e}") - await event.answer("❌ Errore") + await event.answer(t("errors.generic", event.sender_id)) - def _get_subtitle_status(self) -> str: + def _get_subtitle_status(self, user_id: int = None) -> str: """Ottieni stato sistema sottotitoli""" config = self.config.subtitles - status_icon = "✅" if config.enabled else "❌" - auto_icon = "✅" if config.auto_download else "❌" - auth_icon = "✅" if config.is_opensubtitles_configured else "❌" + status_text = t("commands.subtitles.status_enabled", user_id) if config.enabled else t("commands.subtitles.status_disabled", user_id) + auto_text = t("commands.subtitles.auto_enabled", user_id) if config.auto_download else t("commands.subtitles.auto_disabled", user_id) + opensubtitles_text = t("commands.subtitles.opensubtitles_configured", user_id) if config.is_opensubtitles_configured else t("commands.subtitles.opensubtitles_missing", user_id) return ( - f"📝 **Stato Sottotitoli**\n\n" - f"{status_icon} Sistema attivo: **{'Sì' if config.enabled else 'No'}**\n" - f"{auto_icon} Download automatico: **{'Sì' if config.auto_download else 'No'}**\n" - f"🌍 Lingue: **{', '.join(config.languages)}**\n" - f"{auth_icon} OpenSubtitles configurato: **{'Sì' if config.is_opensubtitles_configured else 'No'}**\n" - f"📄 Formato preferito: **{config.preferred_format}**\n\n" - f"User Agent: `{config.opensubtitles_user_agent}`" + t("commands.subtitles.title", user_id) + "\n\n" + + status_text + "\n" + + auto_text + "\n" + + t("commands.subtitles.languages", user_id, languages=', '.join(config.languages)) + "\n" + + t("commands.subtitles.format", user_id, format=config.preferred_format) + "\n" + + opensubtitles_text + "\n\n" + + f"User Agent: `{config.opensubtitles_user_agent}`" + "\n\n" + + t("commands.subtitles.note", user_id) ) - def _create_subtitle_menu(self): + def _create_subtitle_menu(self, user_id: int = None): """Crea menu sottotitoli""" return [ [ - Button.inline("🔄 Aggiorna", "sub_status"), - Button.inline("⚙️ Configurazione", "sub_config") + Button.inline(t("commands.subtitles.menu_buttons.refresh", user_id), "sub_status"), + Button.inline(t("commands.subtitles.menu_buttons.config", user_id), "sub_config") ], [ - Button.inline("🔙 Menu Principale", "sub_back_main") + Button.inline(t("buttons.back", user_id), "menu_back") ] ] + + async def language_handler(self, event: events.NewMessage.Event): + """Handler /language""" + if not await self.auth.check_authorized(event): + return + + user_id = event.sender_id + current_lang = self.i18n.get_locale_info(self.i18n.get_user_locale(user_id)) + + title = t("commands.language.title", user_id) + current_info = t("commands.language.current", user_id, + language=f"{current_lang.emoji} {current_lang.name}") + + await event.reply( + f"{title}\n\n{current_info}", + buttons=self.i18n.create_language_menu_buttons(user_id) + ) + + async def language_callback_handler(self, event: events.CallbackQuery.Event): + """Handler callback selezione lingua""" + if not await self.auth.check_authorized(event): + return + + # Estrai codice lingua dal callback data + lang_code = event.data.decode().replace('lang_', '') + user_id = event.sender_id + + if lang_code in self.i18n.SUPPORTED_LOCALES: + # Imposta nuova lingua per l'utente + self.i18n.set_user_locale(user_id, lang_code) + + # Ottieni info lingua + lang_info = self.i18n.get_locale_info(lang_code) + + # Messaggio di conferma + success_msg = t("commands.language.changed", user_id, + language=f"{lang_info.emoji} {lang_info.name}") + + await event.edit( + success_msg, + buttons=[[Button.inline(t("buttons.menu", user_id), "menu_back")]] + ) + else: + await event.answer("❌ Lingua non supportata") + + await event.answer() \ No newline at end of file diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..b4ed949 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,195 @@ +{ + "app": { + "name": "MediaButler", + "description": "Media Organizer", + "tagline": "Telegram Bot for media organization" + }, + "commands": { + "start": { + "welcome": "🎬 **MediaButler - Media Organizer**\n\nWelcome! {role}\nID: `{user_id}`\n\n**📊 System Status:**\n• 💾 Space: {total_free} GB free\n• 📥 Active: {active} downloads\n• ⏳ Queued: {queued} files\n• {tmdb_emoji} {tmdb_status}\n\n**📤 To start:** Send a video file\n\n{commands_list}\n\n**💡 Use the menu below for easy navigation!**", + "role_admin": "👑 Administrator", + "role_user": "👤 User", + "commands_rapid": "**📝 Quick commands:**\n`/status` - System status\n`/downloads` - Active downloads\n`/space` - Disk space\n`/menu` - Full menu\n`/help` - Help", + "commands_admin": "\n`/users` - User management\n`/stop` - Stop bot" + }, + "menu": { + "title": "🎬 **MediaButler - Main Menu**\n\nSelect an option:", + "main_buttons": { + "status": "📊 Status", + "space": "💾 Space", + "downloads": "📥 Downloads", + "waiting": "⏳ Waiting", + "subtitles": "📝 Subtitles", + "settings": "⚙️ Settings", + "help": "❓ Help", + "cancel_all": "❌ Cancel All", + "users": "👥 Users", + "stop": "🛑 Stop Bot", + "language": "🌍 Language" + }, + "quick_buttons": { + "status": "📊 Status", + "downloads": "📥 Downloads", + "full_menu": "📱 Full Menu" + } + }, + "status": { + "title": "📊 **System Status**", + "downloads_active": "**📥 Active downloads ({count}):**", + "downloads_none": "📭 No active downloads", + "downloads_more": " ...and {count} more", + "queue_pending": "⏳ **Queued:** {count} files", + "queue_waiting_space": "⏸️ **Waiting for space:** {count} files", + "disk_space": "💾 **Space:**" + }, + "downloads": { + "title": "📥 **Active Downloads**", + "none": "📭 No active downloads at the moment.", + "active_count": "**Active ({count}):**", + "item": "• `{filename}`\n {progress}% - {speed} MB/s - ETA: {eta}", + "item_simple": "• `{filename}`\n {progress}% - {speed} MB/s" + }, + "waiting": { + "title": "⏳ **Files waiting for space**", + "none": "✅ **No files waiting**\n\nAll downloads have sufficient space.", + "space_waiting": "**💾 Waiting for space ({count}):**", + "queue_pending": "**⏳ Queued ({count}):**", + "item": "• `{filename}` ({size})", + "note": "\n💡 Files will resume automatically when possible.", + "more": "...and {count} more files" + }, + "space": { + "title": "💾 **Disk Space**", + "status_ok": "✅", + "status_warning": "⚠️", + "status_critical": "🔴", + "item": "{emoji} {name}: {free} GB free ({used} GB used / {total} GB total)" + }, + "settings": { + "title": "⚙️ **Current Settings**", + "downloads": "**Downloads:**", + "concurrent": "• Concurrent: {count}", + "max_size": "• Max size: {size} GB", + "space_section": "**Space:**", + "min_free": "• Minimum reserved: {size} GB", + "warning_threshold": "• Warning threshold: {size} GB", + "check_interval": "• Check every: {seconds}s", + "tmdb_section": "**TMDB:**", + "tmdb_status_active": "✅ Active", + "tmdb_status_disabled": "❌ Not configured", + "tmdb_language": "• Language: {language}", + "paths_section": "**Paths:**", + "path_movies": "• Movies: `{path}`", + "path_tv": "• TV Shows: `{path}`", + "path_temp": "• Temp: `{path}`", + "note": "ℹ️ Edit `.env` to change." + }, + "help": { + "title": "❓ **MediaButler Guide**", + "usage": "**📥 How to use:**", + "step1": "1️⃣ Send a video file", + "step2": "2️⃣ Bot recognizes content", + "step3": "3️⃣ Confirm or choose type", + "step4": "4️⃣ Automatic download", + "commands": "**📝 Main commands:**", + "cmd_menu": "• `/menu` - Interactive menu", + "cmd_status": "• `/status` - Quick status", + "cmd_downloads": "• `/downloads` - Active downloads", + "cmd_space": "• `/space` - Disk space", + "cmd_cancel": "• `/cancel` - Cancel download", + "cmd_help": "• `/help` - This help", + "organization": "**📁 Organization:**", + "org_movies": "• Movies: `/movies/Name (Year)/`", + "org_tv": "• TV Shows: `/tv/Series/Season XX/`", + "tips": "**💡 Tips:**", + "tip1": "• Descriptive names = better results", + "tip2": "• Max 10GB per file", + "tip3": "• Downloads resume after restart", + "support": "For support, contact the admin." + }, + "users": { + "title": "👥 **Authorized Users ({count})**", + "admin_marker": " 👑 Admin", + "modify_instructions": "📝 **To modify:**\n1. Edit `AUTHORIZED_USERS` in `.env`\n2. Restart the bot\n\nThe first user is always admin." + }, + "subtitles": { + "title": "📝 **Subtitles System**", + "status_enabled": "✅ Enabled", + "status_disabled": "❌ Disabled", + "auto_enabled": "✅ Automatic download enabled", + "auto_disabled": "❌ Automatic download disabled", + "languages": "• Languages: {languages}", + "format": "• Preferred format: {format}", + "opensubtitles_configured": "✅ OpenSubtitles configured", + "opensubtitles_missing": "⚠️ OpenSubtitles not configured", + "note": "ℹ️ Configure in `.env` to enable.", + "menu_buttons": { + "refresh": "🔄 Refresh", + "config": "⚙️ Configuration" + }, + "config": { + "toggle_instructions": "⚙️ **Subtitles Configuration**\n\nTo modify subtitle settings, update the .env file:\n\n• `SUBTITLE_ENABLED=true/false`\n• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n• `SUBTITLE_LANGUAGES=it,en`\n\nRestart the bot to apply changes.", + "auto_instructions": "⚙️ **Automatic Subtitle Download**\n\nTo enable/disable automatic download, modify `SUBTITLE_AUTO_DOWNLOAD=true/false` in the .env file\n\nRestart the bot to apply changes.", + "full_instructions": "⚙️ **Subtitles Configuration**\n\nTo modify settings, edit the .env file:\n\n• `SUBTITLE_ENABLED=true/false`\n• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n• `SUBTITLE_LANGUAGES=it,en,es`\n• `OPENSUBTITLES_USERNAME=username`\n• `OPENSUBTITLES_PASSWORD=password`\n\nRestart the bot to apply changes." + } + }, + "language": { + "title": "🌍 **Language Selection**\n\nChoose your preferred language:", + "current": "Current language: {language}", + "changed": "✅ Language changed to **{language}**!" + } + }, + "buttons": { + "back": "🔙 Back", + "menu": "📱 Menu", + "refresh": "🔄 Refresh", + "confirm": "✅ Confirm", + "cancel": "❌ Cancel", + "yes": "✅ Yes", + "no": "❌ No" + }, + "messages": { + "unauthorized": "❌ You are not authorized to use this bot.", + "admin_only": "❌ Only administrators can execute this command.", + "admin_only_action": "❌ Administrators only", + "tmdb_active": "TMDB Active", + "tmdb_disabled": "TMDB Not configured", + "send_video": "📤 To start: Send a video file" + }, + "confirmations": { + "cancel_all": { + "title": "❌ **Confirm Cancellation**", + "message": "⚠️ Do you want to cancel **ALL** downloads?\n\n• {active} active downloads\n• {queued} queued files\n\nThis action cannot be undone!", + "no_downloads": "✅ No downloads to cancel." + }, + "stop_bot": { + "title": "🛑 **Confirm Bot Stop**", + "message": "⚠️ This action will:\n• Cancel all downloads\n• Stop the bot\n• Require manual restart\n\nConfirm?", + "button_confirm": "✅ Confirm Stop" + } + }, + "errors": { + "generic": "❌ An error occurred.", + "not_found": "❌ Not found.", + "unauthorized": "❌ Unauthorized.", + "invalid_command": "❌ Invalid command.", + "file_too_large": "❌ File too large (max {max_size} GB).", + "no_space": "❌ Insufficient space.", + "download_failed": "❌ Download failed: {error}" + }, + "success": { + "download_started": "✅ Download started: `{filename}`", + "download_completed": "✅ Download completed: `{filename}`", + "download_cancelled": "✅ Download cancelled: `{filename}`", + "all_downloads_cancelled": "✅ All downloads have been cancelled.", + "bot_stopping": "🛑 Bot shutting down...", + "settings_updated": "✅ Settings updated." + }, + "time": { + "seconds": "{count}s", + "minutes": "{count}m", + "hours": "{count}h", + "days": "{count}d", + "unknown": "N/A" + } +} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json new file mode 100644 index 0000000..b22384f --- /dev/null +++ b/locales/it.json @@ -0,0 +1,195 @@ +{ + "app": { + "name": "MediaButler", + "description": "Organizzatore Media", + "tagline": "Bot Telegram per organizzazione media" + }, + "commands": { + "start": { + "welcome": "🎬 **MediaButler - Organizzatore Media**\n\nBenvenuto! {role}\nID: `{user_id}`\n\n**📊 Stato Sistema:**\n• 💾 Spazio: {total_free} GB liberi\n• 📥 Attivi: {active} download\n• ⏳ In coda: {queued} file\n• {tmdb_emoji} {tmdb_status}\n\n**📤 Per iniziare:** Invia un file video\n\n{commands_list}\n\n**💡 Usa il menu sotto per navigare facilmente!**", + "role_admin": "👑 Amministratore", + "role_user": "👤 Utente", + "commands_rapid": "**📝 Comandi rapidi:**\n`/status` - Stato sistema\n`/downloads` - Download attivi\n`/space` - Spazio disco\n`/menu` - Menu completo\n`/help` - Aiuto", + "commands_admin": "\n`/users` - Gestione utenti\n`/stop` - Arresta bot" + }, + "menu": { + "title": "🎬 **MediaButler - Menu Principale**\n\nSeleziona un'opzione:", + "main_buttons": { + "status": "📊 Stato", + "space": "💾 Spazio", + "downloads": "📥 Downloads", + "waiting": "⏳ In Attesa", + "subtitles": "📝 Sottotitoli", + "settings": "⚙️ Impostazioni", + "help": "❓ Aiuto", + "cancel_all": "❌ Cancella Tutto", + "users": "👥 Utenti", + "stop": "🛑 Stop Bot", + "language": "🌍 Lingua" + }, + "quick_buttons": { + "status": "📊 Stato", + "downloads": "📥 Downloads", + "full_menu": "📱 Menu Completo" + } + }, + "status": { + "title": "📊 **Stato Sistema**", + "downloads_active": "**📥 Download attivi ({count}):**", + "downloads_none": "📭 Nessun download attivo", + "downloads_more": " ...e altri {count}", + "queue_pending": "⏳ **In coda:** {count} file", + "queue_waiting_space": "⏸️ **In attesa spazio:** {count} file", + "disk_space": "💾 **Spazio:**" + }, + "downloads": { + "title": "📥 **Download Attivi**", + "none": "📭 Nessun download attivo al momento.", + "active_count": "**Attivi ({count}):**", + "item": "• `{filename}`\n {progress}% - {speed} MB/s - ETA: {eta}", + "item_simple": "• `{filename}`\n {progress}% - {speed} MB/s" + }, + "waiting": { + "title": "⏳ **File in attesa di spazio**", + "none": "✅ **Nessun file in attesa**\n\nTutti i download hanno spazio sufficiente.", + "space_waiting": "**💾 In attesa spazio ({count}):**", + "queue_pending": "**⏳ In coda ({count}):**", + "item": "• `{filename}` ({size})", + "note": "\n💡 I file riprenderanno automaticamente quando possibile.", + "more": "...e altri {count} file" + }, + "space": { + "title": "💾 **Spazio Disco**", + "status_ok": "✅", + "status_warning": "⚠️", + "status_critical": "🔴", + "item": "{emoji} {name}: {free} GB liberi ({used} GB usati / {total} GB totali)" + }, + "settings": { + "title": "⚙️ **Impostazioni Correnti**", + "downloads": "**Download:**", + "concurrent": "• Simultanei: {count}", + "max_size": "• Max dimensione: {size} GB", + "space_section": "**Spazio:**", + "min_free": "• Minimo riservato: {size} GB", + "warning_threshold": "• Soglia avviso: {size} GB", + "check_interval": "• Controllo ogni: {seconds}s", + "tmdb_section": "**TMDB:**", + "tmdb_status_active": "✅ Attivo", + "tmdb_status_disabled": "❌ Non configurato", + "tmdb_language": "• Lingua: {language}", + "paths_section": "**Percorsi:**", + "path_movies": "• Film: `{path}`", + "path_tv": "• Serie: `{path}`", + "path_temp": "• Temp: `{path}`", + "note": "ℹ️ Modifica `.env` per cambiare." + }, + "help": { + "title": "❓ **Guida MediaButler**", + "usage": "**📥 Come usare:**", + "step1": "1️⃣ Invia un file video", + "step2": "2️⃣ Il bot riconosce il contenuto", + "step3": "3️⃣ Conferma o scegli tipo", + "step4": "4️⃣ Download automatico", + "commands": "**📝 Comandi principali:**", + "cmd_menu": "• `/menu` - Menu interattivo", + "cmd_status": "• `/status` - Stato rapido", + "cmd_downloads": "• `/downloads` - Download attivi", + "cmd_space": "• `/space` - Spazio disco", + "cmd_cancel": "• `/cancel` - Cancella download", + "cmd_help": "• `/help` - Questo aiuto", + "organization": "**📁 Organizzazione:**", + "org_movies": "• Film: `/movies/Nome (Anno)/`", + "org_tv": "• Serie: `/tv/Serie/Season XX/`", + "tips": "**💡 Suggerimenti:**", + "tip1": "• Nomi descrittivi = migliori risultati", + "tip2": "• Max 10GB per file", + "tip3": "• I download riprendono dopo riavvio", + "support": "Per assistenza, contatta l'admin." + }, + "users": { + "title": "👥 **Utenti Autorizzati ({count})**", + "admin_marker": " 👑 Admin", + "modify_instructions": "📝 **Per modificare:**\n1. Modifica `AUTHORIZED_USERS` in `.env`\n2. Riavvia il bot\n\nIl primo utente è sempre admin." + }, + "subtitles": { + "title": "📝 **Sistema Sottotitoli**", + "status_enabled": "✅ Abilitato", + "status_disabled": "❌ Disabilitato", + "auto_enabled": "✅ Download automatico attivo", + "auto_disabled": "❌ Download automatico disattivo", + "languages": "• Lingue: {languages}", + "format": "• Formato preferito: {format}", + "opensubtitles_configured": "✅ OpenSubtitles configurato", + "opensubtitles_missing": "⚠️ OpenSubtitles non configurato", + "note": "ℹ️ Configura in `.env` per abilitare.", + "menu_buttons": { + "refresh": "🔄 Aggiorna", + "config": "⚙️ Configurazione" + }, + "config": { + "toggle_instructions": "⚙️ **Configurazione Sottotitoli**\n\nPer modificare le impostazioni sottotitoli, aggiorna il file .env:\n\n• `SUBTITLE_ENABLED=true/false`\n• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n• `SUBTITLE_LANGUAGES=it,en`\n\nRiavvia il bot per applicare le modifiche.", + "auto_instructions": "⚙️ **Download Automatico Sottotitoli**\n\nPer abilitare/disabilitare il download automatico, modifica `SUBTITLE_AUTO_DOWNLOAD=true/false` nel file .env\n\nRiavvia il bot per applicare le modifiche.", + "full_instructions": "⚙️ **Configurazione Sottotitoli**\n\nPer modificare le impostazioni, edita il file .env:\n\n• `SUBTITLE_ENABLED=true/false`\n• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n• `SUBTITLE_LANGUAGES=it,en,es`\n• `OPENSUBTITLES_USERNAME=username`\n• `OPENSUBTITLES_PASSWORD=password`\n\nRiavvia il bot per applicare le modifiche." + } + }, + "language": { + "title": "🌍 **Selezione Lingua**\n\nScegli la tua lingua preferita:", + "current": "Lingua corrente: {language}", + "changed": "✅ Lingua cambiata in **{language}**!" + } + }, + "buttons": { + "back": "🔙 Indietro", + "menu": "📱 Menu", + "refresh": "🔄 Aggiorna", + "confirm": "✅ Conferma", + "cancel": "❌ Annulla", + "yes": "✅ Sì", + "no": "❌ No" + }, + "messages": { + "unauthorized": "❌ Non sei autorizzato ad usare questo bot.", + "admin_only": "❌ Solo amministratori possono eseguire questo comando.", + "admin_only_action": "❌ Solo amministratori", + "tmdb_active": "TMDB Attivo", + "tmdb_disabled": "TMDB Non configurato", + "send_video": "📤 Per iniziare: Invia un file video" + }, + "confirmations": { + "cancel_all": { + "title": "❌ **Conferma Cancellazione**", + "message": "⚠️ Vuoi cancellare **TUTTI** i download?\n\n• {active} download attivi\n• {queued} file in coda\n\nQuesta azione non può essere annullata!", + "no_downloads": "✅ Nessun download da cancellare." + }, + "stop_bot": { + "title": "🛑 **Conferma Arresto Bot**", + "message": "⚠️ Questa azione:\n• Cancellerà tutti i download\n• Fermerà il bot\n• Richiederà riavvio manuale\n\nConfermi?", + "button_confirm": "✅ Conferma Arresto" + } + }, + "errors": { + "generic": "❌ Si è verificato un errore.", + "not_found": "❌ Non trovato.", + "unauthorized": "❌ Non autorizzato.", + "invalid_command": "❌ Comando non valido.", + "file_too_large": "❌ File troppo grande (max {max_size} GB).", + "no_space": "❌ Spazio insufficiente.", + "download_failed": "❌ Download fallito: {error}" + }, + "success": { + "download_started": "✅ Download avviato: `{filename}`", + "download_completed": "✅ Download completato: `{filename}`", + "download_cancelled": "✅ Download cancellato: `{filename}`", + "all_downloads_cancelled": "✅ Tutti i download sono stati cancellati.", + "bot_stopping": "🛑 Bot in arresto...", + "settings_updated": "✅ Impostazioni aggiornate." + }, + "time": { + "seconds": "{count}s", + "minutes": "{count}m", + "hours": "{count}h", + "days": "{count}g", + "unknown": "N/D" + } +} \ No newline at end of file diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 0000000..8e8c828 --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,195 @@ +{ + "app": { + "name": "MediaButler", + "description": "媒体管理器", + "tagline": "Telegram 媒体整理机器人" + }, + "commands": { + "start": { + "welcome": "🎬 **MediaButler - 媒体管理器**\n\n欢迎!{role}\nID: `{user_id}`\n\n**📊 系统状态:**\n• 💾 空间:{total_free} GB 可用\n• 📥 活跃:{active} 个下载\n• ⏳ 队列:{queued} 个文件\n• {tmdb_emoji} {tmdb_status}\n\n**📤 开始使用:** 发送视频文件\n\n{commands_list}\n\n**💡 使用下方菜单轻松导航!**", + "role_admin": "👑 管理员", + "role_user": "👤 用户", + "commands_rapid": "**📝 快速命令:**\n`/status` - 系统状态\n`/downloads` - 活跃下载\n`/space` - 磁盘空间\n`/menu` - 完整菜单\n`/help` - 帮助", + "commands_admin": "\n`/users` - 用户管理\n`/stop` - 停止机器人" + }, + "menu": { + "title": "🎬 **MediaButler - 主菜单**\n\n选择一个选项:", + "main_buttons": { + "status": "📊 状态", + "space": "💾 空间", + "downloads": "📥 下载", + "waiting": "⏳ 等待中", + "subtitles": "📝 字幕", + "settings": "⚙️ 设置", + "help": "❓ 帮助", + "cancel_all": "❌ 取消全部", + "users": "👥 用户", + "stop": "🛑 停止机器人", + "language": "🌍 语言" + }, + "quick_buttons": { + "status": "📊 状态", + "downloads": "📥 下载", + "full_menu": "📱 完整菜单" + } + }, + "status": { + "title": "📊 **系统状态**", + "downloads_active": "**📥 活跃下载 ({count}):**", + "downloads_none": "📭 没有活跃下载", + "downloads_more": " ...还有 {count} 个", + "queue_pending": "⏳ **队列中:** {count} 个文件", + "queue_waiting_space": "⏸️ **等待空间:** {count} 个文件", + "disk_space": "💾 **空间:**" + }, + "downloads": { + "title": "📥 **活跃下载**", + "none": "📭 当前没有活跃下载。", + "active_count": "**活跃 ({count}):**", + "item": "• `{filename}`\n {progress}% - {speed} MB/s - 剩余:{eta}", + "item_simple": "• `{filename}`\n {progress}% - {speed} MB/s" + }, + "waiting": { + "title": "⏳ **等待空间的文件**", + "none": "✅ **没有等待的文件**\n\n所有下载都有足够的空间。", + "space_waiting": "**💾 等待空间 ({count}):**", + "queue_pending": "**⏳ 队列中 ({count}):**", + "item": "• `{filename}` ({size})", + "note": "\n💡 文件将在可能时自动恢复。", + "more": "...还有 {count} 个文件" + }, + "space": { + "title": "💾 **磁盘空间**", + "status_ok": "✅", + "status_warning": "⚠️", + "status_critical": "🔴", + "item": "{emoji} {name}:{free} GB 可用 ({used} GB 已用 / {total} GB 总计)" + }, + "settings": { + "title": "⚙️ **当前设置**", + "downloads": "**下载:**", + "concurrent": "• 并发数:{count}", + "max_size": "• 最大大小:{size} GB", + "space_section": "**空间:**", + "min_free": "• 最小保留:{size} GB", + "warning_threshold": "• 警告阈值:{size} GB", + "check_interval": "• 检查间隔:{seconds}秒", + "tmdb_section": "**TMDB:**", + "tmdb_status_active": "✅ 已启用", + "tmdb_status_disabled": "❌ 未配置", + "tmdb_language": "• 语言:{language}", + "paths_section": "**路径:**", + "path_movies": "• 电影:`{path}`", + "path_tv": "• 电视剧:`{path}`", + "path_temp": "• 临时:`{path}`", + "note": "ℹ️ 编辑 `.env` 来更改。" + }, + "help": { + "title": "❓ **MediaButler 指南**", + "usage": "**📥 使用方法:**", + "step1": "1️⃣ 发送视频文件", + "step2": "2️⃣ 机器人识别内容", + "step3": "3️⃣ 确认或选择类型", + "step4": "4️⃣ 自动下载", + "commands": "**📝 主要命令:**", + "cmd_menu": "• `/menu` - 交互式菜单", + "cmd_status": "• `/status` - 快速状态", + "cmd_downloads": "• `/downloads` - 活跃下载", + "cmd_space": "• `/space` - 磁盘空间", + "cmd_cancel": "• `/cancel` - 取消下载", + "cmd_help": "• `/help` - 此帮助", + "organization": "**📁 组织结构:**", + "org_movies": "• 电影:`/movies/名称 (年份)/`", + "org_tv": "• 电视剧:`/tv/系列/Season XX/`", + "tips": "**💡 提示:**", + "tip1": "• 描述性名称 = 更好的结果", + "tip2": "• 每个文件最大 10GB", + "tip3": "• 重启后下载会恢复", + "support": "如需支持,请联系管理员。" + }, + "users": { + "title": "👥 **授权用户 ({count})**", + "admin_marker": " 👑 管理员", + "modify_instructions": "📝 **修改方法:**\n1. 在 `.env` 中编辑 `AUTHORIZED_USERS`\n2. 重启机器人\n\n第一个用户总是管理员。" + }, + "subtitles": { + "title": "📝 **字幕系统**", + "status_enabled": "✅ 已启用", + "status_disabled": "❌ 已禁用", + "auto_enabled": "✅ 自动下载已启用", + "auto_disabled": "❌ 自动下载已禁用", + "languages": "• 语言:{languages}", + "format": "• 首选格式:{format}", + "opensubtitles_configured": "✅ OpenSubtitles 已配置", + "opensubtitles_missing": "⚠️ OpenSubtitles 未配置", + "note": "ℹ️ 在 `.env` 中配置以启用。", + "menu_buttons": { + "refresh": "🔄 刷新", + "config": "⚙️ 配置" + }, + "config": { + "toggle_instructions": "⚙️ **字幕配置**\n\n要修改字幕设置,请更新 .env 文件:\n\n• `SUBTITLE_ENABLED=true/false`\n• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n• `SUBTITLE_LANGUAGES=it,en`\n\n重启机器人以应用更改。", + "auto_instructions": "⚙️ **自动字幕下载**\n\n要启用/禁用自动下载,请在 .env 文件中修改 `SUBTITLE_AUTO_DOWNLOAD=true/false`\n\n重启机器人以应用更改。", + "full_instructions": "⚙️ **字幕配置**\n\n要修改设置,请编辑 .env 文件:\n\n• `SUBTITLE_ENABLED=true/false`\n• `SUBTITLE_AUTO_DOWNLOAD=true/false`\n• `SUBTITLE_LANGUAGES=it,en,es`\n• `OPENSUBTITLES_USERNAME=username`\n• `OPENSUBTITLES_PASSWORD=password`\n\n重启机器人以应用更改。" + } + }, + "language": { + "title": "🌍 **语言选择**\n\n选择您的首选语言:", + "current": "当前语言:{language}", + "changed": "✅ 语言已更改为 **{language}**!" + } + }, + "buttons": { + "back": "🔙 返回", + "menu": "📱 菜单", + "refresh": "🔄 刷新", + "confirm": "✅ 确认", + "cancel": "❌ 取消", + "yes": "✅ 是", + "no": "❌ 否" + }, + "messages": { + "unauthorized": "❌ 您无权使用此机器人。", + "admin_only": "❌ 只有管理员可以执行此命令。", + "admin_only_action": "❌ 仅限管理员", + "tmdb_active": "TMDB 已启用", + "tmdb_disabled": "TMDB 未配置", + "send_video": "📤 开始使用:发送视频文件" + }, + "confirmations": { + "cancel_all": { + "title": "❌ **确认取消**", + "message": "⚠️ 您要取消 **所有** 下载吗?\n\n• {active} 个活跃下载\n• {queued} 个队列文件\n\n此操作无法撤销!", + "no_downloads": "✅ 没有要取消的下载。" + }, + "stop_bot": { + "title": "🛑 **确认停止机器人**", + "message": "⚠️ 此操作将:\n• 取消所有下载\n• 停止机器人\n• 需要手动重启\n\n确认吗?", + "button_confirm": "✅ 确认停止" + } + }, + "errors": { + "generic": "❌ 发生错误。", + "not_found": "❌ 未找到。", + "unauthorized": "❌ 未授权。", + "invalid_command": "❌ 无效命令。", + "file_too_large": "❌ 文件过大(最大 {max_size} GB)。", + "no_space": "❌ 空间不足。", + "download_failed": "❌ 下载失败:{error}" + }, + "success": { + "download_started": "✅ 下载已开始:`{filename}`", + "download_completed": "✅ 下载已完成:`{filename}`", + "download_cancelled": "✅ 下载已取消:`{filename}`", + "all_downloads_cancelled": "✅ 所有下载已被取消。", + "bot_stopping": "🛑 机器人正在停止...", + "settings_updated": "✅ 设置已更新。" + }, + "time": { + "seconds": "{count}秒", + "minutes": "{count}分", + "hours": "{count}小时", + "days": "{count}天", + "unknown": "未知" + } +} \ No newline at end of file