import os from dotenv import load_dotenv import logging import logging.config from pprint import pprint import pandas as pd from transliterate import translit import requests from time import sleep import datetime import sys from urllib.parse import urlparse from pathlib import PureWindowsPath import telebot from telebot import types from synology_drive_api.drive import SynologyDrive from flask import Flask, jsonify import threading from functools import wraps # Инициализация Flask приложения для панели мониторинга flask_app = Flask(__name__) flask_app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET', 'default-secret-key') # Загрузка переменных окружения load_dotenv('AF_environment.env') class Monitoring: """Класс для сбора статистики и мониторинга""" def __init__(self): self.jobs_history = [] self.system_stats = { 'total_jobs': 0, 'successful_jobs': 0, 'failed_jobs': 0, 'active_jobs': 0, 'users': {} } def add_job(self, job_data): """Добавление информации о новой задаче""" self.jobs_history.append(job_data) self.system_stats['total_jobs'] += 1 self.system_stats['active_jobs'] += 1 user_id = job_data.get('user_id') if user_id: if user_id not in self.system_stats['users']: self.system_stats['users'][user_id] = { 'total_jobs': 0, 'successful_jobs': 0, 'failed_jobs': 0 } self.system_stats['users'][user_id]['total_jobs'] += 1 def job_completed(self, job_id, success=True, user_id=None): """Обновление статуса завершенной задачи""" self.system_stats['active_jobs'] -= 1 if success: self.system_stats['successful_jobs'] += 1 else: self.system_stats['failed_jobs'] += 1 if user_id and user_id in self.system_stats['users']: if success: self.system_stats['users'][user_id]['successful_jobs'] += 1 else: self.system_stats['users'][user_id]['failed_jobs'] += 1 # Обновляем статус в истории for job in self.jobs_history: if job.get('job_id') == job_id: job['status'] = 'completed' if success else 'failed' job['completed_at'] = datetime.datetime.now() break def get_stats(self): """Получение текущей статистики""" return self.system_stats def get_recent_jobs(self, limit=10): """Получение последних задач""" return self.jobs_history[-limit:] if self.jobs_history else [] def get_user_stats(self, user_id): """Получение статистики по конкретному пользователю""" return self.system_stats['users'].get(user_id, { 'total_jobs': 0, 'successful_jobs': 0, 'failed_jobs': 0 }) class Config: """Класс для работы с конфигурацией приложения""" def __init__(self): self.nas_user = os.getenv('NAS_USER') self.nas_pass = os.getenv('NAS_PASS') self.nas_ip = os.getenv('NAS_IP') self.nas_port = os.getenv('NAS_PORT') self.nas_file = os.getenv('NAS_FILE') self.token = os.getenv('TELEGRAM_TOKEN') self.group_chat = os.getenv('TELEGRAM_GROUP_CHAT') self.nexrender_url = os.getenv('NEXRENDER_URL') self.admin_password = os.getenv('ADMIN_PASSWORD', 'admin123') self._validate_config() def _validate_config(self): """Проверка наличия обязательных переменных окружения""" required_vars = { 'NAS_USER': self.nas_user, 'NAS_PASS': self.nas_pass, 'NAS_IP': self.nas_ip, 'TELEGRAM_TOKEN': self.token, 'NEXRENDER_URL': self.nexrender_url } missing = [k for k, v in required_vars.items() if not v] if missing: raise ValueError(f"Отсутствуют обязательные переменные окружения: {', '.join(missing)}") class JobManager: """Основной класс для управления задачами рендеринга""" def __init__(self, config, monitoring): self.config = config self.monitoring = monitoring self.bot = telebot.TeleBot(config.token) self.PLACEHOLDER = sys.platform == 'win32' self.setup_logging() self.setup_handlers() if self.PLACEHOLDER: self._init_placeholders() def _init_placeholders(self): """Инициализация заглушек для тестирования""" from random import random, choices def send_job_dumb(data): if random() < 0.8: uid = ''.join(choices('abcdefghijklmnopqrstuvwxyz_', k=8)) return {'uid': uid, 'outname': data['outfile_name']} return None class FakeResp: def __init__(self, state='queued', *args, **kwargs): self.state = state self.status_code = 200 def json(self): return {'state': self.state} def fake_get(*args, **kwargs): rand = random() if rand < 0.8: return FakeResp() elif rand < 0.95: return FakeResp('finished') else: return FakeResp('error') self._send_job_real = self.send_job self.send_job = send_job_dumb self._fake_get = fake_get def setup_logging(self): """Настройка системы логирования""" LOG_CONFIG = { 'version': 1, 'formatters': { 'detailed': { 'format': '%(asctime)s %(levelname)-8s %(name)-15s %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S' } }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'INFO', 'formatter': 'detailed' }, 'file': { 'class': 'logging.FileHandler', 'filename': 'af_bot.log', 'mode': 'a', 'level': 'DEBUG', 'formatter': 'detailed' } }, 'loggers': { '': { 'handlers': ['console', 'file'], 'level': 'DEBUG', 'propagate': True } } } logging.config.dictConfig(LOG_CONFIG) self.logger = logging.getLogger(__name__) def setup_handlers(self): """Настройка обработчиков команд Telegram""" @self.bot.message_handler(commands=['start', 'help', 'menu']) def send_welcome(message): self.show_main_menu(message) @self.bot.message_handler(func=lambda message: True) def handle_text(message): if message.text == '📊 Статистика': self.show_stats(message) elif message.text == '🔄 Создать анонс': self.start_ibash(message) elif message.text == '❌ Отменить задачи': self.cancel_jobs(message) elif message.text == '👤 Моя статистика': self.show_user_stats(message) else: self.bot.reply_to(message, "Используйте меню для навигации") @self.bot.callback_query_handler(func=lambda call: True) def handle_callback(call): if call.data == 'ibash_all': self.process_ibash(call.message, all_announcements=True) elif call.data == 'ibash_new': self.process_ibash(call.message, all_announcements=False) elif call.data == 'cancel': self.bot.edit_message_text( "Действие отменено", call.message.chat.id, call.message.message_id ) self.show_main_menu(call.message) def show_main_menu(self, message): """Отображение главного меню с кнопками""" markup = types.ReplyKeyboardMarkup( row_width=2, resize_keyboard=True, one_time_keyboard=False ) buttons = [ types.KeyboardButton('🔄 Создать анонс'), types.KeyboardButton('📊 Статистика'), types.KeyboardButton('👤 Моя статистика'), types.KeyboardButton('❌ Отменить задачи') ] markup.add(*buttons) self.bot.send_message( message.chat.id, "📱 *Главное меню*:\nВыберите действие:", reply_markup=markup, parse_mode='Markdown' ) def show_stats(self, message): """Отображение статистики системы""" stats = self.monitoring.get_stats() recent_jobs = self.monitoring.get_recent_jobs(5) stats_text = ( "📈 *Статистика системы*\n\n" f"• Всего задач: {stats['total_jobs']}\n" f"• Успешных: {stats['successful_jobs']}\n" f"• Неудачных: {stats['failed_jobs']}\n" f"• Активных: {stats['active_jobs']}\n\n" "⏱ *Последние задачи*:\n" ) for job in recent_jobs: status_icon = '✅' if job.get('status') == 'completed' else '❌' if job.get('status') == 'failed' else '🔄' stats_text += f"{status_icon} {job.get('name', 'N/A')} ({job.get('user', 'system')})\n" self.bot.send_message( message.chat.id, stats_text, parse_mode='Markdown' ) def show_user_stats(self, message): """Отображение статистики пользователя""" user_id = message.from_user.id username = message.from_user.username or message.from_user.first_name user_stats = self.monitoring.get_user_stats(user_id) stats_text = ( f"👤 *Ваша статистика* ({username})\n\n" f"• Всего задач: {user_stats['total_jobs']}\n" f"• Успешных: {user_stats['successful_jobs']}\n" f"• Неудачных: {user_stats['failed_jobs']}\n" f"• Процент успеха: {user_stats['successful_jobs'] / user_stats['total_jobs'] * 100:.1f}%" if user_stats['total_jobs'] > 0 else "0%" ) self.bot.send_message( message.chat.id, stats_text, parse_mode='Markdown' ) def start_ibash(self, message): """Начало процесса создания анонсов с интерактивным меню""" self.logger.info(f"Start ibash requested by {message.from_user.username}") markup = types.InlineKeyboardMarkup() markup.row( types.InlineKeyboardButton("Все анонсы", callback_data="ibash_all"), types.InlineKeyboardButton("Только новые", callback_data="ibash_new") ) markup.row(types.InlineKeyboardButton("Отмена", callback_data="cancel")) self.bot.send_message( message.chat.id, "🔧 *Создание анонсов*\n\nВыберите тип обработки:", reply_markup=markup, parse_mode='Markdown' ) def process_ibash(self, message, all_announcements=False): """Обработка создания анонсов""" user_id = message.from_user.id username = message.from_user.username or message.from_user.first_name self.bot.send_chat_action(message.chat.id, 'typing') try: # Загрузка данных osheet = self.load_osheet(message) start = self.get_sheet_data(osheet, 'Start', header=1) if not all_announcements: start = start[start['STATE'] == False] # Только новые анонсы pack = self.get_sheet_data(osheet, 'SPORT', header=0, index_col='SPORT') pack = pack[pack.index.notna()] logos = self.get_sheet_data(osheet, 'TEAMS', header=0, index_col=[0, 1]) # Очистка старых задач self.cleanup_old_jobs() # Создание задач self.bot.send_chat_action(message.chat.id, 'record_video') watch_list = [] for i, row in start.iterrows(): dd = self.make_data_dict(row) jobs = self.make_job_dicts(dd, pack, logos, message) for job in jobs: if job: job_data = { 'job_id': job['uid'], 'name': job['outname'], 'user_id': user_id, 'user': username, 'status': 'started', 'started_at': datetime.datetime.now() } self.monitoring.add_job(job_data) watch_list.append(job) self.logger.info(f"В очереди {len(watch_list)} задач") self.bot.send_message( message.chat.id, f"🚀 Запущено {len(watch_list)} задач на рендеринг", parse_mode='Markdown' ) # Отслеживание выполнения self.track_jobs(message, watch_list, user_id) except Exception as e: self.logger.exception("Ошибка в process_ibash") self.bot.send_message( message.chat.id, f"❌ Ошибка при создании анонсов: {str(e)}", parse_mode='Markdown' ) def track_jobs(self, message, watch_list, user_id): """Отслеживание выполнения задач""" while watch_list: self.bot.send_chat_action(message.chat.id, 'record_video') sleep(25) for job in watch_list[:]: try: if self.PLACEHOLDER: r = self._fake_get() else: r = requests.get(f"{self.config.nexrender_url}/{job['uid']}") if r.status_code == 200: state = r.json()['state'] if state == 'finished': watch_list.remove(job) self.monitoring.job_completed(job['uid'], True, user_id) self.logger.info(f"{job['outname']} готов, осталось {len(watch_list)}") self.bot.send_message( message.chat.id, f"✅ *{job['outname']}* готов\nОсталось задач: {len(watch_list)}", parse_mode='Markdown' ) elif state == 'error': watch_list.remove(job) self.monitoring.job_completed(job['uid'], False, user_id) self.logger.warning(f"{job['outname']} завершился с ошибкой") self.bot.send_message( message.chat.id, f"❌ *{job['outname']}* завершился с ошибкой", parse_mode='Markdown' ) except Exception as e: self.logger.error(f"Ошибка проверки статуса задачи {job['uid']}: {e}") self.bot.send_message( message.chat.id, "🎉 Все задачи завершены!", reply_markup=types.ReplyKeyboardRemove(), parse_mode='Markdown' ) self.show_main_menu(message) def cleanup_old_jobs(self): """Очистка завершенных задач""" try: r = requests.get(self.config.nexrender_url) if r.status_code == 200: jobs = r.json() for job in jobs: if job['state'] in ('finished', 'error'): requests.delete(f"{self.config.nexrender_url}/{job['uid']}") except Exception as e: self.logger.error(f"Ошибка очистки старых задач: {e}") def cancel_jobs(self, message): """Отмена всех активных задач""" try: r = requests.get(self.config.nexrender_url) if r.status_code == 200: jobs = r.json() cancelled = 0 for job in jobs: if job['state'] in ('queued', 'picked'): requests.delete(f"{self.config.nexrender_url}/{job['uid']}") cancelled += 1 self.logger.info(f"Отменено {cancelled} задач") self.bot.send_message( message.chat.id, f"⏹ Отменено {cancelled} активных задач", parse_mode='Markdown' ) else: self.bot.send_message( message.chat.id, "⚠ Не удалось получить список задач для отмены", parse_mode='Markdown' ) except Exception as e: self.logger.error(f"Ошибка отмены задач: {e}") self.bot.send_message( message.chat.id, f"❌ Ошибка при отмене задач: {str(e)}", parse_mode='Markdown' ) def load_osheet(self, message): """Загрузка файла с Synology Drive""" self.logger.debug('Получение данных') try: synd = SynologyDrive( self.config.nas_user, self.config.nas_pass, self.config.nas_ip, self.config.nas_port, https=True, dsm_version='7' ) self.logger.debug(synd.login()) # Проверка сессии try: self.logger.debug('Попытка загрузки таблицы') bio = synd.download_synology_office_file(self.config.nas_file) self.logger.debug('Успешная загрузка') return bio except Exception as e: self.logger.exception('Ошибка загрузки') self.bot.send_message( message.chat.id, 'Не удалось скачать таблицу', parse_mode='html' ) raise except Exception as e: self.logger.exception('Ошибка авторизации') self.bot.send_message( message.chat.id, 'Не удалось авторизоваться', parse_mode='html' ) raise def get_sheet_data(self, osheet, sheet_name, **kwargs): """Получение данных из листа Excel""" self.logger.debug(f'Чтение листа {sheet_name}') try: sheet = pd.read_excel(osheet, sheet_name=sheet_name, **kwargs) self.logger.debug('Успешное чтение') return sheet except Exception as e: self.logger.exception(f'Ошибка чтения листа {sheet_name}') raise def get_sport_logo(self, sport, pack, message): """Получение логотипа вида спорта""" self.logger.info(f'Получение оформления для {sport}') self.bot.send_message( message.chat.id, f'Ищем оформления для {sport}', parse_mode='html' ) try: d = pack.loc[sport]['LINK'] self.logger.debug(d) if pd.isna(d): self.logger.warning(f'Нет LINK для вида спорта "{sport}"') return '' return d except Exception as e: self.logger.exception(f"Не удалось получить оформление для {sport}") return '' def get_team_logo(self, team, sport, logos, message): """Получение логотипа команды""" self.logger.info(f'Получение логотипа {team}/{sport}') self.bot.send_message( message.chat.id, f'Поиск логотипа {sport}-{team}', parse_mode='html' ) try: d = logos.loc[team, sport]['LINK'] self.logger.debug(d) return d except KeyError: self.logger.warning(f"Нет LINK для {team}/{sport}") return '' except Exception as e: self.logger.exception(f"Ошибка при получении логотипа {sport}") return '' def make_data_dict(self, ds): """Создание словаря с данными""" return { 'date': ds['DATA'], 'time': ds['TIME'], 'channel': ds['CHANEL'], 'sport': ds['SPORT'], 'league': ds['LEAGUE'], 'team_a': ds['TEAM A'], 'team_b': ds['TEAM B'], 'index': ds.name } def unc2uri(self, unc): """Преобразование UNC пути в URI""" self.logger.debug('Преобразование пути') try: p = urlparse(unc) if len(p.scheme) > 2 or not unc: return unc else: p = PureWindowsPath(unc) return p.as_uri() except Exception as e: self.logger.exception('Ошибка преобразования пути') return unc def send_job(self, data, message): """Отправка задачи на рендеринг""" if self.PLACEHOLDER: return self._send_job_dumb(data) payload = { "template": { "src": "file:///c:/users/virtVmix-2/Downloads/PackShot_Sborka_eng.aepx", "composition": "pack", "outputModule": "Start_h264", "outputExt": "mp4" }, "actions": { "postrender": [ { "module": "@nexrender/action-encode", "preset": "mp4", "output": "encoded.mp4" }, { "module": "@nexrender/action-copy", "input": "encoded.mp4", "output": f"//10.10.35.3/edit/Auto_Anons/{data['outfile_name']}.mp4" } ] }, "assets": [] } # Добавление данных в payload self._add_data_to_payload(payload, data, message) url = self.config.nexrender_url try: r = requests.post(url, json=payload) if r.status_code == 200: res = r.json() uid = res['uid'] return {'uid': uid, 'outname': data['outfile_name']} except Exception as e: self.logger.exception('Ошибка отправки задачи') return None def _add_data_to_payload(self, payload, data, message): """Добавление данных в payload""" # Добавление даты self._add_date_data(payload, data, message) # Добавление времени self._add_time_data(payload, data, message) # Добавление лиги payload['assets'].append({ "type": "data", "layerName": "LEAGUE", "property": "Source Text", "value": data['league'] }) # Добавление спорта if data['sport']: payload['assets'].append({ "type": "data", "layerName": "SPORT", "property": "Source Text", "value": data['sport'] }) # Добавление команд и логотипов self._add_team_data(payload, data, message, 'A') self._add_team_data(payload, data, message, 'B') # Добавление оформления if data['pack']: payload['assets'].append({ "src": data['pack'], "type": "video", "layerName": "TOP" }) def _add_date_data(self, payload, data, message): """Добавление данных о дате""" if data['data'] == 'сегодня': self._add_specific_date_style(payload, data, message, "105", [0, 5]) elif data['data'] == 'завтра': self._add_specific_date_style(payload, data, message, "115", [0, 25]) elif len(data['data']) < 6: self._add_specific_date_style(payload, data, message, "120", [0, 20]) payload['assets'].append({ "type": "data", "layerName": "DATA", "property": "Source Text", "value": data['data'] }) def _add_specific_date_style(self, payload, data, message, font_size, anchor_point): """Добавление стилей для конкретной даты""" payload['assets'].extend([ { "layerName": "DATA", "property": "Source Text.fontSize", "type": "data", "value": font_size }, { "layerName": "DATA", "property": "transform.anchorPoint", "type": "data", "value": anchor_point } ]) self.logger.info(f'Для "{data["data"]}" шрифт установлен {font_size}') self.bot.send_message( message.chat.id, f'Для "{data["data"]}" размер шрифта установлен {font_size}', parse_mode='html' ) self.logger.info(f'Сдвиг "{data["data"]}" на {anchor_point} пикселей') self.bot.send_message( message.chat.id, f'Сдвигаем "{data["data"]}" на {anchor_point} пикселей', parse_mode='html' ) def _add_time_data(self, payload, data, message): """Добавление данных о времени""" if len(data['time_h']) < 2: anchor_point = [40, 0] for layer in ["TIME_H", "TIME_M", "TIME"]: payload['assets'].append({ "layerName": layer, "property": "transform.anchorPoint", "type": "data", "value": anchor_point }) self.logger.info(f'Сдвиг "{data["time_h"]}:{data["time_m"]}" на {anchor_point} пикселей') self.bot.send_message( message.chat.id, f'Сдвигаем "{data["time_h"]}:{data["time_m"]}" на {anchor_point} пикседей', parse_mode='html' ) payload['assets'].extend([ { "type": "data", "layerName": "TIME_H", "property": "Source Text", "value": data['time_h'] }, { "type": "data", "layerName": "TIME_M", "property": "Source Text", "value": data['time_m'] } ]) def _add_team_data(self, payload, data, message, team): """Добавление данных о команде""" team_key = f'team_{team.lower()}' if data[team_key]: payload['assets'].append({ "type": "data", "layerName": f"TEAM_{team}", "property": "Source Text", "value": data[team_key] }) logo_key = f'{team_key}_logo' if data[logo_key]: payload['assets'].append({ "src": data[logo_key], "type": "image", "layerName": f"TEAM_{team}_LOGO" }) logo_res_key = f'{team_key}_logo_res' if data.get(logo_res_key): payload['assets'].append({ "property": "scale", "type": "data", "expression": f"if (width > height) {{max_size = width;}} else {{max_size = height;}} var real_size = {data[logo_res_key][0]}/max_size*100;[real_size,real_size]", "layerName": f"TEAM_{team}_LOGO" }) self.logger.info(f'{data[team_key]} логотип изменен до {data[logo_res_key][0]}') self.bot.send_message( message.chat.id, f'{data[team_key]} масштабирован под {data[logo_res_key][0]} пикселей', parse_mode='html' ) def make_job_dicts(self, dd, pack, logos, message): """Создание задач рендеринга""" self.logger.debug('Начало создания имени') fn = '' data = {} empty_sport = pack.iloc[0].name # Дата if isinstance(dd['date'], str): fn += f"{dd['date'][6:]}{dd['date'][3:5]}{dd['date'][0:2]}" d = dd['date'].split('.') data['data'] = f"{int(d[0])} {['','января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'][int(d[1])]}" elif isinstance(dd['date'], datetime.date): fn += f"{dd['date'].year}{dd['date'].month:02}{dd['date'].day:02}" data['data'] = f"{dd['date'].day} {['','января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'][dd['date'].month]}" # Вид спорта и оформление if dd['sport'] != empty_sport: fn += f"_{dd['sport']}" data['sport'] = dd['sport'] data['pack'] = self.unc2uri(self.get_sport_logo(dd['sport'], pack, message)) else: data['sport'] = '' data['pack'] = '' # Лига if dd["league"][-1] == '.': self.logger.debug('Точка в названии лиги!') fn += f'_{dd["league"][:-1]}' data['league'] = dd['league'][:-1] else: data['league'] = dd['league'] fn += f'_{dd["league"]}' # Команды self._process_team_data(dd, data, fn, 'A', logos, message) self._process_team_data(dd, data, fn, 'B', logos, message) # Канал if not pd.isna(dd['channel']): self.logger.debug('Канал установлен ' + dd['channel']) fn += f"_{dd['channel']}" # Финальное форматирование имени файла fn = translit(fn, reversed=True) fn = fn.replace(' ', '-').replace("'", '') data['outfile_name'] = fn # Время if isinstance(dd['time'], str): t = dd['time'].split(':') data['time_h'] = t[0] data['time_m'] = t[1] elif isinstance(dd['time'], datetime.time): data['time_h'] = str(dd['time'].hour) data['time_m'] = str(dd['time'].minute) self.logger.debug('Время ' + data['time_h'] + ':' + data['time_m']) self.logger.debug("Конец создания имени") # Создание задач watch_list = [] watch_list.append(self.send_job(data, message)) if True: # TODO: Заменить на условие, если нужно data['data'] = 'сегодня' data['outfile_name'] = fn + '_Today' watch_list.append(self.send_job(data, message)) data['data'] = 'завтра' data['outfile_name'] = fn + '_Tomorrow' watch_list.append(self.send_job(data, message)) pprint(watch_list) return list(filter(None, watch_list)) def _process_team_data(self, dd, data, fn, team, logos, message): """Обработка данных команды""" team_key = f'team_{team.lower()}' if pd.isna(dd[f'TEAM {team}']): self.logger.info(f'Нет команды {team}') self.bot.send_message( message.chat.id, f'Нет команды {team}', parse_mode='html' ) data[team_key] = '' data[f'{team_key}_logo'] = '' data[f'{team_key}_logo_res'] = '' else: name = dd[f'TEAM {team}'].split('#') fn += f"_{name[0]}" data[f'{team_key}_logo_res'] = name[2:] data[team_key] = name[0] data[f'{team_key}_logo'] = self.unc2uri( self.get_team_logo(dd[f'TEAM {team}'], dd['sport'], logos, message) ) def run_flask(): """Запуск Flask сервера для панели мониторинга""" flask_app.run(host='0.0.0.0', port=5000) @flask_app.route('/admin/stats') def admin_stats(): """API endpoint для получения статистики""" stats = monitoring.get_stats() return jsonify({ 'status': 'success', 'data': stats }) @flask_app.route('/admin/jobs') def admin_jobs(): """API endpoint для получения списка задач""" jobs = monitoring.get_recent_jobs(50) return jsonify({ 'status': 'success', 'data': jobs }) @flask_app.route('/admin/users') def admin_users(): """API endpoint для получения статистики по пользователям""" stats = monitoring.get_stats() return jsonify({ 'status': 'success', 'data': stats.get('users', {}) }) if __name__ == '__main__': try: # Проверяем наличие файла окружения if not os.path.exists('AF_environment.env'): raise FileNotFoundError( "Файл окружения AF_environment.env не найден. " "Создайте его по образцу AF_environment.example.env" ) # Инициализация компонентов config = Config() monitoring = Monitoring() job_manager = JobManager(config, monitoring) # Запуск Flask в отдельном потоке flask_thread = threading.Thread(target=run_flask, daemon=True) flask_thread.start() # Запуск Telegram бота job_manager.bot.infinity_polling() # except FileNotFoundError