Compare commits

...

28 Commits

Author SHA1 Message Date
Alexey Barabanov
5994e18efa Не ябучий 3й питон!!! 2025-11-13 17:42:29 +03:00
Alexey Barabanov
5205dd5c5c lgkllhdfg 2025-11-13 17:40:52 +03:00
Alexey Barabanov
ead3683e4c коминт 2025-11-13 17:26:09 +03:00
Alexey Barabanov
8f300e0154 ОПЕЧАТКА 2025-11-13 16:48:10 +03:00
6e92022e67 коммит 2025-11-13 15:57:12 +03:00
74801ad710 Merge branch 'main' of https://git.tvstart.ru/ychernenko/KHL 2025-11-13 15:54:42 +03:00
28b0f09ec2 что то поменял, но нужно проверить 2025-11-13 15:53:43 +03:00
Alexey Barabanov
c60caaa8aa Jgtxfnrf 2025-11-13 15:30:27 +03:00
Alexey Barabanov
9cbf32f831 Испровление опечатки 2025-11-13 15:30:09 +03:00
Alexey Barabanov
d4d9584bb3 Поменяли взоиможействие с файлом окружения.
Поменяли что береться из файла окружения
2025-11-13 15:28:14 +03:00
Alexey Barabanov
216138ceed Изменения в содании сервиса.
Добавлены:
Лимиты
Логирование
Поведение при перезапуске
Работа с виртуальным окрыжением и файлом окружения
2025-11-13 15:09:50 +03:00
Юрий Черненко
0d06097181 версия с чтением и передачей бинарного файла 2025-11-11 19:46:41 +03:00
71f1e62630 поправил gitignore 2025-11-11 15:18:41 +03:00
d120630548 Remove __pycache__ from history 2025-11-11 15:17:56 +03:00
13c86660ac Merge branch 'main' of https://git.tvstart.ru/ychernenko/KHL 2025-11-11 15:14:33 +03:00
df0b6f8678 добавлена библиотека nasio 2025-11-11 15:14:29 +03:00
3b7b447f40 add *.pyc 2025-11-11 13:55:08 +03:00
711e217d6e Merge branch 'main' of https://git.tvstart.ru/ychernenko/KHL 2025-11-10 19:08:06 +03:00
9778efef99 uyriety 2025-11-10 19:07:27 +03:00
Alexey Barabanov
fb12719e60 add python-dotenv 2025-11-10 16:59:53 +03:00
e81eb5b0fc добавленно поиск файла окружения 2025-11-10 16:46:00 +03:00
5894d2d8a5 Немного изменения для запуска в качестве сервиса на линуксе. 2025-11-10 14:48:44 +03:00
78e28eaa92 add dotenv_path="/mnt/khl/.env" 2025-11-10 14:41:02 +03:00
17b1c9de8b VER1.1 2025-11-10 14:38:39 +03:00
6d1e83d6fe ver 1 2025-11-10 14:38:10 +03:00
6de3607a02 Переделка из РФБ в КХЛ 2025-11-10 08:46:28 +00:00
c50c97bc82 коммит вечером 2025-11-06 20:49:24 +03:00
d95b5f10d1 скрыл docs и тп 2025-11-06 11:56:24 +03:00
6 changed files with 812 additions and 217 deletions

7
.gitignore vendored
View File

