Initial test commit

This commit is contained in:
Alexey Barabanov
2025-08-11 13:38:20 +03:00
parent d85ae1316a
commit 3e670d3722
9 changed files with 4459 additions and 0 deletions

716
AF_script_test_5.py Normal file
View File

@@ -0,0 +1,716 @@
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 synology_drive_api.drive import SynologyDrive
# Загрузка переменных окружения из файла
load_dotenv('AF_environment.env')
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._validate_config()
def _validate_config(self):
"""Проверяет, что все обязательные переменные окружения установлены"""
missing_vars = []
for var, value in vars(self).items():
if value is None:
missing_vars.append(var.upper())
if missing_vars:
raise ValueError(
f"Отсутствуют обязательные переменные окружения: {', '.join(missing_vars)}. "
"Пожалуйста, проверьте файл AF_environment.env"
)
class JobManager:
"""Класс для управления задачами рендеринга"""
def __init__(self, config):
self.config = config
self.bot = telebot.TeleBot(config.token)
self.PLACEHOLDER = sys.platform == 'win32'
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,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'simple',
'stream': 'ext://sys.stdout'
},
'file': {
'class': 'logging.FileHandler',
'level': 'DEBUG',
'formatter': 'simple',
'encoding': 'utf-8',
'filename': 'AF_script.log'
},
},
'loggers': {
__name__: {
'handlers': ['console', 'file'],
'level': 'DEBUG'
}
},
'formatters': {
'simple': {
'class': 'logging.Formatter',
'format': '%(asctime)s %(levelname)-8s %(funcName)12s() - %(message)s',
'datefmt': '%d.%m.%Y %H:%M:%S'
}
}
}
logging.config.dictConfig(LOG_CONFIG)
self.logger = logging.getLogger(__name__)
telebot.logger.addHandler(self.logger.handlers[0])
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,
'<b>Не удалось скачать таблицу</b>',
parse_mode='html'
)
raise
except Exception as e:
self.logger.exception('Ошибка авторизации')
self.bot.send_message(
message.chat.id,
'<b>Не удалось авторизоваться</b>',
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}-<b>{team}</b>',
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'Для "<i>{data["data"]}</i>" размер шрифта установлен <b>{font_size}</b>',
parse_mode='html'
)
self.logger.info(f'Сдвиг "{data["data"]}" на {anchor_point} пикселей')
self.bot.send_message(
message.chat.id,
f'<b>Сдвигаем</b> "<i>{data["data"]}</i>" на <b>{anchor_point}</b> пикселей',
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'<b>Сдвигаем</b> "<i>{data["time_h"]}:{data["time_m"]}</i>" на <b>{anchor_point}</b> пикседей',
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'<b>{data[team_key]}</b> масштабирован под <b>{data[logo_res_key][0]}</b> пикселей',
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(self):
"""Запуск бота"""
@self.bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
self.bot.send_chat_action(message.chat.id, 'typing')
user = f" {message.from_user.username}" if message.from_user.username else '!'
sleep(1)
self.bot.reply_to(
message,
f"Привет{user}\nЯ помогу тебе сделать Анонсы!\nВот список команд которые я могу выполнить:\n/ибаш - наибашу обработку и рендер!\n/харе - остановит нах!"
)
@self.bot.message_handler(commands=['чёкак', 'status'])
def status(message):
self.logger.info(f'Статус запрошен {message.from_user.username}')
try:
r = requests.get(self.config.nexrender_url)
if r.status_code == 200:
jobs = r.json()
s = [{'uid': i['uid'], 'state': i['state']} for i in jobs]
queued = sum(1 for job in s if job['state'] in ('queued', 'picked'))
if s:
self.logger.info(f"{queued} в очереди")
self.bot.send_message(
message.chat.id,
f"В очереди {queued}"
)
else:
self.logger.info("Нет задач в очереди")
self.bot.send_message(
message.chat.id,
"Нет задач в очереди"
)
except Exception as e:
self.logger.exception("Ошибка получения статуса")
self.bot.send_message(
message.chat.id,
"Ошибка получения статуса"
)
@self.bot.message_handler(commands=['харе', 'stop'])
def stop(message):
try:
r = requests.get(self.config.nexrender_url)
if r.status_code == 200:
jobs = r.json()
s = [{'uid': i['uid'], 'state': i['state']} for i in jobs]
cancelled = 0
if s:
for job in s:
requests.delete(f"{self.config.nexrender_url}/{job['uid']}")
cancelled += 1
self.logger.info(f"Отменено {cancelled} задач пользователем {message.from_user.username}")
self.bot.send_message(
message.chat.id,
f"Отменено {cancelled}"
)
else:
self.logger.info(f"{message.from_user.username} запросил отмену, но нет задач для отмены")
self.bot.send_message(
message.chat.id,
"Нет задач для отмены"
)
except Exception as e:
self.logger.exception("Ошибка отмены задач")
self.bot.send_message(
message.chat.id,
"Ошибка отмены задач"
)
@self.bot.message_handler(commands=['ибаш', 'ibash'])
def ibash(message):
self.logger.info(f'Запуск задач для {message.from_user.username}')
self.bot.send_chat_action(message.chat.id, 'typing')
user = message.from_user.username if message.from_user.username else '!'
self.bot.send_message(
message.chat.id,
f"Ну что ж {user}, давай попробуем \n<b>НАИБАШИТЬ!!!</b>",
parse_mode='html'
)
self.bot.send_chat_action(message.chat.id, 'upload_document')
try:
osheet = self.load_osheet(message)
start = self.get_sheet_data(osheet, 'Start', header=1)
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])
# Очистка старых задач
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.exception("Ошибка очистки старых задач")
self.bot.send_chat_action(message.chat.id, 'record_video')
watch_list = []
for i, row in start.iterrows():
dd = self.make_data_dict(row)
watch_list += self.make_job_dicts(dd, pack, logos, message)
self.logger.info(f"В очереди {len(watch_list)} задач")
self.bot.send_message(
message.chat.id,
f"В очереди {len(watch_list)} задач"
)
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.logger.info(f"{job['outname']}, {state}, {len(watch_list)} осталось")
self.bot.send_message(
message.chat.id,
f"<b>{job['outname']}</b>, <i>готов</i>, {len(watch_list)} осталось выполнить",
parse_mode='html'
)
elif state == 'error':
watch_list.remove(job)
self.logger.warning(f"{job}, {state}, {len(watch_list)} осталось")
self.bot.send_message(
message.chat.id,
f"!!!{job}, {state}, {len(watch_list)} осталось выполнить",
parse_mode='html'
)
except Exception as e:
self.logger.exception(f"Ошибка проверки статуса задачи {job['uid']}")
self.bot.send_message(message.chat.id, 'Пойду спать :)')
except Exception as e:
self.logger.exception("Ошибка выполнения команды ибаш")
self.bot.send_message(
message.chat.id,
"Произошла ошибка при обработке команды"
)
self.logger.info('Запуск бота')
self.bot.infinity_polling()
self.logger.info('Завершение работы бота')
if __name__ == '__main__':
try:
# Проверяем, что файл окружения существует
if not os.path.exists('AF_environment.env'):
raise FileNotFoundError(
"Файл окружения AF_environment.env не найден. "
)
config = Config()
job_manager = JobManager(config)
job_manager.setup_logging()
job_manager.run()
except FileNotFoundError as e:
logging.error(str(e))
print(str(e))
sys.exit(1)
except ValueError as e:
logging.error(f"Ошибка конфигурации: {e}")
print(f"Ошибка конфигурации: {e}")
sys.exit(1)
except Exception as e:
logging.error(f"Неожиданная ошибка: {e}", exc_info=True)
print(f"Неожиданная ошибка: {e}")
sys.exit(1)