diff --git a/.gitignore b/.gitignore index f9de277..d7fbaa2 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ coverage.xml .pytest_cache/ cover/ +.vscode/ # Translations *.mo *.pot diff --git a/README.md b/README.md index 1034461..bc8bf7f 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ +![GitHub License](https://img.shields.io/github/license/ch3p4ll3/QBittorrentBot) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/259099080ca24e029a910e3249d32041)](https://app.codacy.com/gh/ch3p4ll3/QBittorrentBot?utm_source=github.com&utm_medium=referral&utm_content=ch3p4ll3/QBittorrentBot&utm_campaign=Badge_Grade) +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ch3p4ll3/QBittorrentBot/docker-image.yml) +![Docker Pulls](https://img.shields.io/docker/pulls/ch3p4ll3/qbittorrent-bot) + + [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) @@ -13,6 +18,21 @@ magnet:?xt=... ``` You can also pause, resume, delete and add/remove and modify categories. +# Table Of Contents +- [QBittorrentBot](#qbittorrentbot) +- [Table Of Contents](#table-of-contents) + - [Warning!](#warning) + - [Configuration](#configuration) + - [Retrieve Telegram API ID and API HASH](#retrieve-telegram-api-id-and-api-hash) + - [JSON Configuration](#json-configuration) + - [Running](#running) + - [Build docker](#build-docker) + - [Running without docker](#running-without-docker) + - [Contributing Translations on Transifex](#contributing-translations-on-transifex) + - [How to enable the qBittorrent Web UI](#how-to-enable-the-qbittorrent-web-ui) + - [Contributors ✨](#contributors-) + + ## Warning! Since version V2, the mapping of the configuration file has been changed. Make sure you have modified it correctly before starting the bot @@ -68,6 +88,25 @@ Pull and run the image with: `docker run -d -v /home/user/docker/QBittorrentBot: - Create a config.json file - Start the bot with `python3 main.py` +## Contributing Translations on Transifex +QBittorrentBot is an open-source project that relies on the contributions of its community members to provide translations for its users. If you are multilingual and would like to help us make QBittorrentBot more accessible to a wider audience, you can contribute by adding new translations or improving existing ones using Transifex: + +- Visit the [QBittorrentBot Transifex Project](https://app.transifex.com/ch3p4ll3/qbittorrentbot/). + +- If you don't have a Transifex account, sign up for one. If you already have an account, log in. + +- Navigate to the "Languages" tab to view the available languages. Choose the language you want to contribute to. + +- Locate the specific string you wish to translate. Please note that the text between "${" and "}" should not be edited, as they are placeholders for dynamic content. + +- Click on the string you want to translate, enter your translation in the provided field, and save your changes. + +- If your language is not listed, you can request its addition. + +- Once you have completed your translations, submit them for review. The project maintainers will review and approve your contributions. + +Thank you for helping improve QBittorrentBot with your valuable translations! + ## How to enable the qBittorrent Web UI For the bot to work, it requires qbittorrent to have the web interface active. You can activate it by going on the menu bar, go to **Tools > Options** qBittorrent WEB UI diff --git a/config.json.template b/config.json.template index 5aa7aa0..af41c30 100644 --- a/config.json.template +++ b/config.json.template @@ -1,20 +1,28 @@ { "client": { "type": "qbittorrent", - "host": "http://192.168.178.102", + "host": "http://192.168.178.102:8080", "user": "admin", "password": "admin" }, "telegram": { "bot_token": "1111111:AAAAAAAA-BBBBBBBBB", "api_id": 1111, - "api_hash": "aaaaaaaa" + "api_hash": "aaaaaaaa", + "proxy": { + "scheme": "http", + "hostname": "myproxy.local", + "port": 8080, + "username": "admin", + "password": "admin" + } }, "users": [ { "user_id": 123456, "notify": false, + "locale": "en", "role": "administrator" } ] diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..a212b2d --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,8 @@ +version: '3.9' +services: + qbittorrent-bot: + image: 'ch3p4ll3/qbittorrent-bot:latest' + container_name: qbittorrent-bot + restart: unless-stopped + volumes: + - '/home/user/docker/QBittorrentBot:/app/config:rw' diff --git a/docs/advanced/add_new_client_manager.md b/docs/advanced/add_new_client_manager.md index 0c67cd4..f0f199e 100644 --- a/docs/advanced/add_new_client_manager.md +++ b/docs/advanced/add_new_client_manager.md @@ -37,7 +37,7 @@ class ClientTypeEnum(str, Enum): - Return to the `src/client_manager` folder and edit the `client_repo.py` file by adding to the dictionary named `repositories` an entry associating the newly created enum with the new manager. Example: ```python from ..configs.enums import ClientTypeEnum -from .qbittorrent_manager import QbittorrentManager, ClientManager +from .qbittorrent_manager import QbittorrentManager, ClientManager, UtorrentManager class ClientRepo: diff --git a/docs/advanced/telegram_proxy.md b/docs/advanced/telegram_proxy.md new file mode 100644 index 0000000..cf21e65 --- /dev/null +++ b/docs/advanced/telegram_proxy.md @@ -0,0 +1,32 @@ +# Configure proxy for Telegram + +QBittorrent Bot can be configured to use a Telegram proxy to connect to the Telegram API. This can be useful if you are behind a firewall that blocks direct connections to Telegram. + +To configure QBittorrent Bot to use a Telegram proxy, you will need to add a **proxy section** to the `config.json` file in the **telegram section**. The telegram section should have the following format: + +```json5 +"telegram": { + "bot_token": "1111111:AAAAAAAA-BBBBBBBBB", + "api_id": 1111, + "api_hash": "aaaaaaaa", + "proxy": { + "scheme": "http", // http, sock4 or sock5 + "hostname": "myproxy.local", + "port": 8080, + "username": "admin", + "password": "admin" + } +} +``` + +Where: + +- `scheme` is the protocol to use for the proxy connection. This can be `http`, `sock4` or `sock5` +- `hostname` is the hostname or IP address of the proxy server. +- `port` is the port number of the proxy server. +- `username` (optional) is the username for the proxy server. +- `password` (optional) is the password for the proxy server. + +!!! +Once you have added the proxy section to the config.json file, you will need to restart QBittorrent Bot for the changes to take effect. +!!! \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..964aba1 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,30 @@ +--- +order: -10 +--- +# Contributing +QBittorrentBot is an open-source Telegram bot that enables seamless management of qBittorrent downloads directly from Telegram. + +By contributing to QBittorrentBot, you can help improve this valuable tool for qBittorrent users. Your contributions can range from fixing bugs and enhancing existing features to adding new functionalities that enhance the bot's capabilities. + +## Adding translations +If you are multilingual and would like to help us make QBittorrentBot more accessible to a wider audience, you can contribute by adding new translations or improving existing ones using Transifex: + +- Visit the [QBittorrentBot Transifex Project](https://app.transifex.com/ch3p4ll3/qbittorrentbot/). + +- If you don't have a Transifex account, sign up for one. If you already have an account, log in. + +- Navigate to the "Languages" tab to view the available languages. Choose the language you want to contribute to. + +- Locate the specific string you wish to translate. Please note that the text between "${" and "}" should not be edited, as they are placeholders for dynamic content. + +- Click on the string you want to translate, enter your translation in the provided field, and save your changes. + +- If your language is not listed, you can request its addition. + +- Once you have completed your translations, submit them for review. The project maintainers will review and approve your contributions. + +Thank you for helping improve QBittorrentBot with your valuable translations! + + +[!ref](/advanced/add_new_client_manager.md) +[!ref](/advanced/add_entries_configuration.md) diff --git a/docs/faq.md b/docs/faq.md index 36d47ef..84f9f3c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -61,4 +61,6 @@ Please follow this guide ### How do I contribute to the development of QBittorrentBot? -QBittorrentBot is an open-source project. You can contribute to the development by reporting bugs, suggesting improvements, or submitting pull requests. The project's code is hosted on [GitHub](https://github.com/ch3p4ll3/QBittorrentBot). \ No newline at end of file +QBittorrentBot is an open-source project. You can contribute to the development by reporting bugs, suggesting improvements, or submitting pull requests. The project's code is hosted on [GitHub](https://github.com/ch3p4ll3/QBittorrentBot). + +[!ref](/contributing.md) \ No newline at end of file diff --git a/docs/getting_started/configuration_file.md b/docs/getting_started/configuration_file.md index 41d4060..3f2a665 100644 --- a/docs/getting_started/configuration_file.md +++ b/docs/getting_started/configuration_file.md @@ -32,7 +32,7 @@ Here's a brief overview of the configuration file and its key sections: - **Clients Section**: Establishes the connection details for the qBittorrent server, including the hostname, port number, username, and password. This enables the bot to interact with the qBittorrent server and manage torrents. -- **Telegram Section**: Contains the bot token, API ID, and API hash, which are essential for authenticating the bot with the Telegram API. These credentials allow the bot to communicate with the Telegram server and receive user commands. Click [here](https://docs.pyrogram.org/intro/quickstart) to find out how to retrive your API ID and API Hash +- **Telegram Section**: Contains the bot token, API ID, and API hash, which are essential for authenticating the bot with the Telegram API. These credentials allow the bot to communicate with the Telegram server and receive user commands. Click [here](https://core.telegram.org/api/obtaining_api_id) to find out how to retrive your API ID and API Hash - **Users Section**: Lists the authorized users of the QBittorrentBot, along with their Telegram user IDs, notification preferences, and user roles. This section defines the users who can interact with the bot, receive notifications, and manage torrents. @@ -40,7 +40,7 @@ Here's a brief overview of the configuration file and its key sections: This section defines the configuration for the qBittorrent client that the bot will be interacting with. -Name | Type | Value +Name | Type | Remarks --- |-----------------------------------| --- type | [ClientTypeEnum](#clienttypeenum) | The type of client. host | [HttpUrl](#httpurl) | The IP address of the qBittorrent server. @@ -51,23 +51,24 @@ password | str | The password for the qBittorrent This section defines the configuration for the Telegram bot that the QBittorrentBot will be communicating with. -Name | Type | Value ---- | --- | --- -bot_token | str | The bot token for the QBittorrentBot. This is a unique identifier that is used to authenticate the bot with the Telegram API. -api_id | int | The API ID for the QBittorrentBot. This is a unique identifier that is used to identify the bot to the Telegram API. -api_hash |str | The API hash for the QBittorrentBot. This is a string of characters that is used to verify the authenticity of the bot's requests to the Telegram API. +Name | Type | Remarks +--- | --- | --- +bot_token | str | The bot token for the QBittorrentBot. This is a unique identifier that is used to authenticate the bot with the Telegram API. +api_id | int | The API ID for the QBittorrentBot. This is a unique identifier that is used to identify the bot to the Telegram API. +api_hash | str | The API hash for the QBittorrentBot. This is a string of characters that is used to verify the authenticity of the bot's requests to the Telegram API. +proxy | [TelegramProxySettings](#telegram-proxy-settings) | Optional, the settings for using a proxy to contact telegram servers ## Users This section defines the list of users who are authorized to use the QBittorrentBot. Each user is defined by their Telegram user ID, whether or not they should be notified about completed torrents, and their role. -Name | Type | Value ---- | --- |--- -user_id | int |The Telegram user ID of the user. This is a unique identifier that is used to identify the user to the Telegram API. -notify | bool |Whether or not the user should be notified about new torrents. -role | [UserRolesEnum](#userrolesenum) |The role of the user. - +Name | Type | Remarks +--- | --- | --- +user_id | int | The Telegram user ID of the user. This is a unique identifier that is used to identify the user to the Telegram API. +notify | bool | Whether or not the user should be notified about new torrents. +role | [UserRolesEnum](#userrolesenum) | The role of the user. Default: `administrator` +locale | str | Language used by the user, [list of supported languages](#languages). Default: `en` ## Enums @@ -75,14 +76,21 @@ role | [UserRolesEnum](#userrolesenum) |The role of the user. Name | Type | Value(to be used in json) --- | --- |--- -QBittorrent | str | qbittorrent +QBittorrent | str | `qbittorrent` ### UserRolesEnum Name | Type | Value(to be used in json) | Remarks --- | --- |--- | -Reader | str | reader | Can perform only reading operations(view torrents) -Manager | str | manager | Can perform only managing operations(view torrents + can download files + can add/edit categories + set torrent priority + can stop/start downloads) -Administrator| str | administrator | Can perform all operations (Manager + remove torrent + remove category + edit configs) +Reader | str | `reader` | Can perform only reading operations(view torrents) +Manager | str | `manager` | Can perform only managing operations(view torrents + can download files + can add/edit categories + set torrent priority + can stop/start downloads) +Administrator| str | `administrator` | Can perform all operations (Manager + remove torrent + remove category + edit configs) + +### Telegram Proxy Scheme +Name | Type | Value(to be used in json) +--- | --- |--- +Sock4 | str | `socks4` +Sock5 |str | `socks5` +Http |str | `http` ## Other types @@ -90,4 +98,22 @@ Administrator| str | administrator | Can perform all operations ( A type that will accept any http or https URL. - TLD required - Host required -- Max length 2083 \ No newline at end of file +- Max length 2083 + +### Languages +Name | Type | Value(to be used in json) +--- | --- |--- +English | str | `en` +Italian | str | `it` +Ukrainian | str | `uk_UA` +Russian(Ukraine) | str | `ru_UA` + +### Telegram Proxy Settings +QBittorrentBot supports proxies with and without authentication. This feature allows QBittorrentBot to exchange data with Telegram through an intermediate SOCKS 4/5 or HTTP proxy server. + +Name | Type | Remarks +--- | --- | --- +scheme | [TelegramProxyScheme](#telegram-proxy-scheme) | The scheme to be used to connect to the proxy +hostname | str | The hostname of the proxy +username | str | Optional, the proxy user +password | str | Optional, the proxy password \ No newline at end of file diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md index e68510c..991cc5b 100644 --- a/docs/getting_started/index.md +++ b/docs/getting_started/index.md @@ -1,29 +1,10 @@ # Getting Started +QBittorrentBot is a Telegram bot that allows you to control your qBittorrent client from within the Telegram messaging app. This makes it easy to add new torrents, manage your existing downloads, and get status updates without having to switch between applications. -In order to start using the bot, you must first create a folder where the bot will fish for settings and where it will save logs +## Prerequisites -For example: let's create a folder called `QBittorrentBot` in the home of the user `user`. The path to the folder will then be `/home/user/docker/QBittorrentBot`. - -Before starting the bot you need to place the configuration file in this folder. You can rename the `config.json.template` file to `config.json` and change the parameters as desired. Go [here](configuration_file.md) to read more about the configuration file. - -Once that is done you can start the bot using docker, you can use either docker or docker compose. - -+++ Docker -Open your terminal and execute the following command to start the bot container: - -`docker run -d -v /home/user/docker/QBittorrentBot:/app/config:rw --name qbittorrent-bot ch3p4ll3/qbittorrent-bot:latest` -+++ Docker compose -Create a file named `docker-compose.yml` inside a directory with the following content: -``` -version: '3.9' -services: - qbittorrent-bot: - image: 'ch3p4ll3/qbittorrent-bot:latest' - container_name: qbittorrent-bot - restart: unless-stopped - volumes: - - '/home/user/docker/QBittorrentBot:/app/config:rw' -``` - -Run the following command to start the bot using Docker Compose: -`docker compose up -d` \ No newline at end of file +- A Telegram account + - A bot token obtained from [botfather](https://core.telegram.org/bots#how-do-i-create-a-bot) + - [Telegram API ID](https://core.telegram.org/api/obtaining_api_id) +- A running qBittorrent instance with WebUI enabled +- Access to your qBittorrent server's IP address and port number diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md new file mode 100644 index 0000000..8d99a14 --- /dev/null +++ b/docs/getting_started/installation.md @@ -0,0 +1,69 @@ +--- +label: Installation-Updating +--- + +# Installation + +In order to start using the bot, you must first create a folder where the bot will fish for settings and where it will save logs + +For example: let's create a folder called `QBittorrentBot` in the home of the user `user`. The path to the folder will then be `/home/user/docker/QBittorrentBot`. + +Before starting the bot you need to place the configuration file in this folder. You can rename the `config.json.template` file to `config.json` and change the parameters as desired. Go [here](configuration_file.md) to read more about the configuration file. + +Once that is done you can start the bot using docker, you can use either docker or docker compose. + ++++ Docker +Open your terminal and execute the following command to start the bot container: + +`docker run -d -v /home/user/docker/QBittorrentBot:/app/config:rw --name qbittorrent-bot ch3p4ll3/qbittorrent-bot:latest` ++++ Docker compose +Create a file named `docker-compose.yml` inside a directory with the following content: +``` +version: '3.9' +services: + qbittorrent-bot: + image: 'ch3p4ll3/qbittorrent-bot:latest' + container_name: qbittorrent-bot + restart: unless-stopped + volumes: + - '/home/user/docker/QBittorrentBot:/app/config:rw' +``` + +Run the following command to start the bot using Docker Compose: +`docker compose up -d` ++++ + +# Updating + ++++ Docker +To update to the latest version of QBittorrentBot, use the following commands to stop then remove the old version: +- `docker stop qbittorrent-bot` +- `docker rm qbittorrent-bot` + +Now that you have stopped and removed the old version of QBittorrentBot, you must ensure that you have the latest version of the image locally. You can do this with a docker pull command: + +`docker pull ch3p4ll3/qbittorrent-bot:latest` + +Finally, deploy the updated version of Portainer: + +`docker run -d -v /home/user/docker/QBittorrentBot:/app/config:rw --name qbittorrent-bot ch3p4ll3/qbittorrent-bot:latest` ++++ Docker compose +To update to the latest version of QBittorrentBot, navigate to the folder where you created the `docker-compose.yml` file. + +Then use the following command to pull the latest version of the image: + +`docker compose pull` + +Finally use the following command to start the bot using Docker Compose: +`docker compose up -d` ++++ + +# Running without docker +it is preferable to use the bot using docker, this gives the developers to isolate their app from its environment, solving the “it works on my machine” headache. + +In case you could not use docker you can use the bot without it. To do so, follow the following steps: +- Clone this repo `git clone https://github.com/ch3p4ll3/QBittorrentBot.git` +- Move in the project directory +- Install dependencies with `pip3 install -r requirements.txt` +- Create a config.json file +- Start the bot with `python3 main.py` \ No newline at end of file diff --git a/docs/getting_started/migrating_to_v2.md b/docs/getting_started/migrating_to_v2.md index 876b38e..827b263 100644 --- a/docs/getting_started/migrating_to_v2.md +++ b/docs/getting_started/migrating_to_v2.md @@ -64,13 +64,21 @@ configurations in comparison "telegram": { "bot_token": "1111111:AAAAAAAA-BBBBBBBBB", "api_id": 1111, - "api_hash": "aaaaaaaa" + "api_hash": "aaaaaaaa", + "proxy": { + "scheme": "http", + "hostname": "myproxy.local", + "port": 8080, + "username": "admin", + "password": "admin" + } }, "users": [ { "user_id": 123456, "notify": false, + "locale": "en", "role": "administrator" } ] diff --git a/requirements.txt b/requirements.txt index 5fbd17e..2f47bd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ APScheduler==3.10.4 async-lru==2.0.4 certifi==2023.11.17 charset-normalizer==3.3.2 +executing==2.0.1 idna==3.6 packaging==23.2 pony==0.7.17 @@ -10,6 +11,7 @@ psutil==5.9.6 pyaes==1.6.1 pydantic==2.5.2 pydantic_core==2.14.5 +pykeyboard==0.1.5 Pyrogram==2.0.106 PySocks==1.7.1 pytz==2023.3.post1 @@ -24,3 +26,4 @@ tzdata==2023.3 tzlocal==5.2 urllib3==2.1.0 uvloop==0.19.0 +varname==0.12.2 diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 1194ff8..4f60289 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -1,3 +1,4 @@ +import uvloop from pyrogram import Client from pyrogram.enums.parse_mode import ParseMode from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -5,20 +6,25 @@ from ..configs import Configs -BOT_CONFIGS = Configs.config - - plugins = dict( root="src.bot.plugins" ) + +proxy = None + +if Configs.config.telegram.proxy is not None: + proxy = Configs.config.telegram.proxy.proxy_settings + +uvloop.install() app = Client( "qbittorrent_bot", - api_id=BOT_CONFIGS.telegram.api_id, - api_hash=BOT_CONFIGS.telegram.api_hash, - bot_token=BOT_CONFIGS.telegram.bot_token, + api_id=Configs.config.telegram.api_id, + api_hash=Configs.config.telegram.api_hash, + bot_token=Configs.config.telegram.bot_token, parse_mode=ParseMode.MARKDOWN, - plugins=plugins + plugins=plugins, + proxy=proxy ) scheduler = AsyncIOScheduler() diff --git a/src/bot/custom_filters.py b/src/bot/custom_filters.py index 3479187..142c473 100644 --- a/src/bot/custom_filters.py +++ b/src/bot/custom_filters.py @@ -45,6 +45,7 @@ user_info_filter = filters.regex(r'^user_info(#.+|$)?$') edit_user_filter = filters.regex(r'^edit_user(#.+|$)?$') toggle_user_var_filter = filters.regex(r'^toggle_user_var(#.+|$)?$') +edit_locale_filter = filters.regex(r'^edit_locale(#.+|$)?$') edit_client_settings_filter = filters.regex(r"^edit_client$") list_client_settings_filter = filters.regex(r"^lst_client$") check_connection_filter = filters.regex(r"^check_connection$") diff --git a/src/bot/plugins/callbacks/add_torrents_callbacks.py b/src/bot/plugins/callbacks/add_torrents_callbacks.py index 2035a55..bdb3315 100644 --- a/src/bot/plugins/callbacks/add_torrents_callbacks.py +++ b/src/bot/plugins/callbacks/add_torrents_callbacks.py @@ -2,15 +2,20 @@ from pyrogram.types import CallbackQuery from .... import db_management from ... import custom_filters +from ....configs.user import User +from ....translator import Translator, Strings +from ....utils import inject_user @Client.on_callback_query(custom_filters.add_magnet_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def add_magnet_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def add_magnet_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: db_management.write_support(f"magnet#{callback_query.data.split('#')[1]}", callback_query.from_user.id) - await client.answer_callback_query(callback_query.id, "Send a magnet link") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.SendMagnetLink, user.locale)) @Client.on_callback_query(custom_filters.add_torrent_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def add_torrent_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def add_torrent_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: db_management.write_support(f"torrent#{callback_query.data.split('#')[1]}", callback_query.from_user.id) - await client.answer_callback_query(callback_query.id, "Send a torrent file") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.SendTorrentFile, user.locale)) diff --git a/src/bot/plugins/callbacks/category/__init__.py b/src/bot/plugins/callbacks/category/__init__.py index 534d78a..cfce486 100644 --- a/src/bot/plugins/callbacks/category/__init__.py +++ b/src/bot/plugins/callbacks/category/__init__.py @@ -2,24 +2,28 @@ from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.menu_category_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def menu_category_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def menu_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( "Pause/Resume a download", reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("➕ Add Category", "add_category"), + InlineKeyboardButton(Translator.translate(Strings.AddCategory, user.locale), "add_category"), ], [ - InlineKeyboardButton("🗑 Remove Category", "select_category#remove_category") + InlineKeyboardButton(Translator.translate(Strings.RemoveCategory, user.locale), "select_category#remove_category") ], [ - InlineKeyboardButton("📝 Modify Category", "select_category#modify_category")], + InlineKeyboardButton(Translator.translate(Strings.EditCategory, user.locale), "select_category#modify_category")], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/category/category_callbacks.py b/src/bot/plugins/callbacks/category/category_callbacks.py index 0cbc71d..35425fa 100644 --- a/src/bot/plugins/callbacks/category/category_callbacks.py +++ b/src/bot/plugins/callbacks/category/category_callbacks.py @@ -8,68 +8,105 @@ from .....client_manager import ClientRepo from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings + @Client.on_callback_query(custom_filters.add_category_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def add_category_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def add_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: db_management.write_support("category_name", callback_query.from_user.id) - button = InlineKeyboardMarkup([[InlineKeyboardButton("🔙 Menu", "menu")]]) + button = InlineKeyboardMarkup([[InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]]) try: - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "Send the category name", reply_markup=button) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.NewCategoryName, user.locale), + reply_markup=button + ) + except MessageIdInvalid: - await client.send_message(callback_query.from_user.id, "Send the category name", reply_markup=button) + await client.send_message( + callback_query.from_user.id, + Translator.translate(Strings.NewCategoryName, user.locale), + reply_markup=button + ) @Client.on_callback_query(custom_filters.select_category_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def list_categories(client: Client, callback_query: CallbackQuery): +@inject_user +async def list_categories(client: Client, callback_query: CallbackQuery, user: User): buttons = [] repository = ClientRepo.get_client_manager(Configs.config.client.type) categories = repository.get_categories() if categories is None: - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "There are no categories", reply_markup=InlineKeyboardMarkup(buttons)) + buttons.append([InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.NoCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + return for _, i in enumerate(categories): buttons.append([InlineKeyboardButton(i, f"{callback_query.data.split('#')[1]}#{i}")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) + buttons.append([InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]) try: - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "Choose a category:", reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + except MessageIdInvalid: - await client.send_message(callback_query.from_user.id, "Choose a category:", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + callback_query.from_user.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) @Client.on_callback_query(custom_filters.remove_category_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def remove_category_callback(client: Client, callback_query: CallbackQuery) -> None: - buttons = [[InlineKeyboardButton("🔙 Menu", "menu")]] +@inject_user +async def remove_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + buttons = [[InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]] repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.remove_category(callback_query.data.split("#")[1]) - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - f"The category {callback_query.data.split('#')[1]} has been removed", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.OnCategoryRemoved, user.locale, category_name=callback_query.data.split('#')[1]), + reply_markup=InlineKeyboardMarkup(buttons) + ) @Client.on_callback_query(custom_filters.modify_category_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def modify_category_callback(client: Client, callback_query: CallbackQuery) -> None: - buttons = [[InlineKeyboardButton("🔙 Menu", "menu")]] +@inject_user +async def modify_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + buttons = [[InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]] db_management.write_support(f"category_dir_modify#{callback_query.data.split('#')[1]}", callback_query.from_user.id) - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - f"Send new path for category {callback_query.data.split('#')[1]}", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.OnCategoryEdited, user.locale, category_name=callback_query.data.split('#')[1]), + reply_markup=InlineKeyboardMarkup(buttons) + ) @Client.on_callback_query(custom_filters.category_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def category(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def category(client: Client, callback_query: CallbackQuery, user: User) -> None: buttons = [] repository = ClientRepo.get_client_manager(Configs.config.client.type) @@ -77,22 +114,30 @@ async def category(client: Client, callback_query: CallbackQuery) -> None: if categories is None: if "magnet" in callback_query.data: - await add_magnet_callback(client, callback_query) + await add_magnet_callback(client, callback_query, user) else: - await add_torrent_callback(client, callback_query) + await add_torrent_callback(client, callback_query, user) return - for key, i in enumerate(categories): + for _, i in enumerate(categories): buttons.append([InlineKeyboardButton(i, f"{callback_query.data.split('#')[1]}#{i}")]) buttons.append([InlineKeyboardButton("None", f"{callback_query.data.split('#')[1]}#None")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) + buttons.append([InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]) try: - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "Choose a category:", reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + except MessageIdInvalid: - await client.send_message(callback_query.from_user.id, "Choose a category:", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + callback_query.from_user.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) diff --git a/src/bot/plugins/callbacks/delete/__init__.py b/src/bot/plugins/callbacks/delete/__init__.py index 9fb3661..d1363d2 100644 --- a/src/bot/plugins/callbacks/delete/__init__.py +++ b/src/bot/plugins/callbacks/delete/__init__.py @@ -2,22 +2,26 @@ from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.menu_delete_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def menu_delete_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def menu_delete_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( "Delete a torrent", reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("🗑 Delete", "delete_one") + InlineKeyboardButton(Translator.translate(Strings.DeleteTorrentBtn, user.locale), "delete_one") ], [ - InlineKeyboardButton("🗑 Delete All", "delete_all") + InlineKeyboardButton(Translator.translate(Strings.DeleteAllMenuBtn, user.locale), "delete_all") ], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/delete/delete_all_callbacks.py b/src/bot/plugins/callbacks/delete/delete_all_callbacks.py index 1d7ed41..a324df7 100644 --- a/src/bot/plugins/callbacks/delete/delete_all_callbacks.py +++ b/src/bot/plugins/callbacks/delete/delete_all_callbacks.py @@ -6,29 +6,46 @@ from ...common import send_menu from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings + + @Client.on_callback_query(custom_filters.delete_all_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_all_callback(client: Client, callback_query: CallbackQuery) -> None: - buttons = [[InlineKeyboardButton("🗑 Delete all torrents", "delete_all_no_data")], - [InlineKeyboardButton("🗑 Delete all torrents and data", "delete_all_data")], - [InlineKeyboardButton("🔙 Menu", "menu")]] +@inject_user +async def delete_all_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + buttons = [ + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteAllBtn, user.locale), "delete_all_no_data") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteAllData, user.locale), "delete_all_data") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ] + await client.edit_message_reply_markup(callback_query.from_user.id, callback_query.message.id, reply_markup=InlineKeyboardMarkup(buttons)) @Client.on_callback_query(custom_filters.delete_all_no_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_all_with_no_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_all_with_no_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.delete_all_no_data() - await client.answer_callback_query(callback_query.id, "Deleted only torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.DeletedAll, user.locale)) await send_menu(client, callback_query.message.id, callback_query.from_user.id) @Client.on_callback_query(custom_filters.delete_all_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_all_with_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_all_with_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.delete_all_data() - await client.answer_callback_query(callback_query.id, "Deleted All+Torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.DeletedAllData, user.locale)) await send_menu(client, callback_query.message.id, callback_query.from_user.id) diff --git a/src/bot/plugins/callbacks/delete/delete_single_callbacks.py b/src/bot/plugins/callbacks/delete/delete_single_callbacks.py index f11c2ca..561c70b 100644 --- a/src/bot/plugins/callbacks/delete/delete_single_callbacks.py +++ b/src/bot/plugins/callbacks/delete/delete_single_callbacks.py @@ -6,25 +6,39 @@ from ...common import send_menu, list_active_torrents from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings + + @Client.on_callback_query(custom_filters.delete_one_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "delete_one") else: buttons = [ - [InlineKeyboardButton("🗑 Delete torrent", f"delete_one_no_data#{callback_query.data.split('#')[1]}")], - [InlineKeyboardButton("🗑 Delete torrent and data", f"delete_one_data#{callback_query.data.split('#')[1]}")], - [InlineKeyboardButton("🔙 Menu", "menu")]] + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteSingleBtn, user.locale), f"delete_one_no_data#{callback_query.data.split('#')[1]}") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteSingleDataBtn, user.locale), f"delete_one_data#{callback_query.data.split('#')[1]}") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ] await client.edit_message_reply_markup(callback_query.from_user.id, callback_query.message.id, reply_markup=InlineKeyboardMarkup(buttons)) @Client.on_callback_query(custom_filters.delete_one_no_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_no_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_no_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "delete_one_no_data") @@ -36,7 +50,8 @@ async def delete_no_data_callback(client: Client, callback_query: CallbackQuery) @Client.on_callback_query(custom_filters.delete_one_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_with_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_with_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "delete_one_data") diff --git a/src/bot/plugins/callbacks/pause_resume/__init__.py b/src/bot/plugins/callbacks/pause_resume/__init__.py index 0ca1890..b9f4eec 100644 --- a/src/bot/plugins/callbacks/pause_resume/__init__.py +++ b/src/bot/plugins/callbacks/pause_resume/__init__.py @@ -1,24 +1,28 @@ from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.menu_pause_resume_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def menu_pause_resume_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def menu_pause_resume_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( - "Pause/Resume a torrent", + Translator.translate(Strings.PauseResumeMenu, user.locale), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("⏸ Pause", "pause"), - InlineKeyboardButton("▶️ Resume", "resume") + InlineKeyboardButton(Translator.translate(Strings.PauseTorrentBtn, user.locale), "pause"), + InlineKeyboardButton(Translator.translate(Strings.ResumeTorrentBtn, user.locale), "resume") ], [ - InlineKeyboardButton("⏸ Pause All", "pause_all"), - InlineKeyboardButton("▶️ Resume All", "resume_all") + InlineKeyboardButton(Translator.translate(Strings.PauseAll, user.locale), "pause_all"), + InlineKeyboardButton(Translator.translate(Strings.ResumeAll, user.locale), "resume_all") ], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py b/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py index 332279d..7c1421b 100644 --- a/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py +++ b/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py @@ -5,21 +5,26 @@ from .....client_manager import ClientRepo from ...common import list_active_torrents from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.pause_all_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def pause_all_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def pause_all_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.pause_all() - await client.answer_callback_query(callback_query.id, "Paused all torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.PauseAllTorrents, user.locale)) @Client.on_callback_query(custom_filters.pause_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def pause_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def pause_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "pause") else: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.pause(torrent_hash=callback_query.data.split("#")[1]) - await callback_query.answer("Torrent Paused") + await callback_query.answer(Translator.translate(Strings.PauseTorrent, user.locale)) diff --git a/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py b/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py index 477adad..6cb9823 100644 --- a/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py +++ b/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py @@ -5,21 +5,26 @@ from .....client_manager import ClientRepo from ...common import list_active_torrents from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.resume_all_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def resume_all_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def resume_all_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.resume_all() - await client.answer_callback_query(callback_query.id, "Resumed all torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.ResumeAllTorrents, user.locale)) @Client.on_callback_query(custom_filters.resume_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def resume_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def resume_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "resume") else: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.resume(torrent_hash=callback_query.data.split("#")[1]) - await callback_query.answer("Torrent Resumed") + await callback_query.answer(Translator.translate(Strings.ResumeTorrent, user.locale)) diff --git a/src/bot/plugins/callbacks/settings/__init__.py b/src/bot/plugins/callbacks/settings/__init__.py index b021c8c..d720db3 100644 --- a/src/bot/plugins/callbacks/settings/__init__.py +++ b/src/bot/plugins/callbacks/settings/__init__.py @@ -1,28 +1,29 @@ from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def settings_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( - "QBittorrentBot Settings", + Translator.translate(Strings.SettingsMenu, user.locale), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("🫂 Users Settings", "get_users") + InlineKeyboardButton(Translator.translate(Strings.UsersSettings, user.locale), "get_users") ], [ - InlineKeyboardButton("📥 Client Settings", "edit_client") + InlineKeyboardButton(Translator.translate(Strings.ClientSettings, user.locale), "edit_client") ], [ - InlineKeyboardButton("🇮🇹 Language Settings", "menu") + InlineKeyboardButton(Translator.translate(Strings.ReloadSettings, user.locale), "reload_settings") ], [ - InlineKeyboardButton("🔄 Reload Settings", "reload_settings") - ], - [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/settings/client_settings_callbacks.py b/src/bot/plugins/callbacks/settings/client_settings_callbacks.py index 3951af2..e08256b 100644 --- a/src/bot/plugins/callbacks/settings/client_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/client_settings_callbacks.py @@ -3,35 +3,44 @@ from .... import custom_filters from .....configs import Configs +from .....configs.user import User from .....client_manager import ClientRepo -from .....utils import convert_type_from_string +from .....utils import convert_type_from_string, inject_user from .....db_management import write_support +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.edit_client_settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def edit_client_settings_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def edit_client_settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: confs = '\n- '.join(iter([f"**{key.capitalize()}:** {item}" for key, item in Configs.config.client.model_dump().items()])) repository = ClientRepo.get_client_manager(Configs.config.client.type) speed_limit = repository.get_speed_limit_mode() - confs += f"\n\n**Speed Limit**: {'Enabled' if speed_limit else 'Disabled'}" + speed_limit_status = Translator.translate(Strings.Enabled if speed_limit else Strings.Disabled, user.locale) + + confs += Translator.translate( + Strings.SpeedLimitStatus, + user.locale, + speed_limit_status=speed_limit_status + ) await callback_query.edit_message_text( - f"Edit Qbittorrent Client Settings \n\n**Current Settings:**\n- {confs}", + Translator.translate(Strings.EditClientSettings, user.locale, client_type=Configs.config.client.type, configs=confs), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("📝 Edit Client Settings", "lst_client") + InlineKeyboardButton(Translator.translate(Strings.EditClientSettingsBtn, user.locale), "lst_client") ], [ - InlineKeyboardButton("🐢 Toggle Speed Limit", "toggle_speed_limit") + InlineKeyboardButton(Translator.translate(Strings.ToggleSpeedLimit, user.locale), "toggle_speed_limit") ], [ - InlineKeyboardButton("✅ Check Client connection", "check_connection") + InlineKeyboardButton(Translator.translate(Strings.CheckClientConnection, user.locale), "check_connection") ], [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -39,29 +48,36 @@ async def edit_client_settings_callback(client: Client, callback_query: Callback @Client.on_callback_query(custom_filters.toggle_speed_limit_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def toggle_speed_limit_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def toggle_speed_limit_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: confs = '\n- '.join(iter([f"**{key.capitalize()}:** {item}" for key, item in Configs.config.client.model_dump().items()])) repository = ClientRepo.get_client_manager(Configs.config.client.type) speed_limit = repository.toggle_speed_limit() - confs += f"\n\n**Speed Limit**: {'Enabled' if speed_limit else 'Disabled'}" + speed_limit_status = Translator.translate(Strings.Enabled if speed_limit else Strings.Disabled, user.locale) + + confs += Translator.translate( + Strings.SpeedLimitStatus, + user.locale, + speed_limit_status=speed_limit_status + ) await callback_query.edit_message_text( - f"Edit Qbittorrent Client Settings \n\n**Current Settings:**\n- {confs}", + Translator.translate(Strings.EditClientSettings, user.locale, client_type=Configs.config.client.type, configs=confs), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("📝 Edit Client Settings", "lst_client") + InlineKeyboardButton(Translator.translate(Strings.EditClientSettingsBtn, user.locale), "lst_client") ], [ - InlineKeyboardButton("🐢 Toggle Speed Limit", "toggle_speed_limit") + InlineKeyboardButton(Translator.translate(Strings.ToggleSpeedLimit, user.locale), "toggle_speed_limit") ], [ - InlineKeyboardButton("✅ Check Client connection", "check_connection") + InlineKeyboardButton(Translator.translate(Strings.CheckClientConnection, user.locale), "check_connection") ], [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -69,32 +85,38 @@ async def toggle_speed_limit_callback(client: Client, callback_query: CallbackQu @Client.on_callback_query(custom_filters.check_connection_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def check_connection_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def check_connection_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: try: repository = ClientRepo.get_client_manager(Configs.config.client.type) version = repository.check_connection() - await callback_query.answer(f"✅ The connection works. QBittorrent version: {version}", show_alert=True) + await callback_query.answer(Translator.translate(Strings.ClientConnectionOk, user.locale, version=version), show_alert=True) except Exception: - await callback_query.answer("❌ Unable to establish connection with QBittorrent", show_alert=True) + await callback_query.answer(Translator.translate(Strings.ClientConnectionBad, user.locale), show_alert=True) @Client.on_callback_query(custom_filters.list_client_settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def list_client_settings_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def list_client_settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: # get all fields of the model dynamically fields = [ - [InlineKeyboardButton(f"Edit {key.replace('_', ' ').capitalize()}", - f"edit_clt#{key}-{item.annotation}")] + [ + InlineKeyboardButton( + Translator.translate(Strings.EditClientSetting, user.locale, setting=key.replace('_', ' ').capitalize()), + f"edit_clt#{key}-{item.annotation}" + ) + ] for key, item in Configs.config.client.model_fields.items() ] await callback_query.edit_message_text( - "Edit Qbittorrent Client", + Translator.translate(Strings.EditClientType, user.locale, client_type=Configs.config.client.type), reply_markup=InlineKeyboardMarkup( fields + [ [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -102,7 +124,8 @@ async def list_client_settings_callback(client: Client, callback_query: Callback @Client.on_callback_query(custom_filters.edit_client_setting_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def edit_client_setting_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def edit_client_setting_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] field_to_edit = data.split("-")[0] data_type = convert_type_from_string(data.split("-")[1]) @@ -110,11 +133,11 @@ async def edit_client_setting_callback(client: Client, callback_query: CallbackQ write_support(callback_query.data, callback_query.from_user.id) await callback_query.edit_message_text( - f"Send the new value for field \"{field_to_edit}\" for client. \n\n**Note:** the field type is \"{data_type}\"", + Translator.translate(Strings.NewValueForClientField, user.locale, field_to_edit=field_to_edit, data_type=data_type), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) diff --git a/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py b/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py index 292cc8d..8d206c1 100644 --- a/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py @@ -3,10 +3,13 @@ from .... import custom_filters from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.reload_settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def reload_settings_callback(client: Client, callback_query: CallbackQuery) -> None: - # TO FIX reload +@inject_user +async def reload_settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: Configs.reload_config() - await callback_query.answer("Settings Reloaded", show_alert=True) + await callback_query.answer(Translator.translate(Strings.SettingsReloaded, user.locale), show_alert=True) diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index da448ef..f91098c 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -1,28 +1,36 @@ -import typing - from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton +from varname import nameof +from pykeyboard import InlineKeyboard, InlineButton from .... import custom_filters from .....configs import Configs +from .....configs.user import User from .....db_management import write_support -from .....utils import get_user_from_config, convert_type_from_string +from .....utils import get_user_from_config, convert_type_from_string, inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.get_users_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def get_users_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def get_users_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: users = [ - [InlineKeyboardButton(f"User #{i.user_id}", f"user_info#{i.user_id}")] + [ + InlineKeyboardButton( + Translator.translate(Strings.UserBtn, user.locale, user_id=i.user_id), + f"user_info#{i.user_id}" + ) + ] for i in Configs.config.users ] await callback_query.edit_message_text( - "Authorized users", + Translator.translate(Strings.AuthorizedUsers, user.locale), reply_markup=InlineKeyboardMarkup( users + [ [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -30,7 +38,8 @@ async def get_users_callback(client: Client, callback_query: CallbackQuery) -> N @Client.on_callback_query(custom_filters.user_info_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def get_user_info_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def get_user_info_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: user_id = int(callback_query.data.split("#")[1]) user_info = get_user_from_config(user_id) @@ -39,20 +48,24 @@ async def get_user_info_callback(client: Client, callback_query: CallbackQuery) # get all fields of the model dynamically fields = [ - [InlineKeyboardButton(f"Edit {key.replace('_', ' ').capitalize()}", - f"edit_user#{user_id}-{key}-{item.annotation}")] + [ + InlineKeyboardButton( + Translator.translate(Strings.EditClientSetting, user.locale, setting=key.replace('_', ' ').capitalize()), + f"edit_user#{user_id}-{key}-{item.annotation}" + ) + ] for key, item in user_info.model_fields.items() ] confs = '\n- '.join(iter([f"**{key.capitalize()}:** {item}" for key, item in user_info.model_dump().items()])) await callback_query.edit_message_text( - f"Edit User #{user_id}\n\n**Current Settings:**\n- {confs}", + Translator.translate(Strings.EditUserSetting, user.locale, user_id=user_id, confs=confs), reply_markup=InlineKeyboardMarkup( fields + [ [ - InlineKeyboardButton("🔙 Users", "get_users") + InlineKeyboardButton(Translator.translate(Strings.BackToUsers, user.locale), "get_users") ] ] ) @@ -60,7 +73,8 @@ async def get_user_info_callback(client: Client, callback_query: CallbackQuery) @Client.on_callback_query(custom_filters.edit_user_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def edit_user_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] user_id = int(data.split("-")[0]) field_to_edit = data.split("-")[1] @@ -68,18 +82,53 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> N user_info = get_user_from_config(user_id) - if data_type == bool or data_type == typing.Optional[bool]: + if field_to_edit == nameof(user.locale): + keyboard = InlineKeyboard() + keyboard.add( + *[ + InlineButton( + f'{Translator.translate(Strings.LangName, i)}/{Translator.translate(Strings.EnLangName, i)}', + f'edit_locale#{user_id}-{i}' + ) + + for i in Translator.locales + ] + ) + await callback_query.edit_message_text( - f"Edit User #{user_id} {field_to_edit} Field", + Translator.translate(Strings.EditLocale, user.locale), reply_markup=InlineKeyboardMarkup( + keyboard.inline_keyboard + [ [ InlineKeyboardButton( - f"{'✅' if user_info.notify else '❌'} Toggle", + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) + ] + ] + ) + ) + + return + + if data_type == bool: + notify_status = Translator.translate(Strings.Enabled if user_info.notify else Strings.Disabled, user.locale) + + await callback_query.edit_message_text( + Translator.translate(Strings.EditUserField, user.locale, user_id=user_id, field_to_edit=field_to_edit), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + notify_status, f"toggle_user_var#{user_id}-{field_to_edit}") ], [ - InlineKeyboardButton(f"🔙 User#{user_id} info", f"user_info#{user_id}") + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) ] ] ) @@ -90,11 +139,14 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> N write_support(callback_query.data, callback_query.from_user.id) await callback_query.edit_message_text( - f"Send the new value for field \"{field_to_edit}\" for user #{user_id}. \n\n**Note:** the field type is \"{data_type}\"", + Translator.translate(Strings.NewValueForUserField, user.locale, field_to_edit=field_to_edit, user_id=user_id, data_type=data_type), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton(f"🔙 User#{user_id} info", f"user_info#{user_id}") + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) ] ] ) @@ -102,13 +154,13 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> N @Client.on_callback_query(custom_filters.toggle_user_var_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def toggle_user_var(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def toggle_user_var(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] user_id = int(data.split("-")[0]) field_to_edit = data.split("-")[1] - user_info = get_user_from_config(user_id) - user_from_configs = Configs.config.users.index(user_info) + user_from_configs = Configs.config.users.index(get_user_from_config(user_id)) if user_from_configs == -1: return @@ -116,18 +168,47 @@ async def toggle_user_var(client: Client, callback_query: CallbackQuery) -> None Configs.config.users[user_from_configs].notify = not Configs.config.users[user_from_configs].notify Configs.update_config(Configs.config) + notify_status = Translator.translate(Strings.Enabled if user.notify else Strings.Disabled, user.locale) + await callback_query.edit_message_text( - f"Edit User #{user_id} {field_to_edit} Field", + Translator.translate(Strings.EditUserField, user.locale, field_to_edit=field_to_edit, user_id=user_id), reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton( - f"{'✅' if Configs.config.users[user_from_configs].notify else '❌'} Toggle", + notify_status, f"toggle_user_var#{user_id}-{field_to_edit}") ], [ - InlineKeyboardButton(f"🔙 User#{user_id} info", f"user_info#{user_id}") + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) ] ] ) ) + + +@Client.on_callback_query(custom_filters.edit_locale_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) +@inject_user +async def edit_locale_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + data = callback_query.data.split("#")[1] + user_id = int(data.split("-")[0]) + new_locale = data.split("-")[1] + + user_from_configs = Configs.config.users.index(get_user_from_config(user_id)) + + if user_from_configs == -1: + return + + Configs.config.users[user_from_configs].locale = new_locale + + await callback_query.answer( + Translator.translate( + Strings.NewLocale, + user.locale, + new_locale=Translator.translate(Strings.LangName, new_locale), + user_id=user_id + ) + ) diff --git a/src/bot/plugins/callbacks/torrent_info.py b/src/bot/plugins/callbacks/torrent_info.py index 79a7821..aa3a5ef 100644 --- a/src/bot/plugins/callbacks/torrent_info.py +++ b/src/bot/plugins/callbacks/torrent_info.py @@ -6,49 +6,68 @@ from ... import custom_filters from ....client_manager import ClientRepo from ....configs import Configs -from ....utils import convert_size, convert_eta +from ....configs.user import User +from ....translator import Translator, Strings +from ....utils import convert_size, convert_eta, inject_user @Client.on_callback_query(custom_filters.torrentInfo_filter & custom_filters.check_user_filter) -async def torrent_info_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def torrent_info_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) - torrent = repository.get_torrent_info(callback_query.data.split("#")[1]) + torrent = repository.get_torrent(callback_query.data.split("#")[1]) text = f"{torrent.name}\n" if torrent.progress == 1: - text += "**COMPLETED**\n" + text += Translator.translate(Strings.TorrentCompleted, user.locale) else: text += f"{tqdm.format_meter(torrent.progress, 1, 0, bar_format='{l_bar}{bar}|')}\n" if "stalled" not in torrent.state: - text += (f"**State:** {torrent.state.capitalize()} \n" - f"**Download Speed:** {convert_size(torrent.dlspeed)}/s\n") - - text += f"**Size:** {convert_size(torrent.size)}\n" + text += Translator.translate( + Strings.TorrentState, + user.locale, + current_state=torrent.state.capitalize(), + download_speed=convert_size(torrent.dlspeed) + ) + + text += Translator.translate( + Strings.TorrentSize, + user.locale, + torrent_size=convert_size(torrent.size) + ) if "stalled" not in torrent.state: - text += f"**ETA:** {convert_eta(int(torrent.eta))}\n" + text += Translator.translate( + Strings.TorrentEta, + user.locale, + torrent_eta=convert_eta(int(torrent.eta)) + ) if torrent.category: - text += f"**Category:** {torrent.category}\n" + text += Translator.translate( + Strings.TorrentCategory, + user.locale, + torrent_category=torrent.category + ) buttons = [ [ - InlineKeyboardButton("💾 Export torrent", f"export#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.ExportTorrentBtn, user.locale), f"export#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("⏸ Pause", f"pause#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.PauseTorrentBtn, user.locale), f"pause#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("▶️ Resume", f"resume#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.ResumeTorrentBtn, user.locale), f"resume#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("🗑 Delete", f"delete_one#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.DeleteTorrentBtn, user.locale), f"delete_one#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] diff --git a/src/bot/plugins/commands.py b/src/bot/plugins/commands.py index ec66a2b..c574e63 100644 --- a/src/bot/plugins/commands.py +++ b/src/bot/plugins/commands.py @@ -4,14 +4,25 @@ from .. import custom_filters from ...db_management import write_support -from ...utils import convert_size +from ...utils import convert_size, inject_user from .common import send_menu +from ...configs.user import User +from ...translator import Translator, Strings @Client.on_message(~custom_filters.check_user_filter) async def access_denied_message(client: Client, message: Message) -> None: - button = InlineKeyboardMarkup([[InlineKeyboardButton("Github", - url="https://github.com/ch3p4ll3/QBittorrentBot/")]]) + button = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Github", + url="https://github.com/ch3p4ll3/QBittorrentBot/" + ) + ] + ] + ) + await client.send_message(message.chat.id, "You are not authorized to use this bot", reply_markup=button) @@ -23,13 +34,19 @@ async def start_command(client: Client, message: Message) -> None: @Client.on_message(filters.command("stats") & custom_filters.check_user_filter) -async def stats_command(client: Client, message: Message) -> None: - stats_text = f"**============SYSTEM============**\n" \ - f"**CPU Usage:** {psutil.cpu_percent(interval=None)}%\n" \ - f"**CPU Temp:** {psutil.sensors_temperatures()['coretemp'][0].current}°C\n" \ - f"**Free Memory:** {convert_size(psutil.virtual_memory().available)} of " \ - f"{convert_size(psutil.virtual_memory().total)} ({psutil.virtual_memory().percent}%)\n" \ - f"**Disks usage:** {convert_size(psutil.disk_usage('/mnt').used)} of " \ - f"{convert_size(psutil.disk_usage('/mnt').total)} ({psutil.disk_usage('/mnt').percent}%)" +@inject_user +async def stats_command(client: Client, message: Message, user: User) -> None: + stats_text = Translator.translate( + Strings.StatsCommand, + user.locale, + cpu_usage=psutil.cpu_percent(interval=None), + cpu_temp=psutil.sensors_temperatures()['coretemp'][0].current, + free_memory=convert_size(psutil.virtual_memory().available), + total_memory=convert_size(psutil.virtual_memory().total), + memory_percent=psutil.virtual_memory().percent, + disk_used=convert_size(psutil.disk_usage('/mnt').used), + disk_total=convert_size(psutil.disk_usage('/mnt').total), + disk_percent=psutil.disk_usage('/mnt').percent + ) await client.send_message(message.chat.id, stats_text) diff --git a/src/bot/plugins/common.py b/src/bot/plugins/common.py index aaa6902..2d02370 100644 --- a/src/bot/plugins/common.py +++ b/src/bot/plugins/common.py @@ -9,81 +9,119 @@ from ...client_manager import ClientRepo from ...utils import get_user_from_config from ...configs.enums import UserRolesEnum +from ...translator import Translator, Strings async def send_menu(client: Client, message_id: int, chat_id: int) -> None: user = get_user_from_config(chat_id) buttons = [ - [InlineKeyboardButton("📝 List", "list")] + [InlineKeyboardButton(Translator.translate(Strings.MenuList, user.locale), "list")] ] if user.role in [UserRolesEnum.Manager, UserRolesEnum.Administrator]: buttons += [ - [InlineKeyboardButton("➕ Add Magnet", "category#add_magnet"), - InlineKeyboardButton("➕ Add Torrent", "category#add_torrent")], - [InlineKeyboardButton("⏯ Pause/Resume", "menu_pause_resume")] + [InlineKeyboardButton(Translator.translate(Strings.AddMagnet, user.locale), "category#add_magnet"), + InlineKeyboardButton(Translator.translate(Strings.AddTorrent, user.locale), "category#add_torrent")], + [InlineKeyboardButton(Translator.translate(Strings.PauseResume, user.locale), "menu_pause_resume")] ] if user.role == UserRolesEnum.Administrator: buttons += [ - [InlineKeyboardButton("🗑 Delete", "menu_delete")], - [InlineKeyboardButton("📂 Categories", "menu_categories")], - [InlineKeyboardButton("⚙️ Settings", "settings")] + [InlineKeyboardButton(Translator.translate(Strings.Delete, user.locale), "menu_delete")], + [InlineKeyboardButton(Translator.translate(Strings.Categories, user.locale), "menu_categories")], + [InlineKeyboardButton(Translator.translate(Strings.Settings, user.locale), "settings")] ] db_management.write_support("None", chat_id) try: - await client.edit_message_text(chat_id, message_id, text="Qbittorrent Control", + await client.edit_message_text(chat_id, message_id, text=Translator.translate(Strings.Menu, user.locale), reply_markup=InlineKeyboardMarkup(buttons)) except MessageIdInvalid: - await client.send_message(chat_id, text="Qbittorrent Control", reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + chat_id, + text=Translator.translate(Strings.Menu, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) -async def list_active_torrents(client: Client, chat_id, message_id, callback: Optional[str] = None, status_filter: str = None) -> None: +async def list_active_torrents(client: Client, chat_id: int, message_id: int, callback: Optional[str] = None, status_filter: str = None) -> None: + user = get_user_from_config(chat_id) repository = ClientRepo.get_client_manager(Configs.config.client.type) - torrents = repository.get_torrent_info(status_filter=status_filter) + torrents = repository.get_torrents(status_filter=status_filter) def render_categories_buttons(): return [ - InlineKeyboardButton(f"⏳ {'*' if status_filter == 'downloading' else ''} Downloading", - "by_status_list#downloading"), - InlineKeyboardButton(f"✔️ {'*' if status_filter == 'completed' else ''} Completed", - "by_status_list#completed"), - InlineKeyboardButton(f"⏸️ {'*' if status_filter == 'paused' else ''} Paused", "by_status_list#paused"), + InlineKeyboardButton( + Translator.translate( + Strings.ListFilterDownloading, user.locale, active='*' if status_filter == 'downloading' else '' + ), + "by_status_list#downloading" + ), + + InlineKeyboardButton( + Translator.translate( + Strings.ListFilterCompleted, user.locale, active='*' if status_filter == 'completed' else '' + ), + "by_status_list#completed" + ), + + InlineKeyboardButton( + Translator.translate( + Strings.ListFilterPaused, user.locale, active='*' if status_filter == 'paused' else '' + ), + "by_status_list#paused" + ), ] categories_buttons = render_categories_buttons() if not torrents: - buttons = [categories_buttons, [InlineKeyboardButton("🔙 Menu", "menu")]] + buttons = [ + categories_buttons, + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ] + try: - await client.edit_message_text(chat_id, message_id, "There are no torrents", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + chat_id, + message_id, + Translator.translate(Strings.NoTorrents, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + except MessageIdInvalid: - await client.send_message(chat_id, "There are no torrents", reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + chat_id, + Translator.translate(Strings.NoTorrents, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + return buttons = [categories_buttons] if callback is not None: for _, i in enumerate(torrents): - buttons.append([InlineKeyboardButton(i.name, f"{callback}#{i.info.hash}")]) - - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) - - try: - await client.edit_message_reply_markup(chat_id, message_id, reply_markup=InlineKeyboardMarkup(buttons)) - except MessageIdInvalid: - await client.send_message(chat_id, "Qbittorrent Control", reply_markup=InlineKeyboardMarkup(buttons)) + buttons.append([InlineKeyboardButton(i.name, f"{callback}#{i.hash}")]) else: - for key, i in enumerate(torrents): - buttons.append([InlineKeyboardButton(i.name, f"torrentInfo#{i.info.hash}")]) + for _, i in enumerate(torrents): + buttons.append([InlineKeyboardButton(i.name, f"torrentInfo#{i.hash}")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) + buttons.append( + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ) - try: - await client.edit_message_reply_markup(chat_id, message_id, reply_markup=InlineKeyboardMarkup(buttons)) - except MessageIdInvalid: - await client.send_message(chat_id, "Qbittorrent Control", reply_markup=InlineKeyboardMarkup(buttons)) + try: + await client.edit_message_reply_markup(chat_id, message_id, reply_markup=InlineKeyboardMarkup(buttons)) + except MessageIdInvalid: + await client.send_message( + chat_id, + Translator.translate(Strings.Menu, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) diff --git a/src/bot/plugins/on_message.py b/src/bot/plugins/on_message.py index 37e045f..c546843 100644 --- a/src/bot/plugins/on_message.py +++ b/src/bot/plugins/on_message.py @@ -1,5 +1,4 @@ import logging -import os import tempfile from pyrogram import Client, filters from pyrogram.types import Message @@ -7,128 +6,169 @@ from ... import db_management from .common import send_menu from ...configs import Configs -from ...utils import get_user_from_config, convert_type_from_string +from ...configs.user import User +from ...utils import convert_type_from_string, inject_user from .. import custom_filters +from ...translator import Translator, Strings logger = logging.getLogger(__name__) -@Client.on_message(~filters.me & custom_filters.check_user_filter) -async def on_text(client: Client, message: Message) -> None: - action = db_management.read_support(message.from_user.id) +async def on_magnet(client, message, user): + if message.text.startswith("magnet:?xt"): + magnet_link = message.text.split("\n") + category = db_management.read_support(message.from_user.id).split("#")[1] - if "magnet" in action: - if message.text.startswith("magnet:?xt"): - magnet_link = message.text.split("\n") + repository = ClientRepo.get_client_manager(Configs.config.client.type) + response = repository.add_magnet( + magnet_link=magnet_link, + category=category + ) + + if not response: + await message.reply_text(Translator.translate(Strings.UnableToAddMagnet, locale=user.locale)) + return + + await send_menu(client, message.id, message.from_user.id) + db_management.write_support("None", message.from_user.id) + + else: + await client.send_message( + message.from_user.id, + Translator.translate(Strings.InvalidMagnet, locale=user.locale) + ) + + +async def on_torrent(client, message, user): + if ".torrent" in message.document.file_name: + with tempfile.TemporaryDirectory() as tempdir: + name = f"{tempdir}/{message.document.file_name}" category = db_management.read_support(message.from_user.id).split("#")[1] + await message.download(name) repository = ClientRepo.get_client_manager(Configs.config.client.type) - response = repository.add_magnet( - magnet_link=magnet_link, - category=category - ) + response = repository.add_torrent(file_name=name, category=category) if not response: - await message.reply_text("Unable to add magnet link") + await message.reply_text(Translator.translate(Strings.UnableToAddTorrent, locale=user.locale)) return - await send_menu(client, message.id, message.from_user.id) - db_management.write_support("None", message.from_user.id) + await send_menu(client, message.id, message.from_user.id) + db_management.write_support("None", message.from_user.id) - else: - await client.send_message(message.from_user.id, "This magnet link is invalid! Retry") + else: + await client.send_message( + message.from_user.id, + Translator.translate(Strings.InvalidTorrent, locale=user.locale) + ) - elif "torrent" in action and message.document: - if ".torrent" in message.document.file_name: - with tempfile.TemporaryDirectory() as tempdir: - name = f"{tempdir}/{message.document.file_name}" - category = db_management.read_support(message.from_user.id).split("#")[1] - await message.download(name) - repository = ClientRepo.get_client_manager(Configs.config.client.type) - response = repository.add_torrent(file_name=name, category=category) +async def on_category_name(client, message): + db_management.write_support(f"category_dir#{message.text}", message.from_user.id) + await client.send_message( + message.from_user.id, + Translator.translate(Strings.CategoryPath, category_name=message.text) + ) - if not response: - await message.reply_text("Unable to add magnet link") - return - await send_menu(client, message.id, message.from_user.id) - db_management.write_support("None", message.from_user.id) +async def on_category_directory(client, message, action): + name = db_management.read_support(message.from_user.id).split("#")[1] - else: - await client.send_message(message.from_user.id, "This is not a torrent file! Retry") + repository = ClientRepo.get_client_manager(Configs.config.client.type) - elif action == "category_name": - db_management.write_support(f"category_dir#{message.text}", message.from_user.id) - await client.send_message(message.from_user.id, f"now send me the path for the category {message.text}") + if "modify" in action: + repository.edit_category(name=name, save_path=message.text) - elif "category_dir" in action: - if os.path.exists(message.text): - name = db_management.read_support(message.from_user.id).split("#")[1] + await send_menu(client, message.id, message.from_user.id) + return - repository = ClientRepo.get_client_manager(Configs.config.client.type) + repository.create_category(name=name, save_path=message.text) - if "modify" in action: - repository.edit_category(name=name, save_path=message.text) + await send_menu(client, message.id, message.from_user.id) - await send_menu(client, message.id, message.from_user.id) - return - repository.create_category(name=name, save_path=message.text) +async def on_edit_user(client, message, user, action): + data = action.split("#")[1] + user_id = int(data.split("-")[0]) + field_to_edit = data.split("-")[1] + data_type = convert_type_from_string(data.split("-")[2].replace("", "")) - await send_menu(client, message.id, message.from_user.id) + try: + new_value = data_type(message.text) - else: - await client.send_message(message.from_user.id, "The path entered does not exist! Retry") + user_from_configs = Configs.config.users.index(user) - elif "edit_user" in action: - data = action.split("#")[1] - user_id = int(data.split("-")[0]) - field_to_edit = data.split("-")[1] - data_type = convert_type_from_string(data.split("-")[2].replace("", "")) + if user_from_configs == -1: + return - try: - new_value = data_type(message.text) + if field_to_edit == "locale" and new_value not in Translator.locales.keys(): + await message.reply_text( + Translator.translate( + Strings.LocaleNotFound, + locale=user.locale, + new_locale=new_value + ) + ) + return - user_info = get_user_from_config(user_id) - user_from_configs = Configs.config.users.index(user_info) + setattr(Configs.config.users[user_from_configs], field_to_edit, new_value) + Configs.update_config(Configs.config) + Configs.reload_config() + logger.debug(f"Updating User #{user_id} {field_to_edit} settings to {new_value}") + db_management.write_support("None", message.from_user.id) - if user_from_configs == -1: - return + await send_menu(client, message.id, message.from_user.id) + except Exception as e: + await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) + logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) - setattr(Configs.config.users[user_from_configs], field_to_edit, new_value) - Configs.update_config(Configs.config) - Configs.reload_config() - logger.debug(f"Updating User #{user_id} {field_to_edit} settings to {new_value}") - db_management.write_support("None", message.from_user.id) - await send_menu(client, message.id, message.from_user.id) - except Exception as e: - await message.reply_text( - f"Error: {e}" - ) - logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) +async def on_edit_client(client: Client, message: Message, user, action): + data = action.split("#")[1] + field_to_edit = data.split("-")[0] + data_type = convert_type_from_string(data.split("-")[1]) - elif "edit_clt" in action: - data = action.split("#")[1] - field_to_edit = data.split("-")[0] - data_type = convert_type_from_string(data.split("-")[1]) + try: + new_value = data_type(message.text) - try: - new_value = data_type(message.text) + setattr(Configs.config.client, field_to_edit, new_value) + Configs.update_config(Configs.config) + Configs.reload_config() + logger.debug(f"Updating Client field \"{field_to_edit}\" to \"{new_value}\"") + db_management.write_support("None", message.from_user.id) - setattr(Configs.config.client, field_to_edit, new_value) - Configs.update_config(Configs.config) - Configs.reload_config() - logger.debug(f"Updating Client field \"{field_to_edit}\" to \"{new_value}\"") - db_management.write_support("None", message.from_user.id) + await send_menu(client, message.id, message.from_user.id) + except Exception as e: + await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) + logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) + + +@Client.on_message(~filters.me & custom_filters.check_user_filter) +@inject_user +async def on_text(client: Client, message: Message, user: User) -> None: + action = db_management.read_support(message.from_user.id) + + if "magnet" in action: + await on_magnet(client, message, user) + + elif "torrent" in action and message.document: + await on_torrent(client, message, user) + + elif action == "category_name": + await on_category_name(client, message) + + elif "category_dir" in action: + await on_category_directory(client, message, action) + + elif "edit_user" in action: + await on_edit_user(client, message, user, action) + + elif "edit_clt" in action: + await on_edit_client(client, message, user, action) - await send_menu(client, message.id, message.from_user.id) - except Exception as e: - await message.reply_text( - f"Error: {e}" - ) - logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) else: - await client.send_message(message.from_user.id, "The command does not exist") + await client.send_message( + message.from_user.id, + Translator.translate(Strings.CommandDoesNotExist, locale=user.locale) + ) diff --git a/src/client_manager/client_manager.py b/src/client_manager/client_manager.py index be5633d..e064be9 100644 --- a/src/client_manager/client_manager.py +++ b/src/client_manager/client_manager.py @@ -60,7 +60,12 @@ def get_categories(cls): raise NotImplementedError @classmethod - def get_torrent_info(cls, torrent_hash: str = None, status_filter: str = None): + def get_torrent(cls, torrent_hash: str, status_filter: str = None): + """Get a torrent info with or without a status filter""" + raise NotImplementedError + + @classmethod + def get_torrents(cls, torrent_hash: str = None, status_filter: str = None): """Get a torrent info with or without a status filter""" raise NotImplementedError diff --git a/src/client_manager/entities/__init__.py b/src/client_manager/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/client_manager/entities/torrent.py b/src/client_manager/entities/torrent.py new file mode 100644 index 0000000..25a6622 --- /dev/null +++ b/src/client_manager/entities/torrent.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Union + + +@dataclass +class Torrent: + hash: str + name: str + progress: int + dlspeed: int + state: str + size: int + eta: int + category: Union[str, None] diff --git a/src/client_manager/mappers/__init__.py b/src/client_manager/mappers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/client_manager/mappers/mapper.py b/src/client_manager/mappers/mapper.py new file mode 100644 index 0000000..7020c96 --- /dev/null +++ b/src/client_manager/mappers/mapper.py @@ -0,0 +1,10 @@ +from abc import ABC +from typing import Union, List, Any +from ..entities.torrent import Torrent + + +class Mapper(ABC): + @classmethod + def map(cls, torrents: Union[Any, List[Any]]) -> Union[Torrent, List[Torrent]]: + """Map a torrent or list of torrents to a mapped torrent or list of torrents""" + raise NotImplementedError diff --git a/src/client_manager/mappers/mapper_repo.py b/src/client_manager/mappers/mapper_repo.py new file mode 100644 index 0000000..b432866 --- /dev/null +++ b/src/client_manager/mappers/mapper_repo.py @@ -0,0 +1,12 @@ +from ...configs.enums import ClientTypeEnum +from .qbittorrent_mapper import QBittorrentMapper, Mapper + + +class MapperRepo: + mappers = { + ClientTypeEnum.QBittorrent: QBittorrentMapper + } + + @classmethod + def get_mapper(cls, client_type: ClientTypeEnum): + return cls.mappers.get(client_type, Mapper) diff --git a/src/client_manager/mappers/qbittorrent_mapper.py b/src/client_manager/mappers/qbittorrent_mapper.py new file mode 100644 index 0000000..1d33766 --- /dev/null +++ b/src/client_manager/mappers/qbittorrent_mapper.py @@ -0,0 +1,35 @@ +from typing import List, Union + +from ..entities.torrent import Torrent +from .mapper import Mapper +from qbittorrentapi.torrents import TorrentInfoList, TorrentDictionary + + +class QBittorrentMapper(Mapper): + @classmethod + def map(cls, torrents: Union[TorrentDictionary, TorrentInfoList]) -> Union[Torrent, List[Torrent]]: + if isinstance(torrents, TorrentInfoList): + return [ + Torrent( + torrent.info.hash, + torrent.name, + torrent.progress, + torrent.dlspeed, + torrent.state, + torrent.size, + torrent.eta, + torrent.category + ) + for torrent in torrents + ] + + return Torrent( + torrents.info.hash, + torrents.name, + torrents.progress, + torrents.dlspeed, + torrents.state, + torrents.size, + torrents.eta, + torrents.category + ) diff --git a/src/client_manager/qbittorrent_manager.py b/src/client_manager/qbittorrent_manager.py index 907d2bf..6fcc661 100644 --- a/src/client_manager/qbittorrent_manager.py +++ b/src/client_manager/qbittorrent_manager.py @@ -6,6 +6,9 @@ from typing import Union, List from .client_manager import ClientManager +from .mappers.mapper_repo import MapperRepo +from .entities.torrent import Torrent + logger = logging.getLogger(__name__) @@ -102,18 +105,25 @@ def get_categories(cls): return @classmethod - def get_torrent_info(cls, torrent_hash: str = None, status_filter: str = None): - if torrent_hash is None: - logger.debug("Getting torrents infos") - with qbittorrentapi.Client(**Configs.config.client.connection_string) as qbt_client: - return qbt_client.torrents_info(status_filter=status_filter) - logger.debug(f"Getting infos for torrent with hash {torrent_hash}") + def get_torrent(cls, torrent_hash: str, status_filter: str = None) -> Union[Torrent, None]: + logger.debug(f"Getting torrent with hash {torrent_hash}") with qbittorrentapi.Client(**Configs.config.client.connection_string) as qbt_client: - return next( + mapper = MapperRepo.get_mapper(Configs.config.client.type) + + torrent = next( iter( qbt_client.torrents_info(status_filter=status_filter, torrent_hashes=torrent_hash) ), None ) + return mapper.map(torrent) + + @classmethod + def get_torrents(cls, torrent_hash: str = None, status_filter: str = None) -> List[Torrent]: + if torrent_hash is None: + logger.debug("Getting torrents infos") + with qbittorrentapi.Client(**Configs.config.client.connection_string) as qbt_client: + mapper = MapperRepo.get_mapper(Configs.config.client.type) + return mapper.map(qbt_client.torrents_info(status_filter=status_filter)) @classmethod def edit_category(cls, name: str, save_path: str) -> None: diff --git a/src/configs/enums.py b/src/configs/enums.py index 3c58caf..c35a54e 100644 --- a/src/configs/enums.py +++ b/src/configs/enums.py @@ -9,3 +9,9 @@ class UserRolesEnum(str, Enum): Reader = "reader" Manager = "manager" Administrator = "administrator" + + +class TelegramProxyScheme(str, Enum): + Sock4 = "socks4" + Sock5 = "socks5" + Http = "http" \ No newline at end of file diff --git a/src/configs/telegram.py b/src/configs/telegram.py index 64ff2de..a554801 100644 --- a/src/configs/telegram.py +++ b/src/configs/telegram.py @@ -1,10 +1,13 @@ +from typing import Optional from pydantic import BaseModel, field_validator +from .telegram_proxy import TelegramProxy class Telegram(BaseModel): bot_token: str api_id: int api_hash: str + proxy: Optional[TelegramProxy] = None @field_validator('bot_token') def bot_token_validator(cls, v): diff --git a/src/configs/telegram_proxy.py b/src/configs/telegram_proxy.py new file mode 100644 index 0000000..4da033a --- /dev/null +++ b/src/configs/telegram_proxy.py @@ -0,0 +1,21 @@ +from typing import Optional +from pydantic import BaseModel +from .enums import TelegramProxyScheme + + +class TelegramProxy(BaseModel): + scheme: TelegramProxyScheme = TelegramProxyScheme.Http # "socks4", "socks5" and "http" are supported + hostname: str + port: int + username: Optional[str] = None + password: Optional[str] = None + + @property + def proxy_settings(self): + return dict( + scheme=self.scheme.value, + hostname=self.hostname, + port=self.port, + username=self.username, + passowrd=self.password + ) diff --git a/src/configs/user.py b/src/configs/user.py index a7d45d3..ace3b4f 100644 --- a/src/configs/user.py +++ b/src/configs/user.py @@ -7,4 +7,5 @@ class User(BaseModel): user_id: int role: UserRolesEnum = UserRolesEnum.Reader + locale: Optional[str] = "en" notify: Optional[bool] = True diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..fb33f30 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,160 @@ +{ + "lang_name": "English", + "en_lang_name": "English", + + "errors": { + "path_not_valid": "The path entered does not exist! Retry", + "error": "Error: ${error}", + "command_does_not_exist": "The command does not exist", + "locale_not_found": "Locale ${new_locale} not found!" + }, + + "on_message": { + "error_adding_magnet": "Unable to add magnet link", + "invalid_magnet": "This magnet link is invalid! Retry", + "error_adding_torrent": "Unable to add torrent file", + "invalid_torrent": "This is not a torrent file! Retry", + "send_category_path": "Please, send the path for the category ${category_name}" + }, + + + "common": { + "menu": "Welcome to QBittorrent Bot", + "menu_btn": { + "menu_list": "\uD83D\uDCDD List", + "add_magnet": "➕ Add Magnet", + "add_torrent": "➕ Add Torrent", + "pause_resume": "⏯ Pause/Resume", + "delete": "\uD83D\uDDD1 Delete", + "categories": "\uD83D\uDCC2 Categories", + "settings": "⚙\uFE0F Settings" + }, + + "list_filter": { + "downloading": "⏳ ${active} Downloading", + "completed": "✔\uFE0F ${active} Completed", + "paused": "⏸\uFE0F ${active} Paused" + }, + + "no_torrents": "There are no torrents", + "back_to_menu_btn": "\uD83D\uDD19 Menu" + }, + + "commands": { + "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**COMPLETED**\n", + "torrent_state": "**State:** ${current_state} \n**Download Speed:** ${download_speed}/s\n", + "torrent_size": "**Size:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Category:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Export torrent", + "pause_torrent": "⏸ Pause", + "resume_torrent": "▶️ Resume", + "delete_torrent": "🗑 Delete" + } + }, + + "add_torrents": { + "send_magnet": "Send a magnet link", + "send_torrent": "Send a torrent file" + }, + + "settings": { + "settings_menu": "QBittorrentBot Settings", + "settings_menu_btns": { + "users_settings": "🫂 Users Settings", + "client_settings": "📥 Client Settings", + "reload_settings": "🔄 Reload Settings" + } + }, + + "client_settings": { + "enabled": "✅ Enabled", + "disabled": "❌ Disabled", + "speed_limit_status": "\n\n**Speed Limit**: ${speed_limit_status}", + "edit_client_settings": "Edit ${client_type} client settings \n\n**Current Settings:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Edit Client Settings", + "toggle_speed_limit": "🐢 Toggle Speed Limit", + "check_client_connection": "✅ Check Client connection", + "back_to_settings": "🔙 Settings" + }, + + "client_connection_ok": "✅ The connection works. QBittorrent version: ${version}", + "client_connection_bad": "❌ Unable to establish connection with QBittorrent", + + "edit_client_setting": "Edit ${setting}", + "edit_client_type": "Edit ${client_type} Client", + + "new_value_for_field": "Send the new value for field \"${field_to_edit}\" for client. \n\n**Note:** the field type is \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Settings Reloaded" + }, + + "user_settings": { + "user_btn": "User #${user_id}", + "authorized_users": "Authorized users", + "new_locale": "Changed language to ${new_locale} for user #${user_id}", + "edit_user_locale": "Choose a new locale:", + "edit_user_setting": "Edit User #${user_id}\n\n**Current Settings:**\n- ${confs}", + "back_to_users": "\uD83D\uDD19 Users", + "edit_user_field": "Edit User #${user_id} ${field_to_edit} field", + "back_to_user": "\uD83D\uDD19 User#${user_id} info", + "new_value_for_field": "Send the new value for field \"${field_to_edit}\" for user #${user_id}. \n\n**Note:** the field type is \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Pause/Resume a torrent", + "pause_all": "⏸ Pause All", + "resume_all": "▶\uFE0F Resume All" + }, + + "pause": { + "pause_all_torrents": "Paused all torrents", + "pause_one_torrent": "Torrent Paused" + }, + + "resume": { + "resume_all_torrents": "Resumed all torrents", + "resume_one_torrent": "Torrent Resumed" + }, + + "delete_delete_all": { + "delete_all": "🗑 Delete All" + }, + + "delete_all": { + "delete_all": "🗑 Delete all torrents", + "delete_all_data": "🗑 Delete all torrents and data", + + "deleted_all": "Deleted all torrents", + "deleted_all_data": "Deleted all torrents and data" + }, + + "delete_single": { + "delete_single": "🗑 Delete torrent", + "delete_single_data": "🗑 Delete torrent and data" + }, + + "category": { + "add_category_btn": "➕ Add Category", + "remove_category_btn": "🗑 Remove Category", + "modify_category_btn": "📝 Modify Category", + + "send_category_name": "Send the category name", + "no_categories": "There are no categories", + "choose_category": "Choose a category:", + "on_category_removed": "The category ${category_name} has been removed", + "on_category_edited": "Send new path for category ${category_name}" + } + } +} \ No newline at end of file diff --git a/src/locales/it.json b/src/locales/it.json new file mode 100644 index 0000000..d34a12b --- /dev/null +++ b/src/locales/it.json @@ -0,0 +1,160 @@ +{ + "lang_name": "Italiano", + "en_lang_name": "Italian", + + "errors": { + "path_not_valid": "Il path non esiste! Riprova", + "error": "Errore: ${error}", + "command_does_not_exist": "Il comando non esiste", + "locale_not_found": "Lingua ${new_locale} non trovata" + }, + + "on_message": { + "error_adding_magnet": "Impossibile aggiungere magnet link", + "invalid_magnet": "Questo magnet link è invalido! Riprova", + "error_adding_torrent": "Impossibile aggiungere file torrent", + "invalid_torrent": "Questo non è un file torrent! Riprova", + "send_category_path": "Per favore, invia il percorso per la categoria ${category_name}" + }, + + + "common": { + "menu": "Benvenuto in QBittorrent Bot", + "menu_btn": { + "menu_list": "📝 Elenco", + "add_magnet": "➕ Aggiungi Magnet", + "add_torrent": "➕ Aggiungi Torrent", + "pause_resume": "⏯ Pausa/Riprendi", + "delete": "🗑 Elimina", + "categories": "📂 Categorie", + "settings": "⚙️ Impostazioni" + }, + + "list_filter": { + "downloading": "⏳ ${active} Downloading", + "completed": "✔️ ${active} Completato", + "paused": "⏸️ ${active} In pausa" + }, + + "no_torrents": "Non ci sono torrent", + "back_to_menu_btn": "🔙 Menu" + }, + + "commands": { + "stats_command": "**============SISTEMA============**\n**Utilizzo CPU:** ${cpu_usage}%\n**Temperatura CPU:** ${cpu_temp}°C\n**Memoria libera:** ${free_memory} di ${total_memory} (${memory_percent}%)\n**Utilizzo dischi:** ${disk_used} di ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**COMPLETATO**\n", + "torrent_state": "**Stato:** ${current_state}\n**Velocità di download:** ${download_speed}/s\n", + "torrent_size": "**Dimensione:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Categoria:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Esporta torrent", + "pause_torrent": "⏸ Pausa", + "resume_torrent": "▶️ Riprendi", + "delete_torrent": "🗑 Elimina" + } + }, + + "add_torrents": { + "send_magnet": "Invia un link magnet", + "send_torrent": "Invia un file torrent" + }, + + "settings": { + "settings_menu": "Impostazioni QBittorrentBot", + "settings_menu_btns": { + "users_settings": "🫂 Impostazioni utenti", + "client_settings": "📥 Impostazioni client", + "reload_settings": "🔄 Ricarica impostazioni" + } + }, + + "client_settings": { + "enabled": "✅ Abilitato", + "disabled": "❌ Disabilitato", + "speed_limit_status": "\n\n**Limite di velocità**: ${speed_limit_status}", + "edit_client_settings": "Modifica le impostazioni del cliente ${client_type}\n\n**Impostazioni attuali:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Modifica impostazioni client", + "toggle_speed_limit": "🐢 Commuta limitazione di velocità", + "check_client_connection": "✅ Verifica la connessione del client", + "back_to_settings": "🔙 Impostazioni" + }, + + "client_connection_ok": "✅ La connessione funziona. Versione di QBittorrent: ${version}", + "client_connection_bad": "❌ Impossibile stabilire una connessione con QBittorrent", + + "edit_client_setting": "Modifica ${setting}", + "edit_client_type": "Modifica ${client_type} client", + + "new_value_for_field": "Invia il nuovo valore per il campo \"${field_to_edit}\" per il cliente.\n\n**Nota:** il tipo di campo è \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Impostazioni Ricaricate" + }, + + "user_settings": { + "user_btn": "User #${user_id}", + "authorized_users": "Utenti autorizzati", + "new_locale": "Modificata la lingua per l'utente #${user_id} in ${new_locale}", + "edit_user_locale": "Scegli una nuova lingua.", + "edit_user_setting": "Modifica utente #${user_id}\n\n**Impostazioni attuali:**\n- ${confs}", + "back_to_users": "🔙 Utenti", + "edit_user_field": "Modifica campo utente #${user_id} ${field_to_edit}", + "back_to_user": "🔙 Informazioni utente#${user_id}", + "new_value_for_field": "Invia il nuovo valore per il campo \"${field_to_edit}\" per l'utente #${user_id}.\n\n**Nota:** il tipo di campo è \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Pausa/Riprendi un torrent", + "pause_all": "⏸ Pausa tutto", + "resume_all": "▶️ Riprendi tutto" + }, + + "pause": { + "pause_all_torrents": "Fermati tutti i torrent", + "pause_one_torrent": "Torrent in pausa" + }, + + "resume": { + "resume_all_torrents": "Ripresi tutti i torrent", + "resume_one_torrent": "Torrent Ripreso" + }, + + "delete_delete_all": { + "delete_all": "🗑 Elimina tutto" + }, + + "delete_all": { + "delete_all": "🗑 Elimina tutti i torrent", + "delete_all_data": "🗑 Elimina tutti i torrent ed i dati", + + "deleted_all": "Eliminati tutti i torrent", + "deleted_all_data": "Eliminati tutti i torrent ed i dati." + }, + + "delete_single": { + "delete_single": "🗑 Elimina torrent", + "delete_single_data": "🗑 Elimina torrent e dati" + }, + + "category": { + "add_category_btn": "➕ Aggiungi Categoria", + "remove_category_btn": "🗑 Rimuovi Categoria", + "modify_category_btn": "📝 Modifica Categoria", + + "send_category_name": "Invia il nome della categoria", + "no_categories": "Non ci sono categorie", + "choose_category": "Scegli una categoria.", + "on_category_removed": "La categoria ${category_name} è stata rimossa", + "on_category_edited": "Invia il nuovo percorso per la categoria ${category_name}" + } + } +} \ No newline at end of file diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json new file mode 100644 index 0000000..acc5e6d --- /dev/null +++ b/src/locales/ru_UA.json @@ -0,0 +1,160 @@ +{ + "lang_name": "Русский(Украина)", + "en_lang_name": "Russian(Ukraine)", + + "errors": { + "path_not_valid": "Путь не верный! Проверь и повтори", + "error": "Ошибочка: ${error}", + "command_does_not_exist": "Не понимаю что ты хош ", + "locale_not_found": "Локаль ${new_locale} не найдена!" + }, + + "on_message": { + "error_adding_magnet": "Не возможно добавить магнит", + "invalid_magnet": "Твой магнит инвалид, проверь", + "error_adding_torrent": "Не возможно добавить торрент", + "invalid_torrent": "Твой торрент инвалид, проверь", + "send_category_path": "Пж, пришли мне путь к новой категории ${category_name}" + }, + + + "common": { + "menu": "Приветули от QBittorrent Bot", + "menu_btn": { + "menu_list": "📝 Список", + "add_magnet": "➕ Добавить магнит", + "add_torrent": "➕ Добавить торрент", + "pause_resume": "⏯ Пауза\\Продолжить", + "delete": "🗑 Удалить", + "categories": "📂 Категории", + "settings": "⚙️ Настройки" + }, + + "list_filter": { + "downloading": "⏳ ${active} Загружается", + "completed": "✔️ ${active} Загружен", + "paused": "⏸️ ${active} На паузе" + }, + + "no_torrents": "Нет торрентов", + "back_to_menu_btn": "🔙 Меню" + }, + + "commands": { + "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**ВЫПОЛНЕНО**\n", + "torrent_state": "**State:** ${current_state} \n**Download Speed:** ${download_speed}/s\n", + "torrent_size": "**Размер:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Категория:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Экспортировать торрент", + "pause_torrent": "⏸ Пауза", + "resume_torrent": "▶️ Продолжить", + "delete_torrent": "🗑 Удалить" + } + }, + + "add_torrents": { + "send_magnet": "Отправь мне магнит", + "send_torrent": "Отправь мне торрент" + }, + + "settings": { + "settings_menu": "QBittorrentBot Настройки", + "settings_menu_btns": { + "users_settings": "🫂 Настройки пользователей", + "client_settings": "📥 Настройки клиента QBt", + "reload_settings": "🔄 Перезагрузить настройки" + } + }, + + "client_settings": { + "enabled": "✅ Включено", + "disabled": "❌ Выключено", + "speed_limit_status": "\n\n**Ограничение скорости**: ${speed_limit_status}", + "edit_client_settings": "Редактировать ${client_type} настройки QBt \n\n**Настройки:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Редактировать настройки QBt", + "toggle_speed_limit": "🐢 Переключить ограничение скорости", + "check_client_connection": "✅ Проверить подключение к QBt", + "back_to_settings": "🔙 Настройки" + }, + + "client_connection_ok": "✅ Подключение в норме. Версия QBt: ${version}", + "client_connection_bad": "❌ Нет подключения к QBt", + + "edit_client_setting": "Редактировать ${setting}", + "edit_client_type": "Редактировать ${client_type} ", + + "new_value_for_field": "Отправь новое значение для поля \"${field_to_edit}\" \n \n**Примечание.** тип поля: \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Настройки перезагружены" + }, + + "user_settings": { + "user_btn": "Юзер #${user_id}", + "authorized_users": "Авторизованные пользователи", + "new_locale": "Изменил язык на ${new_locale} для пользователя #${user_id}", + "edit_user_locale": "Выберый новую локаль:", + "edit_user_setting": "Edit User #${user_id}\n\n**Текущие настройки:**\n- ${confs}", + "back_to_users": "🔙 Пользователи", + "edit_user_field": "Изменить настройки пользователя #${user_id} ${field_to_edit} ", + "back_to_user": "🔙 Инфо о пользователе #${user_id} ", + "new_value_for_field": "Отправь новое значение для поля \"${field_to_edit}\" для пользователя #${user_id}. \n\n**Примечание.** тип поля: \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Пауза/Продолжить торрент", + "pause_all": "⏸ Запаузить все", + "resume_all": "▶️ Продолжить все" + }, + + "pause": { + "pause_all_torrents": "Все торренты на паузе", + "pause_one_torrent": "Торрент на паузе" + }, + + "resume": { + "resume_all_torrents": "Продолжить все торренты", + "resume_one_torrent": "Торрент снова в загрузке" + }, + + "delete_delete_all": { + "delete_all": "🗑 Удалить все" + }, + + "delete_all": { + "delete_all": "🗑 Удалить все *torrents", + "delete_all_data": "🗑 Удалить все *torrents и их файлики", + + "deleted_all": "Все торренты удалены", + "deleted_all_data": "Все файлики и их торренты удалены" + }, + + "delete_single": { + "delete_single": "🗑 Удалить торрент", + "delete_single_data": "🗑 Удалить торрент и файлики" + }, + + "category": { + "add_category_btn": "➕ Добавить категорию", + "remove_category_btn": "🗑 Удалить категорию", + "modify_category_btn": "📝 Изменить категорию", + + "send_category_name": "Пришли мне название категории", + "no_categories": "Нет категорий", + "choose_category": "Выбери категорию:", + "on_category_removed": "Категория ${category_name} удалена", + "on_category_edited": "Отправь мне новый путь к категории ${category_name}" + } + } +} \ No newline at end of file diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json new file mode 100644 index 0000000..c19e9ca --- /dev/null +++ b/src/locales/uk_UA.json @@ -0,0 +1,160 @@ +{ + "lang_name": "Українська", + "en_lang_name": "Ukrainian", + + "errors": { + "path_not_valid": "Введений шлях не існує! Перевiр та повтори спробу.", + "error": "Помилка: ${error}", + "command_does_not_exist": "Команда не існує", + "locale_not_found": "Локалізацію ${new_locale} не знайдено!" + }, + + "on_message": { + "error_adding_magnet": "Неможливо додати магніт", + "invalid_magnet": "Цей магніт недійсний! Перевiр та повтори спробу.", + "error_adding_torrent": "Неможливо додати торрент-файл", + "invalid_torrent": "Це не торрент-файл! Перевiр та повтори спробу.", + "send_category_path": "Будьласочка, надішли шлях для категорії ${category_name}" + }, + + + "common": { + "menu": "Ласкаво просимо до QBittorrent Bot", + "menu_btn": { + "menu_list": "📝 Список", + "add_magnet": "➕ Додати магніт", + "add_torrent": "➕ Додати торрент", + "pause_resume": "⏯ Пауза/Продовжити", + "delete": "🗑 Видалити", + "categories": "📂 Категорії", + "settings": "⚙️ Налаштування" + }, + + "list_filter": { + "downloading": "⏳ ${active} Завантаження", + "completed": "✔️ ${active} Завершено", + "paused": "⏸️ ${active} Призупинено" + }, + + "no_torrents": "Немає торрентів", + "back_to_menu_btn": "🔙 Меню" + }, + + "commands": { + "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**ЗАВЕРШЕНО**\n", + "torrent_state": "**Стан:** ${current_state}\n**Швидкість завантаження:** ${download_speed}/с\n\n", + "torrent_size": "**Розмір:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Категорія:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Експорт торрента", + "pause_torrent": "⏸ Пауза", + "resume_torrent": "▶️ Продовжити", + "delete_torrent": "🗑 Видалити" + } + }, + + "add_torrents": { + "send_magnet": "Надішли магніт", + "send_torrent": "Надішли торрент" + }, + + "settings": { + "settings_menu": "QBittorrentBot Налаштування", + "settings_menu_btns": { + "users_settings": "🫂 Налаштування користувачів", + "client_settings": "📥 Налаштування клієнта", + "reload_settings": "🔄 Перезавантажити налаштування" + } + }, + + "client_settings": { + "enabled": "✅ Увімкнено", + "disabled": "❌ Вимкнено", + "speed_limit_status": "\n\n**Обмеження швидкості**: ${speed_limit_status}", + "edit_client_settings": "Редагувати налаштування клієнта ${client_type}\n\n**Поточні налаштування:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Редагувати налаштування клієнта", + "toggle_speed_limit": "🐢 Перемкнути обмеження швидкості", + "check_client_connection": "✅ Перевірка підключення клієнта", + "back_to_settings": "🔙 Налаштування" + }, + + "client_connection_ok": "✅ Підключення працює. Версія QBittorrent: ${version}", + "client_connection_bad": "❌ Неможливо встановити з'єднання з QBittorrent", + + "edit_client_setting": "Редагувати ${setting}", + "edit_client_type": "Редагувати клієнта ${client_type}", + + "new_value_for_field": "Надішли нове значення для поля \"${field_to_edit}\" для клієнта.\n\n**Примітка:** тип поля - \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Налаштування перезавантажено" + }, + + "user_settings": { + "user_btn": "Користувач #${user_id}", + "authorized_users": "Авторизовані користувачі", + "new_locale": "Змінено мову на ${new_locale} для користувача №${user_id}", + "edit_user_locale": "Оберіть нову локаль:", + "edit_user_setting": "Редагувати користувача #${user_id}\n\n**Поточні налаштування:**\n- ${confs}", + "back_to_users": "🔙 Користувачі", + "edit_user_field": "Редагувати поле користувача №${user_id} ${field_to_edit}", + "back_to_user": "🔙 Інформація користувача#${user_id}", + "new_value_for_field": "Надішліть нове значення для поля \"${field_to_edit}\" для користувача №${user_id}.\n\n**Примітка:** тип поля - \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Призупинити/продовжити торрент", + "pause_all": "⏸ Призупинити все", + "resume_all": "▶️ Відновити все" + }, + + "pause": { + "pause_all_torrents": "Призупинено всі торренти", + "pause_one_torrent": "Торрент призупинено" + }, + + "resume": { + "resume_all_torrents": "Відновлено всі торренти", + "resume_one_torrent": "Торрент відновлено" + }, + + "delete_delete_all": { + "delete_all": "🗑 Видалити все" + }, + + "delete_all": { + "delete_all": "🗑 Видалити всі торренти", + "delete_all_data": "🗑 Видалити всі торренти та дані", + + "deleted_all": "Видалено всі торренти", + "deleted_all_data": "Видалено всі торренти та дані" + }, + + "delete_single": { + "delete_single": "🗑 Видалити торрент", + "delete_single_data": "🗑 Видалити торрент та дані" + }, + + "category": { + "add_category_btn": "➕ Додати категорію", + "remove_category_btn": "🗑 Видалити категорію", + "modify_category_btn": "📝 Змінити категорію", + + "send_category_name": "Надішли назву категорії", + "no_categories": "Немає категорій", + "choose_category": "Оберіть категорію.", + "on_category_removed": "Категорію ${category_name} було видалено", + "on_category_edited": "Відправ менi новий шлях для категорії ${category_name}" + } + } +} \ No newline at end of file diff --git a/src/translator/__init__.py b/src/translator/__init__.py new file mode 100644 index 0000000..b01d998 --- /dev/null +++ b/src/translator/__init__.py @@ -0,0 +1,2 @@ +from .translator import Translator +from .strings import Strings diff --git a/src/translator/strings.py b/src/translator/strings.py new file mode 100644 index 0000000..7177f4d --- /dev/null +++ b/src/translator/strings.py @@ -0,0 +1,133 @@ +from enum import StrEnum + + +class Strings(StrEnum): + LangName = "lang_name" + EnLangName = "en_lang_name" + # Errors + GenericError = "errors.error" + PathNotValid = "errors.path_not_valid" + CommandDoesNotExist = "errors.command_does_not_exist" + LocaleNotFound = "errors.locale_not_found" + + # On Message + UnableToAddMagnet = "on_message.error_adding_magnet" + InvalidMagnet = "on_message.invalid_magnet" + UnableToAddTorrent = "on_message.error_adding_torrent" + InvalidTorrent = "on_message.invalid_torrent" + CategoryPath = "on_message.send_category_path" + + # Common + MenuList = "common.menu_btn.menu_list" + AddMagnet = "common.menu_btn.add_magnet" + AddTorrent = "common.menu_btn.add_torrent" + PauseResume = "common.menu_btn.pause_resume" + Delete = "common.menu_btn.delete" + Categories = "common.menu_btn.categories" + Settings = "common.menu_btn.settings" + + Menu = "common.menu" + + ListFilterDownloading = "common.list_filter.downloading" + ListFilterCompleted = "common.list_filter.completed" + ListFilterPaused = "common.list_filter.paused" + + NoTorrents = "common.no_torrents" + BackToMenu = "common.back_to_menu_btn" + + # Commands + NotAuthorized = "commands.not_authorized" + StatsCommand = "commands.stats_command" + + ########################## CALLBACKS ################################### + # Torrent Info + TorrentCompleted = "callbacks.torrent_info.torrent_completed" + TorrentState = "callbacks.torrent_info.torrent_state" + TorrentSize = "callbacks.torrent_info.torrent_size" + TorrentEta = "callbacks.torrent_info.torrent_eta" + TorrentCategory = "callbacks.torrent_info.torrent_category" + + ExportTorrentBtn = "callbacks.torrent_info.info_btns.export_torrent" + PauseTorrentBtn = "callbacks.torrent_info.info_btns.pause_torrent" + ResumeTorrentBtn = "callbacks.torrent_info.info_btns.resume_torrent" + DeleteTorrentBtn = "callbacks.torrent_info.info_btns.delete_torrent" + + # Add Torrents + SendMagnetLink = "callbacks.add_torrents.send_magnet" + SendTorrentFile = "callbacks.add_torrents.send_torrent" + + # Settings + SettingsMenu = "callbacks.settings.settings_menu" + UsersSettings = "callbacks.settings.settings_menu_btns.users_settings" + ClientSettings = "callbacks.settings.settings_menu_btns.client_settings" + ReloadSettings = "callbacks.settings.settings_menu_btns.reload_settings" + + # Client Settings + Enabled = "callbacks.client_settings.enabled" + Disabled = "callbacks.client_settings.disabled" + SpeedLimitStatus = "callbacks.client_settings.speed_limit_status" + EditClientSettings = "callbacks.client_settings.edit_client_settings" + + EditClientSettingsBtn = "callbacks.client_settings.edit_client_settings_btns.edit_client_settings" + ToggleSpeedLimit = "callbacks.client_settings.edit_client_settings_btns.toggle_speed_limit" + CheckClientConnection = "callbacks.client_settings.edit_client_settings_btns.check_client_connection" + BackToSettings = "callbacks.client_settings.edit_client_settings_btns.back_to_settings" + + ClientConnectionOk = "callbacks.client_settings.client_connection_ok" + ClientConnectionBad = "callbacks.client_settings.client_connection_bad" + + EditClientSetting = "callbacks.client_settings.edit_client_setting" + EditClientType = "callbacks.client_settings.edit_client_type" + + NewValueForClientField = "callbacks.client_settings.new_value_for_field" + + # Reload Settings + SettingsReloaded = "callbacks.reload_settings.settings_reloaded" + + # User Settings + + UserBtn = "callbacks.user_settings.user_btn" + NewLocale = "callbacks.user_settings.new_locale" + EditLocale = "callbacks.user_settings.edit_user_locale" + AuthorizedUsers = "callbacks.user_settings.authorized_users" + EditUserSetting = "callbacks.user_settings.edit_user_setting" + BackToUsers = "callbacks.user_settings.back_to_users" + EditUserField = "callbacks.user_settings.edit_user_field" + BackToUSer = "callbacks.user_settings.back_to_user" + NewValueForUserField = "callbacks.user_settings.new_value_for_field" + + # Pause Resume + PauseResumeMenu = "callbacks.pause_resume.pause_resume_menu" + PauseAll = "callbacks.pause_resume.pause_all" + ResumeAll = "callbacks.pause_resume.resume_all" + + # Pause + PauseAllTorrents = "callbacks.pause.pause_all_torrents" + PauseTorrent = "callbacks.pause.pause_one_torrent" + + # Resume + ResumeAllTorrents = "callbacks.resume.resume_all_torrents" + ResumeTorrent = "callbacks.resume.resume_one_torrent" + + # Delete + + DeleteAllMenuBtn = "callbacks.delete_delete_all.delete_all" + DeleteAllBtn = "callbacks.delete_all.delete_all" + DeleteAllData = "callbacks.delete_all.delete_all_data" + + DeletedAll = "callbacks.delete_all.deleted_all" + DeletedAllData = "callbacks.delete_all.deleted_all_data" + + DeleteSingleBtn = "callbacks.delete_single.delete_single" + DeleteSingleDataBtn = "callbacks.delete_single.delete_single_data" + + # Categories + AddCategory = "callbacks.category.add_category_btn" + RemoveCategory = "callbacks.category.remove_category_btn" + EditCategory = "callbacks.category.modify_category_btn" + + NewCategoryName = "callbacks.category.send_category_name" + NoCategory = "callbacks.category.no_categories" + ChooseCategory = "callbacks.category.choose_category" + OnCategoryRemoved = "callbacks.category.on_category_removed" + OnCategoryEdited = "callbacks.category.on_category_edited" diff --git a/src/translator/translator.py b/src/translator/translator.py new file mode 100644 index 0000000..0a68aae --- /dev/null +++ b/src/translator/translator.py @@ -0,0 +1,39 @@ +import json +from pathlib import Path +from typing import Dict +from string import Template +from ..utils import get_value + + +def load_locales(locales_path: Path) -> Dict: + # check if format is supported + # get list of files with specific extensions + data = {} + + for file in locales_path.glob("*.json"): + # get the name of the file without extension, will be used as locale name + locale = file.stem + with open(file, 'r', encoding='utf8') as f: + data[locale] = json.load(f) + + return data + + +class Translator: + locales: Dict = load_locales(Path(__file__).parent.parent / 'locales') + + @classmethod + def translate(cls, key, locale: str = 'en', **kwargs) -> str: + # return the key instead of translation text if locale is not supported + if locale not in cls.locales: + return key + + text = get_value(cls.locales[locale], key) + + return Template(text).safe_substitute(**kwargs) + + @classmethod + def reload_locales(cls) -> Dict: + cls.locales = load_locales(Path(__file__).parent.parent / 'locales') + + return cls.locales diff --git a/src/utils.py b/src/utils.py index 6214ad4..f8612a1 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,7 +1,8 @@ from math import log, floor import datetime +from typing import Dict -from pydantic import IPvAnyAddress +from pydantic import HttpUrl from pyrogram.errors.exceptions import UserIsBlocked from src import db_management @@ -14,7 +15,7 @@ async def torrent_finished(app): repository = ClientRepo.get_client_manager(Configs.config.client.type) - for i in repository.get_torrent_info(status_filter="completed"): + for i in repository.get_torrents(status_filter="completed"): if db_management.read_completed_torrents(i.hash) is None: for user in Configs.config.users: @@ -51,11 +52,30 @@ def convert_eta(n) -> str: def convert_type_from_string(input_type: str): if "int" in input_type: return int - elif "IPvAnyAddress" in input_type: - return IPvAnyAddress + elif "HttpUrl" in input_type: + return HttpUrl elif "ClientTypeEnum" in input_type: return ClientTypeEnum elif "UserRolesEnum" in input_type: return UserRolesEnum elif "str" in input_type: return str + elif "bool" in input_type: + return bool + + +def get_value(locales_dict: Dict, key_string: str) -> str: + """Function to get value from dictionary using key strings like 'on_message.error_adding_magnet'""" + if '.' not in key_string: + return locales_dict[key_string] + else: + head, tail = key_string.split('.', 1) + return get_value(locales_dict[head], tail) + + +def inject_user(func): + async def wrapper(client, message): + user = get_user_from_config(message.from_user.id) + await func(client, message, user) + + return wrapper