@@ -1 +1,8 @@
.env .env
/.venv
logs/
/logs/*
__pycache__/
*.pyc
*.pyd
*.pyo

View File

@@ -0,0 +1,4 @@
## Проверка логов
```shell
sudo journalctl -t KHL
```

Binary file not shown.

390
deploy.sh Normal file → Executable file
View File

@@ -4,18 +4,21 @@
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Конфигурация
REPO_URL="https://git.tvstart.ru/ychernenko/KHL.git"
TARGET_DIR="/root/KHL"
SERVICE_NAME="khl-data.service"
TARGET_ENV="/mnt/khl/.env"
show_help() { show_help() {
echo "Использование: $0 -t <домашняя команда> -r <релиз> [-l <лига>]" echo "Использование: $0 -r <релиз> [-h]"
echo " -t Домашняя команда"
echo " -r Релиз (тег или ветка в git)" echo " -r Релиз (тег или ветка в git)"
echo " -l Лига (опционально)"
echo " -h Показать эту справку" echo " -h Показать эту справку"
echo "" echo ""
echo "Пример: $0 -t cska -r main" echo "Пример: $0 -r Barabanov_TEST"
echo "Пример: $0 -t zenit -r Barabanov_TEST -l vtb"
echo "Пример: $0 -t avtodor -r main"
echo "" echo ""
exit 0 exit 0
} }
@@ -33,13 +36,41 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2 echo -e "${RED}[ERROR]${NC} $1" >&2
} }
log_debug() {
echo -e "${BLUE}[DEBUG]${NC} $1" >&2
}
# Функция проверки зависимостей системы
check_dependencies() {
local deps=("git" "python3" "pip" "netstat" "systemctl")
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
missing+=("$dep")
fi
done
if [ ${#missing[@]} -ne 0 ]; then
log_warn "Отсутствуют некоторые команды: ${missing[*]}"
return 1
fi
return 0
}
# Функция проверки и установки пакетов # Функция проверки и установки пакетов
install_packages() { install_packages() {
log_info "Обновление списка пакетов..." log_info "Обновление списка пакетов..."
apt-get update if ! apt-get update; then
log_error "Ошибка при обновлении списка пакетов"
exit 1
fi
log_info "Установка необходимых пакетов..." log_info "Установка необходимых пакетов..."
apt-get install -y python3 python3-pip python3-venv git net-tools if ! apt-get install -y python3 python3-pip python3-venv git net-tools; then
log_error "Ошибка при установке пакетов"
exit 1
fi
# Проверка установки # Проверка установки
if ! command -v python3 &> /dev/null; then if ! command -v python3 &> /dev/null; then
@@ -47,44 +78,48 @@ install_packages() {
exit 1 exit 1
fi fi
if ! command -v pip3 &> /dev/null; then if ! command -v pip &> /dev/null; then
log_error "pip3 не установлен!" log_error "pip не установлен!"
exit 1 exit 1
fi fi
log_info "Версия Python: $(python3 --version)" log_info "Версия Python: $(python3 --version)"
log_info "Версия pip: $(pip3 --version)" log_info "Версия pip: $(pip --version)"
} }
# Функция загрузки кода # Функция загрузки кода
download_code() { download_code() {
local repo_url="https://git.tvstart.ru/ychernenko/RFB.git"
local release="$1" local release="$1"
local target_dir="/root/RFB" local target_dir="$TARGET_DIR"
log_info "Создание рабочей директории $target_dir..." log_info "Создание рабочей директории $target_dir..."
mkdir -p $target_dir mkdir -p "$target_dir"
if [ -d "$target_dir/.git" ]; then if [ -d "$target_dir/.git" ]; then
log_info "Обновление существующего репозитория..." log_info "Обновление существующего репозитория..."
cd $target_dir cd "$target_dir" || exit 1
git fetch --all git fetch --all
git checkout $release if ! git checkout "$release"; then
git pull origin $release
else
log_info "Клонирование репозитория $repo_url..."
git clone $repo_url $target_dir
cd $target_dir
git checkout $release
fi
# Проверка успешности checkout
if [ $? -ne 0 ]; then
log_error "Не удалось переключиться на релиз $release" log_error "Не удалось переключиться на релиз $release"
log_error "Доступные ветки:" log_info "Доступные ветки:"
git branch -r git branch -r
exit 1 exit 1
fi fi
git pull origin "$release"
else
log_info "Клонирование репозитория $REPO_URL..."
if ! git clone "$REPO_URL" "$target_dir"; then
log_error "Ошибка при клонировании репозитория"
exit 1
fi
cd "$target_dir" || exit 1
if ! git checkout "$release"; then
log_error "Не удалось переключиться на релиз $release"
log_info "Доступные ветки:"
git branch -r
exit 1
fi
fi
log_info "Код успешно загружен (релиз: $release)" log_info "Код успешно загружен (релиз: $release)"
log_info "Текущая ветка: $(git branch --show-current)" log_info "Текущая ветка: $(git branch --show-current)"
@@ -93,11 +128,20 @@ download_code() {
# Функция настройки виртуального окружения # Функция настройки виртуального окружения
setup_venv() { setup_venv() {
local target_dir="/root/RFB" local target_dir="$TARGET_DIR"
cd $target_dir cd "$target_dir" || exit 1
# Удаляем существующее виртуальное окружение для чистоты
if [ -d ".venv" ]; then
log_info "Удаление существующего виртуального окружения..."
rm -rf .venv
fi
log_info "Создание виртуального окружения..." log_info "Создание виртуального окружения..."
python3 -m venv .venv if ! python3 -m venv .venv; then
log_error "Не удалось создать виртуальное окружение"
exit 1
fi
if [ ! -f ".venv/bin/activate" ]; then if [ ! -f ".venv/bin/activate" ]; then
log_error "Не удалось создать виртуальное окружение" log_error "Не удалось создать виртуальное окружение"
@@ -108,7 +152,9 @@ setup_venv() {
source .venv/bin/activate source .venv/bin/activate
# Обновление pip # Обновление pip
pip install --upgrade pip if ! pip install --upgrade pip; then
log_warn "Не удалось обновить pip"
fi
# Проверка наличия requirements.txt # Проверка наличия requirements.txt
if [ -f "requirements.txt" ]; then if [ -f "requirements.txt" ]; then
@@ -117,24 +163,29 @@ setup_venv() {
log_info "Все зависимости успешно установлены" log_info "Все зависимости успешно установлены"
else else
log_error "Ошибка при установке зависимостей из requirements.txt" log_error "Ошибка при установке зависимостей из requirements.txt"
log_warn "Попытка установки базовых зависимостей..." exit 1
pip install requests pandas numpy
fi fi
else else
log_warn "Файл requirements.txt не найден, устанавливаем базовые зависимости..." log_warn "Файл requirements.txt не найден, устанавливаем базовые зависимости..."
pip install requests pandas numpy if ! pip install requests pandas numpy fastapi uvicorn python-telegram-handler python-dotenv; then
log_error "Ошибка при установке базовых зависимостей"
exit 1
fi
fi fi
# Проверка основных пакетов # Проверка основных пакетов
log_info "Проверка установки основных пакетов..." log_info "Проверка установки основных пакетов..."
python -c " if ! python -c "
try: try:
import requests, pandas, numpy import requests, pandas, numpy
print('✓ Все основные пакеты успешно импортируются') print('✓ Все основные пакеты успешно импортируются')
except ImportError as e: except ImportError as e:
print(f'✗ Ошибка импорта: {e}') print(f'✗ Ошибка импорта: {e}')
exit(1) exit(1)
" "; then
log_error "Ошибка при проверке пакетов"
exit 1
fi
log_info "Установленные пакеты:" log_info "Установленные пакеты:"
pip list --format=columns pip list --format=columns
@@ -142,50 +193,52 @@ except ImportError as e:
# Функция создания systemd сервиса # Функция создания systemd сервиса
create_systemd_service() { create_systemd_service() {
local team="$1" log_info "Создание systemd сервиса"
local league="$2"
log_info "Создание systemd сервиса для команды: $team"
if [ -n "$league" ]; then
log_info "Лига: $league"
fi
# Останавливаем и отключаем старый сервис если он есть # Останавливаем и отключаем старый сервис если он есть
if systemctl is-active --quiet rfb-data.service; then if systemctl is-active --quiet "$SERVICE_NAME"; then
log_info "Остановка rfb-data.service..." log_info "Остановка $SERVICE_NAME..."
systemctl stop rfb-data.service systemctl stop "$SERVICE_NAME"
fi fi
if systemctl is-enabled --quiet rfb-data.service; then if systemctl is-enabled --quiet "$SERVICE_NAME"; then
log_info "Отключение rfb-data.service..." log_info "Отключение $SERVICE_NAME..."
systemctl disable rfb-data.service systemctl disable "$SERVICE_NAME"
fi fi
# Формируем команду для data сервиса # Формируем команду для data сервиса
local data_command="/root/RFB/.venv/bin/python3 /root/RFB/get_data.py --team \"$team\"" local data_service_file="/etc/systemd/system/$SERVICE_NAME"
if [ -n "$league" ]; then
data_command="$data_command --league \"$league\""
fi
# Создаем сервисный файл в правильной директории
local data_service_file="/etc/systemd/system/rfb-data.service"
log_info "Создание файла сервиса: $data_service_file" log_info "Создание файла сервиса: $data_service_file"
cat > "$data_service_file" << EOF cat > "$data_service_file" << EOF
[Unit] [Unit]
Description=RFB Data Service Description=KHL Data Service
Documentation=https://git.tvstart.ru/ychernenko/KHL
After=network.target After=network.target
Wants=network.target
[Service] [Service]
Type=simple Type=simple
User=root User=root
WorkingDirectory=/root/RFB WorkingDirectory=$TARGET_DIR
Environment=PATH=/root/RFB/.venv/bin Environment=PATH=$TARGET_DIR/.venv/bin
ExecStart=$data_command EnvironmentFile=$TARGET_ENV
Restart=always ExecStart=python3 $TARGET_DIR/get_data.py
RestartSec=30
# Лимиты ресурсов
MemoryMax=1G
CPUQuota=80%
# Логирование
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal
SyslogIdentifier=KHL
# Поведение при перезапуске
Restart=always
RestartSec=10
StartLimitInterval=300
StartLimitBurst=5
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@@ -195,16 +248,22 @@ EOF
chmod 644 "$data_service_file" chmod 644 "$data_service_file"
log_info "Перезагрузка systemd демона..." log_info "Перезагрузка systemd демона..."
systemctl daemon-reload if ! systemctl daemon-reload; then
log_error "Ошибка при перезагрузке systemd демона"
exit 1
fi
log_info "Включение сервиса rfb-data.service..." log_info "Включение сервиса $SERVICE_NAME..."
systemctl enable rfb-data.service if ! systemctl enable "$SERVICE_NAME"; then
log_error "Ошибка при включении сервиса"
exit 1
fi
} }
# Функция проверки файлов # Функция проверки файлов
check_required_files() { check_required_files() {
local target_dir="/root/RFB" local target_dir="$TARGET_DIR"
cd $target_dir cd "$target_dir" || exit 1
local missing_files=() local missing_files=()
local required_files=("get_data.py") local required_files=("get_data.py")
@@ -230,74 +289,26 @@ check_required_files() {
fi fi
} }
# Функция определения IP и команды (исправленная)
detect_team() {
local team_arg="$1"
# Определение IP адреса
if [[ "$(uname -s)" == "Linux" ]]; then
ip_address=$(ip route get 1 2>/dev/null | awk '{print $7; exit}' ||
ip a 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' |
grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -n1)
else
log_error "Скрипт работает только на Linux"
exit 1
fi
log_info "Определен IP адрес: $ip_address"
# База данных хостов в виде пар IP-команда
declare -A hosts=(
["10.10.35.21"]="cska"
["10.10.35.22"]="Lokomotiv Kuban"
["10.10.35.23"]="uralmash"
["10.10.35.24"]="betcity parma"
["10.10.35.25"]="avtodor"
["10.10.35.26"]="zenit"
["10.10.35.27"]="samara"
["10.10.35.28"]="mba-mai"
["10.10.35.29"]="Pari Nizhny Novgorod"
["10.10.35.30"]="unics"
)
# Определение команды по IP
detected_team="${hosts[$ip_address]}"
# Определение финальной команды (приоритет у аргумента, затем у автоопределения)
if [[ -n "$team_arg" ]]; then
final_team="$team_arg"
log_info "Используется команда из аргумента: $final_team"
elif [[ -n "$detected_team" ]]; then
final_team="$detected_team"
log_info "Используется автоопределенная команда: $final_team"
else
log_error "Не удалось определить команду. Укажите явно через -t"
echo "Доступные команды:" >&2
for ip in "${!hosts[@]}"; do
echo " $ip: ${hosts[$ip]}" >&2
done
exit 1
fi
# Возвращаем только чистую строку с названием команды (без цветовых кодов)
echo "$final_team"
}
# Функция настройки firewall # Функция настройки firewall
setup_firewall() { setup_firewall() {
log_info "Настройка firewall (открытие порта 8000)..." log_info "Настройка firewall (открытие порта 8000)..."
# Проверяем наличие ufw # Проверяем наличие ufw
if command -v ufw &> /dev/null && systemctl is-active --quiet ufw; then if command -v ufw &> /dev/null && systemctl is-active --quiet ufw; then
ufw allow 8000/tcp if ufw allow 8000/tcp; then
log_info "Порт 8000 открыт в ufw" log_info "Порт 8000 открыт в ufw"
else
log_warn "Не удалось открыть порт 8000 в ufw"
fi
fi fi
# Для firewalld (CentOS/RHEL) # Для firewalld (CentOS/RHEL)
if command -v firewall-cmd &> /dev/null && systemctl is-active --quiet firewalld; then if command -v firewall-cmd &> /dev/null && systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-port=8000/tcp if firewall-cmd --permanent --add-port=8000/tcp && firewall-cmd --reload; then
firewall-cmd --reload
log_info "Порт 8000 открыт в firewalld" log_info "Порт 8000 открыт в firewalld"
else
log_warn "Не удалось открыть порт 8000 в firewalld"
fi
fi fi
} }
@@ -306,9 +317,9 @@ check_port() {
local port=8000 local port=8000
local occupied=false local occupied=false
if command -v netstat &> /dev/null && netstat -tuln 2>/dev/null | grep ":$port " > /dev/null; then if command -v netstat &> /dev/null && netstat -tuln 2>/dev/null | grep -q ":$port "; then
occupied=true occupied=true
elif command -v ss &> /dev/null && ss -tuln 2>/dev/null | grep ":$port " > /dev/null; then elif command -v ss &> /dev/null && ss -tuln 2>/dev/null | grep -q ":$port "; then
occupied=true occupied=true
fi fi
@@ -325,30 +336,42 @@ manage_services() {
case $action in case $action in
"start") "start")
log_info "Запуск сервиса rfb-data.service..." log_info "Запуск сервиса $SERVICE_NAME..."
systemctl start rfb-data.service if ! systemctl start "$SERVICE_NAME"; then
log_error "Ошибка при запуске сервиса"
return 1
fi
;; ;;
"restart") "restart")
log_info "Перезапуск сервиса rfb-data.service..." log_info "Перезапуск сервиса $SERVICE_NAME..."
systemctl restart rfb-data.service if ! systemctl restart "$SERVICE_NAME"; then
log_error "Ошибка при перезапуске сервиса"
return 1
fi
;; ;;
"stop") "stop")
log_info "Остановка сервиса rfb-data.service..." log_info "Остановка сервиса $SERVICE_NAME..."
systemctl stop rfb-data.service if ! systemctl stop "$SERVICE_NAME"; then
log_error "Ошибка при остановке сервиса"
return 1
fi
;; ;;
esac esac
# Даем время сервису на запуск/остановку # Даем время сервису на запуск/остановку
sleep 3 sleep 3
return 0
} }
# Функция проверки статуса сервиса # Функция проверки статуса сервиса
check_service_status() { check_service_status() {
log_info "Статус сервиса rfb-data.service:" log_info "Статус сервиса $SERVICE_NAME:"
if systemctl is-active rfb-data.service; then if systemctl is-active "$SERVICE_NAME" &>/dev/null; then
systemctl status rfb-data.service --no-pager -l systemctl status "$SERVICE_NAME" --no-pager -l
return 0
else else
log_warn "Сервис rfb-data.service не запущен" log_warn "Сервис $SERVICE_NAME не запущен"
return 1
fi fi
} }
@@ -363,7 +386,12 @@ check_system() {
fi fi
# Проверка свободного места # Проверка свободного места
local available_space=$(df /root --output=avail | tail -1) local available_space
available_space=$(df "$TARGET_DIR" --output=avail 2>/dev/null | tail -1)
if [ -z "$available_space" ]; then
available_space=$(df / --output=avail | tail -1)
fi
if [ "$available_space" -lt 1048576 ]; then # Меньше 1GB if [ "$available_space" -lt 1048576 ]; then # Меньше 1GB
log_warn "Мало свободного места: $((available_space / 1024)) MB" log_warn "Мало свободного места: $((available_space / 1024)) MB"
else else
@@ -371,39 +399,46 @@ check_system() {
fi fi
# Проверка памяти # Проверка памяти
local total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}') if [ -f /proc/meminfo ]; then
local total_mem
total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')
log_info "Оперативная память: $((total_mem / 1024)) MB" log_info "Оперативная память: $((total_mem / 1024)) MB"
fi
# Проверка архитектуры
log_info "Архитектура: $(uname -m)"
}
# Функция очистки при прерывании
cleanup() {
log_info "Очистка..."
if [ -f "/etc/systemd/system/$SERVICE_NAME" ]; then
manage_services "stop"
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
rm -f "/etc/systemd/system/$SERVICE_NAME"
systemctl daemon-reload
fi
} }
# Основная функция # Основная функция
main() { main() {
local team=""
local release="main" # значение по умолчанию local release="main" # значение по умолчанию
local league="" # переменная для лиги
# Обработка аргументов командной строки # Обработка аргументов командной строки
while getopts "t:r:l:h" opt; do while getopts "r:h" opt; do
case $opt in case $opt in
t) team="$OPTARG" ;;
r) release="$OPTARG" ;; r) release="$OPTARG" ;;
l) league="$OPTARG" ;;
h) show_help ;; h) show_help ;;
*) log_error "Неверный аргумент"; exit 1 ;; *) log_error "Неверный аргумент"; exit 1 ;;
esac esac
done done
# Проверка обязательных аргументов log_info "Начало установки KHL Data Service..."
if [ -z "$team" ] && [ -z "$release" ]; then if [ -n "$release" ]; then
log_error "Необходимо указать команду (-t) и релиз (-r)" log_info "Релиз: $release"
show_help
fi
log_info "Начало установки RFB Data Service..."
log_info "Команда: $team, Релиз: $release"
if [ -n "$league" ]; then
log_info "Лига: $league"
else else
log_info "Лига: не указана (будет использовано значение по умолчанию)" log_info "Релиз: не указан (будет использовано значение по умолчанию: main)"
release="main"
fi fi
# Проверка прав root # Проверка прав root
@@ -412,9 +447,15 @@ main() {
exit 1 exit 1
fi fi
# Установка обработчика прерывания
trap 'log_error "Прервано пользователем"; cleanup; exit 1' INT TERM
# Проверка системы # Проверка системы
check_system check_system
# Проверка зависимостей
check_dependencies
# Установка пакетов # Установка пакетов
install_packages install_packages
@@ -427,16 +468,6 @@ main() {
# Настройка виртуального окружения # Настройка виртуального окружения
setup_venv setup_venv
# Определение команды - ТЕПЕРЬ БЕЗ временного файла
final_team=$(detect_team "$team")
log_info "Финальная команда: '$final_team'"
# Проверка что команда не пустая и не содержит спецсимволов
if [[ -z "$final_team" || "$final_team" =~ [^a-zA-Z0-9[:space:]-] ]]; then
log_error "Некорректное название команды: '$final_team'"
exit 1
fi
# Настройка firewall # Настройка firewall
setup_firewall setup_firewall
@@ -444,46 +475,47 @@ main() {
check_port check_port
# Создание systemd сервиса # Создание systemd сервиса
create_systemd_service "$final_team" "$league" create_systemd_service
log_info "Настройка завершена!" log_info "Настройка завершена!"
# Запуск сервиса # Запуск сервиса
manage_services "start" if manage_services "start"; then
log_info "Сервис успешно запущен"
else
log_error "Не удалось запустить сервис"
exit 1
fi
# Проверка статуса # Проверка статуса
check_service_status check_service_status
# Получаем IP для вывода # Получаем IP для вывода
local ip_address
ip_address=$(ip route get 1 2>/dev/null | awk '{print $7; exit}' || ip_address=$(ip route get 1 2>/dev/null | awk '{print $7; exit}' ||
ip a 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | ip a 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' |
grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -n1) grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -n1)
log_info "==================================================" log_info "=================================================="
log_info "Установка завершена успешно!" log_info "Установка завершена успешно!"
log_info "Команда: $final_team" log_info "Релиз: $release"
if [ -n "$league" ]; then log_info "Рабочая директория: $TARGET_DIR"
log_info "Лига: $league"
fi
log_info "Режим: $release"
log_info "" log_info ""
log_info "Для просмотра логов:" log_info "Для просмотра логов:"
log_info " journalctl -u rfb-data.service -f" log_info " journalctl -u $SERVICE_NAME -f"
log_info " journalctl -t KHL -f"
log_info "" log_info ""
log_info "Управление сервисом:" log_info "Управление сервисом:"
log_info " Перезапуск: systemctl restart rfb-data.service" log_info " Перезапуск: systemctl restart $SERVICE_NAME"
log_info " Остановка: systemctl stop rfb-data.service" log_info " Остановка: systemctl stop $SERVICE_NAME"
log_info " Статус: systemctl status rfb-data.service" log_info " Статус: systemctl status $SERVICE_NAME"
log_info " Логи: journalctl -u rfb-data.service" log_info " Логи: journalctl -u $SERVICE_NAME"
log_info "" log_info ""
log_info "Проверка работы:" log_info "Проверка работы:"
log_info " Проверить процессы: ps aux | grep get_data.py" log_info " Проверить процессы: ps aux | grep get_data.py"
log_info " Проверить логи: tail -f /root/RFB/logs/*.log 2>/dev/null || echo 'Директория логов не найдена'" log_info " Проверить логи: tail -f $TARGET_DIR/logs/*.log 2>/dev/null || echo 'Директория логов не найдена'"
log_info "==================================================" log_info "=================================================="
} }
# Обработка прерывания
trap 'log_error "Прервано пользователем"; exit 1' INT TERM
# Запуск основной функции # Запуск основной функции
main "$@" main "$@"

View File

@@ -1,5 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import Response, JSONResponse, HTMLResponse from fastapi.responses import Response, JSONResponse, HTMLResponse, StreamingResponse
from fastapi.encoders import jsonable_encoder
import pandas as pd import pandas as pd
import requests, io, os import requests, io, os
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
@@ -9,29 +10,39 @@ import uvicorn
from threading import Thread, Event, Lock from threading import Thread, Event, Lock
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import numpy as np
import nasio
import logging
import logging.config
import platform
import json
from pprint import pprint
# --- Глобальные переменные --- # --- Глобальные переменные ---
selected_game_id: int | None = None selected_game_id: int | None = None
current_tournament_id: int | None = None # будем обновлять при загрузке расписания current_tournament_id: int | None = None # будем обновлять при загрузке расписания
current_season: str | None = None # будем обновлять при загрузке расписания
latest_game_data: dict | None = None # сюда кладём последние данные по матчу latest_game_data: dict | None = None # сюда кладём последние данные по матчу
latest_game_error: str | None = None latest_game_error: str | None = None
_latest_lock = Lock() _latest_lock = Lock()
_stop_event = Event() _stop_event = Event()
_worker_thread: Thread | None = None _worker_thread: Thread | None = None
# Загружаем переменные из .env pprint(f"Локальный файл окружения ={load_dotenv(verbose=True)}")
load_dotenv()
api_user = os.getenv("API_USER") api_user = os.getenv("API_USER")
api_pass = os.getenv("API_PASS") api_pass = os.getenv("API_PASS")
league = os.getenv("LEAGUE") league = os.getenv("LEAGUE")
api_base_url = os.getenv("API_BASE_URL")
POLL_SEC = int(os.getenv("GAME_POLL_SECONDS")) POLL_SEC = int(os.getenv("GAME_POLL_SECONDS"))
SERVER_NAME = os.getenv("SYNO_URL")
USER = os.getenv("SYNO_USERNAME")
PASSWORD = os.getenv("SYNO_PASSWORD")
PATH = f'{os.getenv("SYNO_PATH")}MATCH.xlsm'
def load_today_schedule(): def load_today_schedule():
"""Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF).""" """Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF)."""
url_tournaments = "http://stat2tv.khl.ru/tournaments.xml" url_tournaments = f"{api_base_url}tournaments.xml"
r = requests.get( r = requests.get(
url_tournaments, auth=HTTPBasicAuth(api_user, api_pass), verify=False url_tournaments, auth=HTTPBasicAuth(api_user, api_pass), verify=False
) )
@@ -51,14 +62,26 @@ def load_today_schedule():
return pd.DataFrame() return pd.DataFrame()
tournament_id = int(filtered.iloc[0]["id"]) tournament_id = int(filtered.iloc[0]["id"])
global current_tournament_id season = str(filtered.iloc[0]["season"])
global current_tournament_id, current_season
current_tournament_id = tournament_id current_tournament_id = tournament_id
url_schedule = f"http://stat2tv.khl.ru/{tournament_id}/schedule-{tournament_id}.xml" current_season = season
url_schedule = f"{api_base_url}{tournament_id}/schedule-{tournament_id}.xml"
r = requests.get(url_schedule, auth=HTTPBasicAuth(api_user, api_pass), verify=False) r = requests.get(url_schedule, auth=HTTPBasicAuth(api_user, api_pass), verify=False)
schedule_df = pd.read_xml(io.StringIO(r.text)) schedule_df = pd.read_xml(io.StringIO(r.text))
# Нужные колонки (скорректируй под реальные имена из XML) # Нужные колонки (скорректируй под реальные имена из XML)
needed_columns = ["id", "date", "time", "homeName_en", "visitorName_en", "arena"] needed_columns = [
"id",
"date",
"time",
"homeName_en",
"visitorName_en",
"arena_en",
"arena_city_en",
"homeCity_en",
"visitorCity_en",
]
exist = [c for c in needed_columns if c in schedule_df.columns] exist = [c for c in needed_columns if c in schedule_df.columns]
schedule_df = schedule_df[exist].copy() schedule_df = schedule_df[exist].copy()
@@ -101,16 +124,14 @@ def load_today_schedule():
def _build_game_url(tournament_id: int, game_id: int) -> str: def _build_game_url(tournament_id: int, game_id: int) -> str:
# URL по аналогии с расписанием: .../{tournament_id}/json_en/{game_id}.json # URL по аналогии с расписанием: .../{tournament_id}/json_en/{game_id}.json
# Если у тебя другой шаблон — просто поменяй строку ниже. # Если у тебя другой шаблон — просто поменяй строку ниже.
return f"http://stat2tv.khl.ru/{tournament_id}/json_en/{game_id}.json" return f"{api_base_url}{tournament_id}/json_en/{game_id}.json"
def _fetch_game_once(tournament_id: int, game_id: int) -> dict: def _fetch_game_once(tournament_id: int, game_id: int) -> dict:
"""Один запрос к API матча -> чистый JSON из API.""" """Один запрос к API матча -> чистый JSON из API."""
url = _build_game_url(tournament_id, game_id) url = _build_game_url(tournament_id, game_id)
r = requests.get( r = requests.get(
url, url, auth=HTTPBasicAuth(api_user, api_pass), verify=False, timeout=10
auth=HTTPBasicAuth(api_user, api_pass),
verify=False,
timeout=10
) )
r.raise_for_status() r.raise_for_status()
@@ -121,7 +142,7 @@ def _fetch_game_once(tournament_id: int, game_id: int) -> dict:
# если это не JSON — вернём текст # если это не JSON — вернём текст
data = {"raw": r.text} data = {"raw": r.text}
return {"url": url, "json": data} return data
def _game_poll_worker(): def _game_poll_worker():
@@ -166,11 +187,18 @@ async def lifespan(app: FastAPI):
_worker_thread.join(timeout=2) _worker_thread.join(timeout=2)
print("🛑 Background thread stopped") print("🛑 Background thread stopped")
app = FastAPI(lifespan=lifespan)
app = FastAPI(
lifespan=lifespan,
docs_url=None, # ❌ отключает /docs
redoc_url=None, # ❌ отключает /redoc
openapi_url=None, # ❌ отключает /openapi.json
)
@app.get("/games") @app.get("/games")
async def games(): async def games():
df = load_today_schedule() df = load_today_schedule()
if df.empty: if df.empty:
return JSONResponse({"message": "Сегодня матчей нет"}) return JSONResponse({"message": "Сегодня матчей нет"})
@@ -179,8 +207,8 @@ async def games():
return Response(content=json_schedule, media_type="application/json") return Response(content=json_schedule, media_type="application/json")
@app.get("/games/html") @app.get("/select")
async def games_html(): async def select():
df = load_today_schedule() df = load_today_schedule()
if df.empty: if df.empty:
return HTMLResponse( return HTMLResponse(
@@ -194,7 +222,8 @@ async def games_html():
home = row.get("homeName_en", "") home = row.get("homeName_en", "")
away = row.get("visitorName_en", "") away = row.get("visitorName_en", "")
when = row.get("datetime_str", "") when = row.get("datetime_str", "")
arena = row.get("arena", "") arena = row.get("arena_en", "")
city = row.get("arena_city_en", "")
rows_html.append( rows_html.append(
f""" f"""
<tr data-id="{gid}"> <tr data-id="{gid}">
@@ -203,6 +232,7 @@ async def games_html():
<td>{home}</td> <td>{home}</td>
<td>{away}</td> <td>{away}</td>
<td>{arena}</td> <td>{arena}</td>
<td>{city}</td>
<td><button class="pick">Выбрать</button></td> <td><button class="pick">Выбрать</button></td>
</tr> </tr>
""" """
@@ -233,7 +263,7 @@ async def games_html():
<table id="tbl"> <table id="tbl">
<thead> <thead>
<tr> <tr>
<th>ID</th><th>Дата/время</th><th>Хозяева</th><th>Гости</th><th>Арена</th><th></th> <th>ID</th><th>Дата/время</th><th>Хозяева</th><th>Гости</th><th>Арена</th><th>Город</th><th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -309,20 +339,538 @@ async def get_selected_game():
async def game_url(): async def game_url():
if not (selected_game_id and current_tournament_id): if not (selected_game_id and current_tournament_id):
return JSONResponse({"message": "game_id или tournament_id не задан"}) return JSONResponse({"message": "game_id или tournament_id не задан"})
return JSONResponse({ return JSONResponse(
{
"url": _build_game_url(current_tournament_id, selected_game_id), "url": _build_game_url(current_tournament_id, selected_game_id),
"game_id": selected_game_id, "game_id": selected_game_id,
"tournament_id": current_tournament_id "tournament_id": current_tournament_id,
}) }
)
@app.get("/game/data")
# @app.get("/info")
# async def info():
# if selected_game_id:
# df = load_today_schedule()
@app.get("/data")
async def game_data(): async def game_data():
with _latest_lock: with _latest_lock:
if latest_game_data: if latest_game_data:
return JSONResponse(latest_game_data) return JSONResponse(latest_game_data)
if latest_game_error: if latest_game_error:
return JSONResponse({"error": latest_game_error}, status_code=502) return JSONResponse({"error": latest_game_error}, status_code=502)
return JSONResponse({"message": "Ещё нет данных. Выберите матч и подождите первое обновление."}) return JSONResponse(
{"message": "Ещё нет данных. Выберите матч и подождите первое обновление."}
)
@app.get("/referee")
async def referee():
json_data = latest_game_data["data"]
data_referees = [
{
"number": json_data["game"]["mref1_num"],
"fullname": f'{json_data["game"]["mref1"].split()[1]} {json_data["game"]["mref1"].split()[0]}',
},
{
"number": json_data["game"]["mref2_num"],
"fullname": f'{json_data["game"]["mref2"].split()[1]} {json_data["game"]["mref2"].split()[0]}',
},
{
"number": json_data["game"]["lref1_num"],
"fullname": f'{json_data["game"]["lref1"].split()[1]} {json_data["game"]["lref1"].split()[0]}',
},
{
"number": json_data["game"]["lref2_num"],
"fullname": f'{json_data["game"]["lref2"].split()[1]} {json_data["game"]["lref2"].split()[0]}',
},
]
return data_referees
async def team(who: str):
""" "who: A - домашняя команда, B - гостевая"""
with _latest_lock:
lgd = latest_game_data
# print(lgd)
if not lgd or "data" not in lgd:
return [{"details": "Нет данных по матчу!"}]
players1_temp = lgd["data"]["players"][who]
players1 = []
players1_f = []
players1_d = []
players1_g = []
goaltenders1 = []
for player in players1_temp:
players1_temp[player]["mask"] = "#FF0000"
# for s in seasonA_json:
# if int(players1_temp[player]["id"]) == int(s["id"]):
# for key, value in s.items():
# # Создаем новый ключ с префиксом
# new_key = f"season_{key}"
# players1_temp[player][new_key] = value
if players1_temp[player]["ps"] not in ["в", "g"]:
data1 = players1_temp[player]
if "." in data1["name"]:
lastname1, *names1 = data1["name"].split()
names_new = " ".join(names1)
elif len(data1["name"].split()) == 3:
*lastname1, names1 = data1["name"].split()
names_new = names1
lastname1 = " ".join(lastname1)
else:
lastname1, *names1 = data1["name"].split()
names_new = " ".join(names1)
if players1_temp[player]["ps"] == "н":
position = "нападающий"
elif players1_temp[player]["ps"] == "з":
position = "защитник"
elif players1_temp[player]["ps"] == "f":
position = "forward"
elif players1_temp[player]["ps"] == "d":
position = "defenseman"
data_with_number = {
"number": player,
"NameSurnameGFX": names_new + " " + lastname1,
"NameGFX": names_new,
"SurnameGFX": lastname1,
"PositionGFX": position,
**data1,
}
if players1_temp[player]["ps"] == "н":
players1_f.append(data_with_number)
elif players1_temp[player]["ps"] == "з":
players1_d.append(data_with_number)
elif players1_temp[player]["ps"] == "f":
players1_f.append(data_with_number)
elif players1_temp[player]["ps"] == "d":
players1_d.append(data_with_number)
else:
data2 = players1_temp[player]
position = ""
if players1_temp[player]["ps"] == "в":
position = "вратарь"
elif players1_temp[player]["ps"] == "g":
position = "Goaltender"
lastname1, *names1 = data2["name"].split()
names_new = " ".join(names1)
data_with_number2 = {
"number": player,
"NameSurnameGFX": names_new + " " + lastname1,
"NameGFX": names_new,
"SurnameGFX": lastname1,
"PositionGFX": position,
**data2,
}
players1_g.append(data_with_number2)
goaltenders1.append(data_with_number2)
def make_empty(example_list):
if not example_list:
return {}
return {key: "" for key in example_list[0].keys()}
empty_d = make_empty(players1_d)
empty_f = make_empty(players1_f)
empty_g = make_empty(players1_g)
# добивка пустыми слотами
while len(players1_d) < 9:
players1_d.append(empty_d.copy())
while len(players1_f) < 13:
players1_f.append(empty_f.copy())
while len(players1_g) < 3:
players1_g.append(empty_g.copy())
players1 = players1_d + players1_f + players1_g
# print(len(players1))
return players1
@app.get("/team1")
async def team1():
return await team("A")
@app.get("/team2")
async def team2():
return await team("B")
# 👉 метка для первой строки (period row)
def _period_label(period: str | int) -> str:
s = str(period).strip().upper()
# овертаймы: OT, OT1, OT2... или числовые >= 4
if s == "OT" or (s.startswith("OT") and s[2:].isdigit()):
return "AFTER OVERTIME"
if s.isdigit():
n = int(s)
if n >= 4:
return "AFTER OVERTIME"
# 13 — с порядковым суффиксом
if n % 100 in (11, 12, 13):
suffix = "TH"
else:
suffix = {1: "ST", 2: "ND", 3: "RD"}.get(n % 10, "TH")
return f"AFTER {n}{suffix} PERIOD"
# например, Total
if s == "TOTAL":
return "FINAL"
return ""
# 👉 сортировка периодов: 1,2,3, затем OT/OT2/...
def _period_sort_key(k: str) -> tuple[int, int]:
s = str(k).strip().upper()
# 13 — обычные периоды
if s.isdigit():
n = int(s)
if 1 <= n <= 3:
return (n, 0)
# 4-й и далее — трактуем как овертаймы (4 -> OT1, 5 -> OT2 ...)
return (100, n - 3)
# явные овертаймы: OT, OT1, OT2...
if s == "OT":
return (100, 1)
if s.startswith("OT") and s[2:].isdigit():
return (100, int(s[2:]))
# неизвестные — в конец
return (200, 0)
def _sorted_period_keys(teams_periods: dict) -> list[str]:
a = teams_periods.get("A", {})
b = teams_periods.get("B", {})
keys = [k for k in a.keys() if k in b]
return sorted(keys, key=_period_sort_key)
def _current_period_key(payload: dict) -> str | None:
keys = _sorted_period_keys(payload.get("teams_periods", {}))
return keys[-1] if keys else None
# >>> 1) Замените вашу async-функцию get_team_stat на обычную sync:
def format_team_stat(team1: dict, team2: dict, period: str | None = None) -> list[dict]:
"""Форматирует статы двух команд в список записей для GFX (общие ключи + подписи)."""
stat_list = [
("coach_id", "coach id", "ID тренеров"),
("coach_fullname", "coach fullname", "Тренеры"),
("name", "name", "Команды"),
("outs", "Outs", ""),
("pim", "Penalty Minutes", "Минуты штрафа"),
("shots", "Shots on goal", "Броски в створ"),
("goals", "Goals", "Голы"),
("fo", "Face-offs", "Вбрасывания всего"),
("fow", "Face-offs won", "Вбрасывания"),
("fow_pct", "Face-offs won, %", "Выигранные вбрасывания, %"),
("hits", "Hits", "Силовые приемы"),
("bls", "Blocked shots", "Блокированные броски"),
("tka", "Takeaways", "Отборы"),
("toa", "Time on attack", "Время в атаке"),
("tie", "Even Strength Time on Ice", "Время в равных составах"),
("tipp", "Powerplay Time On Ice", "Время при большинстве"),
("tish", "Shorthanded Time On Ice", "Время при меньшинстве"),
("tien", "Emptry Net Time On Ice", "Время с пустыми воротами"),
("gva", "Giveaways", "Потери шайбы"),
("p_intc", "Pass interceptions", "Перехваты передачи"),
]
teams = [{k: str(v) for k, v in t.items()} for t in [team1, team2]]
keys = list(teams[0].keys())
formatted = []
if period is not None:
formatted.append(
{
"name0": "period",
"name1": str(period),
"name2": "",
"StatParameterGFX": _period_label(period) or "Period",
}
)
for key in keys:
row = {
"name0": key,
"name1": teams[0].get(key, ""),
"name2": teams[1].get(key, ""),
}
# подписи
for code, eng, _ru in stat_list:
if key == code:
row["StatParameterGFX"] = eng
break
formatted.append(row)
# постобработка отдельных полей
for r in formatted:
if r["name0"] == "fow_pct":
r["name1"] = str(round(float(r["name1"]))) if r["name1"] else r["name1"]
r["name2"] = str(round(float(r["name2"]))) if r["name2"] else r["name2"]
if r["name0"] == "coach_fullname":
# "Фамилия Имя" -> "Имя Фамилия" если есть пробел
def flip(s: str) -> str:
parts = s.split()
return f"{parts[1]} {parts[0]}" if len(parts) >= 2 else s
r["name1"] = flip(r["name1"])
r["name2"] = flip(r["name2"])
return formatted
# >>> 2) Вспомогалки для обхода периодов
def _iter_period_pairs(teams_periods: dict):
"""
Итерируем по периодам, отдаём (period_key, A_stats, B_stats).
Ключи сортируем по числу, если это цифры.
"""
a = teams_periods.get("A", {})
b = teams_periods.get("B", {})
def _key(k):
try:
return int(k)
except (TypeError, ValueError):
return k
for k in sorted(a.keys(), key=_key):
if k in b:
yield k, a[k], b[k]
def _build_all_stats(payload: dict) -> dict:
"""
Собирает общий блок ('total') и список по периодам ('periods') из json матча.
"""
total_a = payload["teams"]["A"]
total_b = payload["teams"]["B"]
result = {"total": format_team_stat(total_a, total_b), "periods": []}
for period_key, a_stat, b_stat in _iter_period_pairs(payload["teams_periods"]):
result["periods"].append(
{"period": period_key, "stats": format_team_stat(a_stat, b_stat)}
)
return result
@app.get("/teams/stats")
async def teams_stats(
scope: str = Query("all", pattern="^(all|total|period)$"),
n: str | None = Query(
None, description="Номер периода (строка или число) при scope=period"
),
):
"""
Все-в-одном: GET /teams/stats?scope=all
вернёт { total: [...], periods: [{period: "1", stats:[...]}, ...] }
Только общий: GET /teams/stats?scope=total
Конкретный период: GET /teams/stats?scope=period&n=2
"""
# читаем атомарно снапшот
with _latest_lock:
lgd = latest_game_data
if not lgd or "data" not in lgd or not isinstance(lgd["data"], dict):
raise HTTPException(status_code=400, detail="Нет данных по матчу.")
payload = lgd["data"]
if scope == "total":
data = format_team_stat(
payload["teams"]["A"], payload["teams"]["B"], period="Total"
)
return JSONResponse({"scope": "total", "data": data})
if scope == "period":
# 👉 если n не задан/или просит актуальный — берём последний период
wants_current = (n is None) or (str(n).strip().lower() in {"current", "last"})
if wants_current:
key = _current_period_key(payload)
if key is None:
raise HTTPException(status_code=404, detail="Периоды не найдены.")
n = key # используем актуальный ключ периода
# ключи в исходном json строковые
period_key = str(n)
a = payload["teams_periods"]["A"].get(period_key)
b = payload["teams_periods"]["B"].get(period_key)
if a is None or b is None:
raise HTTPException(
status_code=404, detail=f"Период {period_key} не найден."
)
return JSONResponse(
{
"scope": "period",
"period": period_key,
"is_current": period_key == _current_period_key(payload),
"data": format_team_stat(a, b, period=period_key),
}
)
# scope == "all"
cur = _current_period_key(payload)
return JSONResponse(
{"scope": "all", "current_period": cur, "data": _build_all_stats(payload)}
)
def _norm_name(s: str | None) -> str:
"""Нормализует название команды для сравнения."""
if not s:
return ""
return str(s).strip().casefold()
def _load_buf():
buf = nasio.load_bio(user=USER, password=PASSWORD,
nas_ip=SERVER_NAME, nas_port="443", path=PATH)
if isinstance(buf, (bytes, bytearray, memoryview)):
buf = io.BytesIO(buf)
buf.seek(0)
return buf
@app.get("/info")
async def info(format: str = "xlsx", sheet: str = "TEAMS"):
# 1) Проверяем, выбран ли матч
global current_season
if not selected_game_id:
return JSONResponse({"message": "Матч не выбран", "selected_id": None})
# 2) Берём расписание и ищем строку по выбранному ID
df = load_today_schedule()
if df.empty:
return JSONResponse(
{"message": "Сегодня матчей нет", "selected_id": selected_game_id}
)
# безопасно приводим id к int и ищем
try:
row = df.loc[df["id"].astype(int) == int(selected_game_id)].iloc[0]
except Exception:
return JSONResponse(
{
"message": "Выбранный матч не найден в расписании на сегодня",
"selected_id": selected_game_id,
},
status_code=404,
)
home_name = str(row.get("homeName_en", "")).strip()
away_name = str(row.get("visitorName_en", "")).strip()
# 3) Подтягиваем справочник команд из Excel (лист TEAMS)
src = _load_buf()
if format == "xlsx":
# читаем нужный лист из исходного XLSM
df = pd.read_excel(src, sheet_name=sheet, engine="openpyxl")
# пишем НОВЫЙ XLSX (без макросов) — это то, что понимает vMix
out = io.BytesIO()
with pd.ExcelWriter(out, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name=sheet, index=False)
out.seek(0)
return StreamingResponse(
out,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
# стабильное имя файла, чтобы vMix не путался
"Content-Disposition": "inline; filename=vmix.xlsx",
# отключаем кэш браузера/прокси, vMix сам опрашивает по интервалу
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
)
elif format == "csv":
df = pd.read_excel(src, sheet_name=sheet, engine="openpyxl")
csv_bytes = df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8")
return StreamingResponse(
io.BytesIO(csv_bytes),
media_type="text/csv; charset=utf-8",
headers={
"Content-Disposition": "inline; filename=vmix.csv",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
)
elif format == "json":
df = pd.read_excel(src, sheet_name=sheet, engine="openpyxl")
payload = json.dumps(df.to_dict(orient="records"), ensure_ascii=False)
return Response(
content=payload,
media_type="application/json; charset=utf-8",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
)
return Response("Unsupported format", status_code=400)
# # Оставляем только полезные поля (подгони под свой файл)
# keep = ["Team", "Logo", "Short", "HexPodl", "HexBase", "HexText"]
# keep = [c for c in keep if c in teams_df.columns]
# teams_df = teams_df.loc[:, keep].copy()
# # 4) Нормализованные ключи для джоина по имени
# teams_df["__key"] = teams_df["Team"].apply(_norm_name)
# def _pick_team_info(name: str) -> dict:
# key = _norm_name(name)
# hit = teams_df.loc[teams_df["__key"] == key]
# if hit.empty:
# # не нашли точное совпадение — вернём только название
# return {"Team": name}
# rec = hit.iloc[0].to_dict()
# rec.pop("__key", None)
# # заменим NaN/inf на None, чтобы JSON не падал
# for k, v in list(rec.items()):
# if pd.isna(v) or v in (np.inf, -np.inf):
# rec[k] = None
# return rec
# home_info = _pick_team_info(home_name)
# away_info = _pick_team_info(away_name)
# date_obj = datetime.strptime(row.get("datetime_str", ""), "%d.%m.%Y %H:%M")
# try:
# full_format = date_obj.strftime("%B %-d, %Y")
# except ValueError:
# full_format = date_obj.strftime("%B %#d, %Y")
# payload = [
# {
# "selected_id": int(selected_game_id),
# "tournament_id": (
# int(current_tournament_id) if current_tournament_id else None
# ),
# "datetime": str(full_format),
# "arena": str(row.get("arena_en", "")),
# "arena_city": str(row.get("arena_city_en", "")),
# "home": home_info,
# "home_city": str(row.get("homeCity_en", "")),
# "away": away_info,
# "away_city": str(row.get("visitorCity_en", "")),
# "season": current_season,
# }
# ]
# return JSONResponse(content=payload)
# except Exception as ex:
# pprint(ex)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(

View File

@@ -6,3 +6,7 @@ uvicorn>=0.30.0
requests>=2.31.0 requests>=2.31.0
python-telegram-handler python-telegram-handler
python-dotenv>=1.1.0 python-dotenv>=1.1.0
lxml
python-dotenv
--extra-index-url https://git.tvstart.ru/api/packages/lexx/pypi/simple
nasio