diff --git a/.env.dist b/.env.dist index c1e6c4f..44d039c 100644 --- a/.env.dist +++ b/.env.dist @@ -1,2 +1,6 @@ -# Paste the bot token here and rename the file to .env +# Paste the bot token here BOT_TOKEN=123456:Your-TokEn_ExaMple +# Insert the id of administrators here, separated by commas. +# You can find out your id by writing a message to the bot https://t.me/getmyid_bot +ADMINS=First-AdminID_ExaMple,Second-AdminID_ExaMple +# Rename the file to .env diff --git a/bot.py b/bot.py index 49cab27..b666271 100644 --- a/bot.py +++ b/bot.py @@ -6,12 +6,15 @@ from aiogram.contrib.fsm_storage.memory import MemoryStorage from tgbot.config import load_config, Config +from tgbot.filters.admin import AdminFilter +from tgbot.handlers.admin import register_admin_handlers from tgbot.handlers.commands import register_commands_handlers from tgbot.handlers.error import register_errors_handlers from tgbot.handlers.messages import register_messages_handlers from tgbot.middlewares.localization import i18n from tgbot.misc.commands import set_default_commands from tgbot.misc.logger import logger +from tgbot.services.database import database_init def register_all_middlewares(dp: Dispatcher) -> None: @@ -19,8 +22,14 @@ def register_all_middlewares(dp: Dispatcher) -> None: dp.middleware.setup(i18n) +def register_all_filters(dp: Dispatcher) -> None: + """Registers filters""" + dp.filters_factory.bind(AdminFilter) + + def register_all_handlers(dp: Dispatcher) -> None: """Registers handlers""" + register_admin_handlers(dp) register_commands_handlers(dp) register_messages_handlers(dp) register_errors_handlers(dp) @@ -31,9 +40,12 @@ async def main() -> None: config: Config = load_config(path=".env") bot: Bot = Bot(token=config.tg_bot.token, parse_mode="HTML") dp: Dispatcher = Dispatcher(bot, storage=MemoryStorage()) + bot["config"] = config register_all_middlewares(dp) + register_all_filters(dp) register_all_handlers(dp) try: # On starting bot + await database_init() await set_default_commands(dp) await dp.skip_updates() await dp.start_polling() diff --git a/requirements-dev.txt b/requirements-dev.txt index 23c0ad7..7406fdd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ -mypy==1.1.1 -pylint==2.17.1 +mypy==1.2.0 +pylint==2.17.2 diff --git a/requirements.txt b/requirements.txt index 9d601c8..455cdf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ aiogram==2.25.1 +aiosqlite==0.18.0 environs==9.5.0 -pytube==12.1.2 +matplotlib==3.7.1 +pytube==12.1.3 diff --git a/tgbot/config.py b/tgbot/config.py index d0ab957..309ec1c 100644 --- a/tgbot/config.py +++ b/tgbot/config.py @@ -2,7 +2,7 @@ from os.path import join, normpath from pathlib import Path -from typing import NamedTuple, Optional +from typing import NamedTuple from environs import Env @@ -10,6 +10,7 @@ _BASE_DIR: Path = Path(__file__).resolve().parent.parent LOG_FILE: str = join(_BASE_DIR, "log.log") BOT_LOGO: str = normpath(join(_BASE_DIR, "tgbot/assets/img/bot_logo.png")) +DB_FILE: str = normpath(join(_BASE_DIR, "tgbot/db.sqlite3")) TEMP_DIR: str = normpath(join(_BASE_DIR, "tgbot/temp")) LOCALES_DIR: str = normpath(join(_BASE_DIR, "tgbot/locales")) @@ -18,6 +19,7 @@ class TgBot(NamedTuple): """Bot data""" token: str + admin_ids: tuple[int, ...] class Config(NamedTuple): @@ -26,8 +28,8 @@ class Config(NamedTuple): tg_bot: TgBot -def load_config(path: Optional[str] = None) -> Config: +def load_config(path: str | None) -> Config: """Loads settings from environment variables""" env = Env() env.read_env(path) - return Config(tg_bot=TgBot(token=env.str("BOT_TOKEN"))) + return Config(tg_bot=TgBot(token=env.str("BOT_TOKEN"), admin_ids=tuple(map(int, env.list("ADMINS"))))) diff --git a/tgbot/filters/__init__.py b/tgbot/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/filters/admin.py b/tgbot/filters/admin.py new file mode 100644 index 0000000..24cea66 --- /dev/null +++ b/tgbot/filters/admin.py @@ -0,0 +1,22 @@ +""" Allows to perform actions if the user is an administrator """ + +from aiogram.dispatcher.filters import BoundFilter +from aiogram.types import CallbackQuery, Message + +from tgbot.config import Config + + +class AdminFilter(BoundFilter): + """Allows to define administrators""" + + key = "is_admin" + + def __init__(self, is_admin: bool | None = None): + self.is_admin = is_admin + + async def check(self, obj: Message | CallbackQuery) -> bool: + """Checks if the user is an administrator""" + if self.is_admin is None: + return False + config: Config = obj.bot.get("config") + return (obj.from_user.id in config.tg_bot.admin_ids) == self.is_admin diff --git a/tgbot/handlers/admin.py b/tgbot/handlers/admin.py new file mode 100644 index 0000000..0df36ba --- /dev/null +++ b/tgbot/handlers/admin.py @@ -0,0 +1,36 @@ +"""Message handlers for administrators""" + +from os import remove as os_remove + +from aiogram import Dispatcher +from aiogram.types import InputFile, Message + +from tgbot.config import BOT_LOGO +from tgbot.middlewares.localization import i18n +from tgbot.services.database import get_downloads_data +from tgbot.services.statistics import get_path_to_statistics_graph + +_ = i18n.gettext # Alias for gettext method + + +async def if_admin_sent_command_stats(message: Message) -> None: + """Shows statistics for administrators""" + lang_code: str = message.from_user.language_code + path_to_statistics_graph: str | None = await get_path_to_statistics_graph( + downloads_data=await get_downloads_data(), locale=lang_code + ) + if path_to_statistics_graph: + await message.reply_photo( + photo=InputFile(path_to_statistics_graph), + caption="📊 " + _("Statistics of bot video downloads by months", locale=lang_code), + ) + os_remove(path_to_statistics_graph) + else: + await message.reply_photo( + photo=InputFile(BOT_LOGO), caption="❌ " + _("Error in plotting the graph", locale=lang_code) + ) + + +def register_admin_handlers(dp: Dispatcher) -> None: + """Registers admin handlers""" + dp.register_message_handler(if_admin_sent_command_stats, commands="stats", state="*", is_admin=True) diff --git a/tgbot/handlers/messages.py b/tgbot/handlers/messages.py index 35d6050..4e4b533 100644 --- a/tgbot/handlers/messages.py +++ b/tgbot/handlers/messages.py @@ -8,6 +8,7 @@ from tgbot.middlewares.localization import i18n from tgbot.misc.states import UserInput +from tgbot.services.database import increase_downloads_counter from tgbot.services.youtube import get_path_to_video_file _ = i18n.gettext # Alias for gettext method @@ -24,6 +25,7 @@ async def if_user_sent_link(message: Message) -> None: await message.reply_video(InputFile(path_to_mp4_file)) await message.bot.delete_message(chat_id=chat_id, message_id=bot_reply.message_id) os_remove(path_to_mp4_file) + await increase_downloads_counter() else: await message.bot.edit_message_text( text="❌ " + _("Unable to download this video", locale=lang_code), diff --git a/tgbot/locales/en/LC_MESSAGES/tgbot.mo b/tgbot/locales/en/LC_MESSAGES/tgbot.mo index c0c3a50..31608ae 100644 Binary files a/tgbot/locales/en/LC_MESSAGES/tgbot.mo and b/tgbot/locales/en/LC_MESSAGES/tgbot.mo differ diff --git a/tgbot/locales/en/LC_MESSAGES/tgbot.po b/tgbot/locales/en/LC_MESSAGES/tgbot.po index a5f5ba8..ec27fa7 100644 --- a/tgbot/locales/en/LC_MESSAGES/tgbot.po +++ b/tgbot/locales/en/LC_MESSAGES/tgbot.po @@ -2,15 +2,15 @@ # Copyright (C) 2022 Ringil # This file is distributed under the same license as the # YouTubeShortsDownloader project. -# FIRST AUTHOR , 2022. +# Ringil , 2022. # msgid "" msgstr "" -"Project-Id-Version: YouTubeShortsDownloader v1.0.0\n" +"Project-Id-Version: YouTubeShortsDownloader v1.1.0\n" "Report-Msgid-Bugs-To: noreply@domain.com\n" -"POT-Creation-Date: 2022-11-30 23:59+0200\n" -"PO-Revision-Date: 2022-11-30 23:59+0200\n" -"Last-Translator: FULL NAME \n" +"POT-Creation-Date: 2023-04-06 20:15+0300\n" +"PO-Revision-Date: 2023-04-06 20:18+0300\n" +"Last-Translator: Ringil \n" "Language: en\n" "Language-Team: en \n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" @@ -19,6 +19,14 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" +#: tgbot/handlers/admin.py:25 +msgid "Statistics of bot video downloads by months" +msgstr "Statistics of bot video downloads by months" + +#: tgbot/handlers/admin.py:30 +msgid "Error in plotting the graph" +msgstr "Error in plotting the graph" + #: tgbot/handlers/commands.py:15 msgid "Send a link to YouTube Shorts" msgstr "Send a link to YouTube Shorts" @@ -31,15 +39,15 @@ msgstr "I can download videos from YouTube Shorts" msgid "Error downloading the video" msgstr "Error downloading the video" -#: tgbot/handlers/messages.py:20 +#: tgbot/handlers/messages.py:21 msgid "Wait, downloading..." msgstr "Wait, downloading..." -#: tgbot/handlers/messages.py:29 +#: tgbot/handlers/messages.py:31 msgid "Unable to download this video" msgstr "Unable to download this video" -#: tgbot/handlers/messages.py:38 +#: tgbot/handlers/messages.py:40 msgid "This is not a link to YouTube Shorts" msgstr "This is not a link to YouTube Shorts" @@ -51,3 +59,58 @@ msgstr "Start" msgid "Bot info" msgstr "Bot info" +#: tgbot/services/statistics.py:29 +msgid "Jan" +msgstr "Jan" + +#: tgbot/services/statistics.py:30 +msgid "Feb" +msgstr "Feb" + +#: tgbot/services/statistics.py:31 +msgid "Mar" +msgstr "Mar" + +#: tgbot/services/statistics.py:32 +msgid "Apr" +msgstr "Apr" + +#: tgbot/services/statistics.py:33 +msgid "May" +msgstr "May" + +#: tgbot/services/statistics.py:34 +msgid "Jun" +msgstr "Jun" + +#: tgbot/services/statistics.py:35 +msgid "Jul" +msgstr "Jul" + +#: tgbot/services/statistics.py:36 +msgid "Aug" +msgstr "Aug" + +#: tgbot/services/statistics.py:37 +msgid "Sep" +msgstr "Sep" + +#: tgbot/services/statistics.py:38 +msgid "Oct" +msgstr "Oct" + +#: tgbot/services/statistics.py:39 +msgid "Nov" +msgstr "Nov" + +#: tgbot/services/statistics.py:40 +msgid "Dec" +msgstr "Dec" + +#: tgbot/services/statistics.py:62 +msgid "Downloads chart by dates" +msgstr "Downloads chart by dates" + +#: tgbot/services/statistics.py:63 +msgid "Number of downloads" +msgstr "Number of downloads" diff --git a/tgbot/locales/ru/LC_MESSAGES/tgbot.mo b/tgbot/locales/ru/LC_MESSAGES/tgbot.mo index d45c3d6..75a401e 100644 Binary files a/tgbot/locales/ru/LC_MESSAGES/tgbot.mo and b/tgbot/locales/ru/LC_MESSAGES/tgbot.mo differ diff --git a/tgbot/locales/ru/LC_MESSAGES/tgbot.po b/tgbot/locales/ru/LC_MESSAGES/tgbot.po index 2f6d92a..5afb896 100644 --- a/tgbot/locales/ru/LC_MESSAGES/tgbot.po +++ b/tgbot/locales/ru/LC_MESSAGES/tgbot.po @@ -2,15 +2,15 @@ # Copyright (C) 2022 Ringil # This file is distributed under the same license as the # YouTubeShortsDownloader project. -# FIRST AUTHOR , 2022. +# Ringil , 2022. # msgid "" msgstr "" -"Project-Id-Version: YouTubeShortsDownloader v1.0.0\n" +"Project-Id-Version: YouTubeShortsDownloader v1.1.0\n" "Report-Msgid-Bugs-To: noreply@domain.com\n" -"POT-Creation-Date: 2022-11-30 23:59+0200\n" -"PO-Revision-Date: 2022-11-30 23:59+0200\n" -"Last-Translator: FULL NAME \n" +"POT-Creation-Date: 2023-04-06 20:15+0300\n" +"PO-Revision-Date: 2023-04-06 20:18+0300\n" +"Last-Translator: Ringil \n" "Language: ru\n" "Language-Team: ru \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -20,6 +20,14 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" +#: tgbot/handlers/admin.py:25 +msgid "Statistics of bot video downloads by months" +msgstr "Статистика загрузок видео с ботом по месяцам" + +#: tgbot/handlers/admin.py:30 +msgid "Error in plotting the graph" +msgstr "Ошибка при построении графика" + #: tgbot/handlers/commands.py:15 msgid "Send a link to YouTube Shorts" msgstr "Отправь мне ссылку на YouTube Shorts" @@ -32,15 +40,15 @@ msgstr "Я могу загружать видео с YouTube Shorts" msgid "Error downloading the video" msgstr "Ошибка при загрузке видео" -#: tgbot/handlers/messages.py:20 +#: tgbot/handlers/messages.py:21 msgid "Wait, downloading..." msgstr "Подожди, загружаю..." -#: tgbot/handlers/messages.py:29 +#: tgbot/handlers/messages.py:31 msgid "Unable to download this video" msgstr "Невозможно загрузить это видео" -#: tgbot/handlers/messages.py:38 +#: tgbot/handlers/messages.py:40 msgid "This is not a link to YouTube Shorts" msgstr "Это не ссылка на YouTube Shorts" @@ -52,3 +60,58 @@ msgstr "Старт" msgid "Bot info" msgstr "Информация о боте" +#: tgbot/services/statistics.py:29 +msgid "Jan" +msgstr "Янв" + +#: tgbot/services/statistics.py:30 +msgid "Feb" +msgstr "Фев" + +#: tgbot/services/statistics.py:31 +msgid "Mar" +msgstr "Мар" + +#: tgbot/services/statistics.py:32 +msgid "Apr" +msgstr "Апр" + +#: tgbot/services/statistics.py:33 +msgid "May" +msgstr "Май" + +#: tgbot/services/statistics.py:34 +msgid "Jun" +msgstr "Июн" + +#: tgbot/services/statistics.py:35 +msgid "Jul" +msgstr "Июл" + +#: tgbot/services/statistics.py:36 +msgid "Aug" +msgstr "Авг" + +#: tgbot/services/statistics.py:37 +msgid "Sep" +msgstr "Сен" + +#: tgbot/services/statistics.py:38 +msgid "Oct" +msgstr "Окт" + +#: tgbot/services/statistics.py:39 +msgid "Nov" +msgstr "Ноя" + +#: tgbot/services/statistics.py:40 +msgid "Dec" +msgstr "Дек" + +#: tgbot/services/statistics.py:62 +msgid "Downloads chart by dates" +msgstr "График загрузок по датам" + +#: tgbot/services/statistics.py:63 +msgid "Number of downloads" +msgstr "Количество загрузок" diff --git a/tgbot/locales/tgbot.pot b/tgbot/locales/tgbot.pot index 3658d46..fba40ed 100644 --- a/tgbot/locales/tgbot.pot +++ b/tgbot/locales/tgbot.pot @@ -2,22 +2,30 @@ # Copyright (C) 2022 Ringil # This file is distributed under the same license as the # YouTubeShortsDownloader project. -# FIRST AUTHOR , 2022. +# Ringil , 2022. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: YouTubeShortsDownloader v1.0.0\n" +"Project-Id-Version: YouTubeShortsDownloader v1.1.0\n" "Report-Msgid-Bugs-To: noreply@domain.com\n" -"POT-Creation-Date: 2022-11-30 23:59+0200\n" +"POT-Creation-Date: 2023-04-06 20:15+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" +"Last-Translator: Ringil \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" +#: tgbot/handlers/admin.py:25 +msgid "Statistics of bot video downloads by months" +msgstr "" + +#: tgbot/handlers/admin.py:30 +msgid "Error in plotting the graph" +msgstr "" + #: tgbot/handlers/commands.py:15 msgid "Send a link to YouTube Shorts" msgstr "" @@ -26,15 +34,19 @@ msgstr "" msgid "I can download videos from YouTube Shorts" msgstr "" -#: tgbot/handlers/messages.py:20 +#: tgbot/handlers/error.py:18 +msgid "Error downloading the video" +msgstr "" + +#: tgbot/handlers/messages.py:21 msgid "Wait, downloading..." msgstr "" -#: tgbot/handlers/messages.py:29 +#: tgbot/handlers/messages.py:31 msgid "Unable to download this video" msgstr "" -#: tgbot/handlers/messages.py:38 +#: tgbot/handlers/messages.py:40 msgid "This is not a link to YouTube Shorts" msgstr "" @@ -46,3 +58,58 @@ msgstr "" msgid "Bot info" msgstr "" +#: tgbot/services/statistics.py:29 +msgid "Jan" +msgstr "" + +#: tgbot/services/statistics.py:30 +msgid "Feb" +msgstr "" + +#: tgbot/services/statistics.py:31 +msgid "Mar" +msgstr "" + +#: tgbot/services/statistics.py:32 +msgid "Apr" +msgstr "" + +#: tgbot/services/statistics.py:33 +msgid "May" +msgstr "" + +#: tgbot/services/statistics.py:34 +msgid "Jun" +msgstr "" + +#: tgbot/services/statistics.py:35 +msgid "Jul" +msgstr "" + +#: tgbot/services/statistics.py:36 +msgid "Aug" +msgstr "" + +#: tgbot/services/statistics.py:37 +msgid "Sep" +msgstr "" + +#: tgbot/services/statistics.py:38 +msgid "Oct" +msgstr "" + +#: tgbot/services/statistics.py:39 +msgid "Nov" +msgstr "" + +#: tgbot/services/statistics.py:40 +msgid "Dec" +msgstr "" + +#: tgbot/services/statistics.py:62 +msgid "Downloads chart by date" +msgstr "" + +#: tgbot/services/statistics.py:63 +msgid "Number of downloads" +msgstr "" diff --git a/tgbot/locales/uk/LC_MESSAGES/tgbot.mo b/tgbot/locales/uk/LC_MESSAGES/tgbot.mo index 0df97aa..d7258bc 100644 Binary files a/tgbot/locales/uk/LC_MESSAGES/tgbot.mo and b/tgbot/locales/uk/LC_MESSAGES/tgbot.mo differ diff --git a/tgbot/locales/uk/LC_MESSAGES/tgbot.po b/tgbot/locales/uk/LC_MESSAGES/tgbot.po index 96e29f1..5b2fceb 100644 --- a/tgbot/locales/uk/LC_MESSAGES/tgbot.po +++ b/tgbot/locales/uk/LC_MESSAGES/tgbot.po @@ -2,15 +2,15 @@ # Copyright (C) 2022 Ringil # This file is distributed under the same license as the # YouTubeShortsDownloader project. -# FIRST AUTHOR , 2022. +# Ringil , 2022. # msgid "" msgstr "" -"Project-Id-Version: YouTubeShortsDownloader v1.0.0\n" +"Project-Id-Version: YouTubeShortsDownloader v1.1.0\n" "Report-Msgid-Bugs-To: noreply@domain.com\n" -"POT-Creation-Date: 2022-11-30 23:59+0200\n" -"PO-Revision-Date: 2022-11-30 23:59+0200\n" -"Last-Translator: FULL NAME \n" +"POT-Creation-Date: 2023-04-06 20:15+0300\n" +"PO-Revision-Date: 2023-04-06 20:18+0300\n" +"Last-Translator: Ringil \n" "Language: uk\n" "Language-Team: uk \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -20,6 +20,14 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" +#: tgbot/handlers/admin.py:25 +msgid "Statistics of bot video downloads by months" +msgstr "Статистика завантажень відео ботом по місяцях" + +#: tgbot/handlers/admin.py:30 +msgid "Error in plotting the graph" +msgstr "Помилка при побудові графіка" + #: tgbot/handlers/commands.py:15 msgid "Send a link to YouTube Shorts" msgstr "Надішли мені посилання на YouTube Shorts" @@ -32,15 +40,15 @@ msgstr "Я можу завантажувати відео з YouTube Shorts" msgid "Error downloading the video" msgstr "Помилка при завантаженні відео" -#: tgbot/handlers/messages.py:20 +#: tgbot/handlers/messages.py:21 msgid "Wait, downloading..." msgstr "Зачекай, завантажую..." -#: tgbot/handlers/messages.py:29 +#: tgbot/handlers/messages.py:31 msgid "Unable to download this video" msgstr "Неможливо завантажити це відео" -#: tgbot/handlers/messages.py:38 +#: tgbot/handlers/messages.py:40 msgid "This is not a link to YouTube Shorts" msgstr "Це не посилання на YouTube Shorts" @@ -52,3 +60,59 @@ msgstr "Початок" msgid "Bot info" msgstr "Інформація про бота" +#: tgbot/services/statistics.py:29 +msgid "Jan" +msgstr "Січ" + +#: tgbot/services/statistics.py:30 +msgid "Feb" +msgstr "Лют" + +#: tgbot/services/statistics.py:31 +msgid "Mar" +msgstr "Бер" + +#: tgbot/services/statistics.py:32 +msgid "Apr" +msgstr "Кві" + +#: tgbot/services/statistics.py:33 +msgid "May" +msgstr "Тра" + +#: tgbot/services/statistics.py:34 +msgid "Jun" +msgstr "Чер" + +#: tgbot/services/statistics.py:35 +msgid "Jul" +msgstr "Лип" + +#: tgbot/services/statistics.py:36 +msgid "Aug" +msgstr "Сер" + +#: tgbot/services/statistics.py:37 +msgid "Sep" +msgstr "Вер" + +#: tgbot/services/statistics.py:38 +msgid "Oct" +msgstr "Жов" + +#: tgbot/services/statistics.py:39 +msgid "Nov" +msgstr "Лис" + +#: tgbot/services/statistics.py:40 +msgid "Dec" +msgstr "Гру" + +#: tgbot/services/statistics.py:62 +msgid "Downloads chart by dates" +msgstr "Графік завантажень за датами" + +#: tgbot/services/statistics.py:63 +msgid "Number of downloads" +msgstr "Кількість завантажень" + diff --git a/tgbot/services/database.py b/tgbot/services/database.py new file mode 100644 index 0000000..5494faf --- /dev/null +++ b/tgbot/services/database.py @@ -0,0 +1,60 @@ +"""Functions for working with the database""" + +from datetime import datetime +from sqlite3 import OperationalError +from sys import exit as sys_exit + +from aiosqlite import connect + +from tgbot.config import DB_FILE +from tgbot.misc.logger import logger +from tgbot.services.statistics import DownloadsData + + +async def database_init() -> None: + """Creates a database file and a table in it""" + try: + async with connect(database=DB_FILE) as db: + await db.execute( + """ + CREATE TABLE IF NOT EXISTS downloads_counters ( + date VARCHAR(7) PRIMARY KEY, + counter INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + except OperationalError as ex: + logger.critical("Database connection error: %s", ex) + sys_exit() + + +async def get_downloads_data() -> DownloadsData: + """Returns the number of YouTube Shorts uploads made by the bot in the last 12 months""" + dates: list = [] + downloads: list = [] + async with connect(database=DB_FILE) as db: + async with db.execute( + """ + SELECT date, counter + FROM downloads_counters + ORDER BY date DESC + LIMIT 12; + """ + ) as cursor: + async for row in cursor: + dates.append(row[0]) + downloads.append(row[1]) + return DownloadsData(date=dates, downloads_counter=downloads) + + +async def increase_downloads_counter() -> None: + """Increases the value of the YouTube Shorts download counter""" + async with connect(database=DB_FILE) as db: + await db.execute( + """ + INSERT INTO downloads_counters (date, counter) VALUES (?, ?) + ON CONFLICT (date) DO UPDATE SET counter=counter+1; + """, + (datetime.now().strftime("%Y.%m"), 1), + ) + await db.commit() diff --git a/tgbot/services/statistics.py b/tgbot/services/statistics.py new file mode 100644 index 0000000..58ec387 --- /dev/null +++ b/tgbot/services/statistics.py @@ -0,0 +1,86 @@ +"""Functions for graphing downloads in the bot and saving the graph image""" + +from asyncio import get_running_loop +from os import makedirs, path +from typing import NamedTuple + +import matplotlib.pyplot as plt +from matplotlib.axes import Axes +from matplotlib.container import BarContainer +from matplotlib.figure import Figure + +from tgbot.config import TEMP_DIR +from tgbot.middlewares.localization import i18n +from tgbot.misc.logger import logger + +_ = i18n.gettext # Alias for gettext method + + +class DownloadsData(NamedTuple): + """Represents a counter of downloads by months""" + + date: list[str] + downloads_counter: list[int] + + +def _format_date(date: str, locale: str) -> str: + """Returns the formatted date value in the user's local language""" + month_in_user_local_language: dict[str, str] = { + "01": _("Jan", locale=locale), + "02": _("Feb", locale=locale), + "03": _("Mar", locale=locale), + "04": _("Apr", locale=locale), + "05": _("May", locale=locale), + "06": _("Jun", locale=locale), + "07": _("Jul", locale=locale), + "08": _("Aug", locale=locale), + "09": _("Sep", locale=locale), + "10": _("Oct", locale=locale), + "11": _("Nov", locale=locale), + "12": _("Dec", locale=locale), + } + return f"{month_in_user_local_language.get(date[5:])}\n{date[:4]}" + + +def plot_download_graph(downloads_data: DownloadsData, locale: str) -> str | None: + """Builds an image of the download graph and saves it to a file""" + try: + figure: Figure = plt.figure(figsize=(8, 4.5)) + axes: Axes = figure.add_subplot() + + # Let's get the data for the graph + downloads_counter: list[int] = downloads_data.downloads_counter + dates: list[str] = [_format_date(date=date, locale=locale) for date in downloads_data.date] + range_of_dates: range = range(len(dates)) # Number of values on the abscissa axis + + # Making a chart + bar_container: BarContainer = axes.bar(range_of_dates, downloads_counter) + axes.bar_label(container=bar_container) + axes.invert_xaxis() + + # Add explanatory labels + axes.set_title(label=_("Downloads chart by dates", locale=locale)) + axes.set_ylabel(ylabel=_("Number of downloads", locale=locale)) + axes.set_yticks([]) # Remove labels on the ordinate axis + plt.xticks(range_of_dates, dates) # Add the labels on the abscissa axis + + # Save the chart to a file + path_to_statistics_graph: str = path.join(TEMP_DIR, "stats.png") + figure.savefig(path.join(TEMP_DIR, "stats.png")) + + return path_to_statistics_graph + + except Exception as ex: + logger.info("Error in plotting the graph: %s", ex) + + return None + + +async def get_path_to_statistics_graph(downloads_data: DownloadsData, locale: str) -> str | None: + """Returns the path to the graph image""" + if not path.exists(TEMP_DIR): + makedirs(TEMP_DIR) + path_to_graph_image: str | None = await get_running_loop().run_in_executor( + None, plot_download_graph, downloads_data, locale + ) + return path_to_graph_image diff --git a/tgbot/services/youtube.py b/tgbot/services/youtube.py index b57ad20..08ed3ee 100644 --- a/tgbot/services/youtube.py +++ b/tgbot/services/youtube.py @@ -21,12 +21,7 @@ def _remove_unwanted_chars(string: str) -> str: def download_video(url: str) -> str | None: - """ - Downloads videos from YouTube via a link - - :param url: link to YouTube Shorts video - :return: path to downloaded mp4 file or None - """ + """Downloads videos from YouTube via a link""" try: youtube_video: YouTube = YouTube(url=url) video_stream: Stream = youtube_video.streams.get_highest_resolution()