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
/.venv
logs/
/logs/*
__pycache__/
*.pyc
*.pyd
*.pyo

View File

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

Binary file not shown.

400
deploy.sh Normal file → Executable file
View File

@@ -4,18 +4,21 @@
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
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() {
echo "Использование: $0 -t <домашняя команда> -r <релиз> [-l <лига>]"
echo " -t Домашняя команда"
echo "Использование: $0 -r <релиз> [-h]"
echo " -r Релиз (тег или ветка в git)"
echo " -l Лига (опционально)"
echo " -h Показать эту справку"
echo ""
echo "Пример: $0 -t cska -r main"
echo "Пример: $0 -t zenit -r Barabanov_TEST -l vtb"
echo "Пример: $0 -t avtodor -r main"
echo "Пример: $0 -r Barabanov_TEST"
echo ""
exit 0
}
@@ -33,13 +36,41 @@ log_error() {
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() {
log_info "Обновление списка пакетов..."
apt-get update
if ! apt-get update; then
log_error "Ошибка при обновлении списка пакетов"
exit 1
fi
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
@@ -47,43 +78,47 @@ install_packages() {
exit 1
fi
if ! command -v pip3 &> /dev/null; then
log_error "pip3 не установлен!"
if ! command -v pip &> /dev/null; then
log_error "pip не установлен!"
exit 1
fi
log_info "Версия Python: $(python3 --version)"
log_info "Версия pip: $(pip3 --version)"
log_info "Версия pip: $(pip --version)"
}
# Функция загрузки кода
download_code() {
local repo_url="https://git.tvstart.ru/ychernenko/RFB.git"
local release="$1"
local target_dir="/root/RFB"
local target_dir="$TARGET_DIR"
log_info "Создание рабочей директории $target_dir..."
mkdir -p $target_dir
mkdir -p "$target_dir"
if [ -d "$target_dir/.git" ]; then
log_info "Обновление существующего репозитория..."
cd $target_dir
cd "$target_dir" || exit 1
git fetch --all
git checkout $release
git pull origin $release
if ! git checkout "$release"; then
log_error "Не удалось переключиться на релиз $release"
log_info "Доступные ветки:"
git branch -r
exit 1
fi
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 "Доступные ветки:"
git branch -r
exit 1
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)"
@@ -93,11 +128,20 @@ download_code() {
# Функция настройки виртуального окружения
setup_venv() {
local target_dir="/root/RFB"
cd $target_dir
local target_dir="$TARGET_DIR"
cd "$target_dir" || exit 1
# Удаляем существующее виртуальное окружение для чистоты
if [ -d ".venv" ]; then
log_info "Удаление существующего виртуального окружения..."
rm -rf .venv
fi
log_info "Создание виртуального окружения..."
python3 -m venv .venv
if ! python3 -m venv .venv; then
log_error "Не удалось создать виртуальное окружение"
exit 1
fi
if [ ! -f ".venv/bin/activate" ]; then
log_error "Не удалось создать виртуальное окружение"
@@ -108,7 +152,9 @@ setup_venv() {
source .venv/bin/activate
# Обновление pip
pip install --upgrade pip
if ! pip install --upgrade pip; then
log_warn "Не удалось обновить pip"
fi
# Проверка наличия requirements.txt
if [ -f "requirements.txt" ]; then
@@ -117,24 +163,29 @@ setup_venv() {
log_info "Все зависимости успешно установлены"
else
log_error "Ошибка при установке зависимостей из requirements.txt"
log_warn "Попытка установки базовых зависимостей..."
pip install requests pandas numpy
exit 1
fi
else
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
# Проверка основных пакетов
log_info "Проверка установки основных пакетов..."
python -c "
if ! python -c "
try:
import requests, pandas, numpy
print('✓ Все основные пакеты успешно импортируются')
except ImportError as e:
print(f'✗ Ошибка импорта: {e}')
exit(1)
"
"; then
log_error "Ошибка при проверке пакетов"
exit 1
fi
log_info "Установленные пакеты:"
pip list --format=columns
@@ -142,50 +193,52 @@ except ImportError as e:
# Функция создания systemd сервиса
create_systemd_service() {
local team="$1"
local league="$2"
log_info "Создание systemd сервиса для команды: $team"
if [ -n "$league" ]; then
log_info "Лига: $league"
fi
log_info "Создание systemd сервиса"
# Останавливаем и отключаем старый сервис если он есть
if systemctl is-active --quiet rfb-data.service; then
log_info "Остановка rfb-data.service..."
systemctl stop rfb-data.service
if systemctl is-active --quiet "$SERVICE_NAME"; then
log_info "Остановка $SERVICE_NAME..."
systemctl stop "$SERVICE_NAME"
fi
if systemctl is-enabled --quiet rfb-data.service; then
log_info "Отключение rfb-data.service..."
systemctl disable rfb-data.service
if systemctl is-enabled --quiet "$SERVICE_NAME"; then
log_info "Отключение $SERVICE_NAME..."
systemctl disable "$SERVICE_NAME"
fi
# Формируем команду для data сервиса
local data_command="/root/RFB/.venv/bin/python3 /root/RFB/get_data.py --team \"$team\""
if [ -n "$league" ]; then
data_command="$data_command --league \"$league\""
fi
# Создаем сервисный файл в правильной директории
local data_service_file="/etc/systemd/system/rfb-data.service"
local data_service_file="/etc/systemd/system/$SERVICE_NAME"
log_info "Создание файла сервиса: $data_service_file"
cat > "$data_service_file" << EOF
[Unit]
Description=RFB Data Service
Description=KHL Data Service
Documentation=https://git.tvstart.ru/ychernenko/KHL
After=network.target
Wants=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/RFB
Environment=PATH=/root/RFB/.venv/bin
ExecStart=$data_command
Restart=always
RestartSec=30
WorkingDirectory=$TARGET_DIR
Environment=PATH=$TARGET_DIR/.venv/bin
EnvironmentFile=$TARGET_ENV
ExecStart=python3 $TARGET_DIR/get_data.py
# Лимиты ресурсов
MemoryMax=1G
CPUQuota=80%
# Логирование
StandardOutput=journal
StandardError=journal
SyslogIdentifier=KHL
# Поведение при перезапуске
Restart=always
RestartSec=10
StartLimitInterval=300
StartLimitBurst=5
[Install]
WantedBy=multi-user.target
@@ -195,16 +248,22 @@ EOF
chmod 644 "$data_service_file"
log_info "Перезагрузка systemd демона..."
systemctl daemon-reload
if ! systemctl daemon-reload; then
log_error "Ошибка при перезагрузке systemd демона"
exit 1
fi
log_info "Включение сервиса rfb-data.service..."
systemctl enable rfb-data.service
log_info "Включение сервиса $SERVICE_NAME..."
if ! systemctl enable "$SERVICE_NAME"; then
log_error "Ошибка при включении сервиса"
exit 1
fi
}
# Функция проверки файлов
check_required_files() {
local target_dir="/root/RFB"
cd $target_dir
local target_dir="$TARGET_DIR"
cd "$target_dir" || exit 1
local missing_files=()
local required_files=("get_data.py")
@@ -230,74 +289,26 @@ check_required_files() {
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
setup_firewall() {
log_info "Настройка firewall (открытие порта 8000)..."
# Проверяем наличие ufw
if command -v ufw &> /dev/null && systemctl is-active --quiet ufw; then
ufw allow 8000/tcp
log_info "Порт 8000 открыт в ufw"
if ufw allow 8000/tcp; then
log_info "Порт 8000 открыт в ufw"
else
log_warn "Не удалось открыть порт 8000 в ufw"
fi
fi
# Для firewalld (CentOS/RHEL)
if command -v firewall-cmd &> /dev/null && systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-port=8000/tcp
firewall-cmd --reload
log_info "Порт 8000 открыт в firewalld"
if firewall-cmd --permanent --add-port=8000/tcp && firewall-cmd --reload; then
log_info "Порт 8000 открыт в firewalld"
else
log_warn "Не удалось открыть порт 8000 в firewalld"
fi
fi
}
@@ -306,9 +317,9 @@ check_port() {
local port=8000
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
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
fi
@@ -325,30 +336,42 @@ manage_services() {
case $action in
"start")
log_info "Запуск сервиса rfb-data.service..."
systemctl start rfb-data.service
log_info "Запуск сервиса $SERVICE_NAME..."
if ! systemctl start "$SERVICE_NAME"; then
log_error "Ошибка при запуске сервиса"
return 1
fi
;;
"restart")
log_info "Перезапуск сервиса rfb-data.service..."
systemctl restart rfb-data.service
log_info "Перезапуск сервиса $SERVICE_NAME..."
if ! systemctl restart "$SERVICE_NAME"; then
log_error "Ошибка при перезапуске сервиса"
return 1
fi
;;
"stop")
log_info "Остановка сервиса rfb-data.service..."
systemctl stop rfb-data.service
log_info "Остановка сервиса $SERVICE_NAME..."
if ! systemctl stop "$SERVICE_NAME"; then
log_error "Ошибка при остановке сервиса"
return 1
fi
;;
esac
# Даем время сервису на запуск/остановку
sleep 3
return 0
}
# Функция проверки статуса сервиса
check_service_status() {
log_info "Статус сервиса rfb-data.service:"
if systemctl is-active rfb-data.service; then
systemctl status rfb-data.service --no-pager -l
log_info "Статус сервиса $SERVICE_NAME:"
if systemctl is-active "$SERVICE_NAME" &>/dev/null; then
systemctl status "$SERVICE_NAME" --no-pager -l
return 0
else
log_warn "Сервис rfb-data.service не запущен"
log_warn "Сервис $SERVICE_NAME не запущен"
return 1
fi
}
@@ -363,7 +386,12 @@ check_system() {
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
log_warn "Мало свободного места: $((available_space / 1024)) MB"
else
@@ -371,39 +399,46 @@ check_system() {
fi
# Проверка памяти
local total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')
log_info "Оперативная память: $((total_mem / 1024)) MB"
if [ -f /proc/meminfo ]; then
local total_mem
total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')
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() {
local team=""
local release="main" # значение по умолчанию
local league="" # переменная для лиги
# Обработка аргументов командной строки
while getopts "t:r:l:h" opt; do
while getopts "r:h" opt; do
case $opt in
t) team="$OPTARG" ;;
r) release="$OPTARG" ;;
l) league="$OPTARG" ;;
h) show_help ;;
*) log_error "Неверный аргумент"; exit 1 ;;
esac
done
# Проверка обязательных аргументов
if [ -z "$team" ] && [ -z "$release" ]; then
log_error "Необходимо указать команду (-t) и релиз (-r)"
show_help
fi
log_info "Начало установки RFB Data Service..."
log_info "Команда: $team, Релиз: $release"
if [ -n "$league" ]; then
log_info "Лига: $league"
log_info "Начало установки KHL Data Service..."
if [ -n "$release" ]; then
log_info "Релиз: $release"
else
log_info "Лига: не указана (будет использовано значение по умолчанию)"
log_info "Релиз: не указан (будет использовано значение по умолчанию: main)"
release="main"
fi
# Проверка прав root
@@ -412,9 +447,15 @@ main() {
exit 1
fi
# Установка обработчика прерывания
trap 'log_error "Прервано пользователем"; cleanup; exit 1' INT TERM
# Проверка системы
check_system
# Проверка зависимостей
check_dependencies
# Установка пакетов
install_packages
@@ -427,16 +468,6 @@ main() {
# Настройка виртуального окружения
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
setup_firewall
@@ -444,46 +475,47 @@ main() {
check_port
# Создание systemd сервиса
create_systemd_service "$final_team" "$league"
create_systemd_service
log_info "Настройка завершена!"
# Запуск сервиса
manage_services "start"
if manage_services "start"; then
log_info "Сервис успешно запущен"
else
log_error "Не удалось запустить сервис"
exit 1
fi
# Проверка статуса
check_service_status
# Получаем IP для вывода
local ip_address
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)
log_info "=================================================="
log_info "Установка завершена успешно!"
log_info "Команда: $final_team"
if [ -n "$league" ]; then
log_info "Лига: $league"
fi
log_info "Режим: $release"
log_info "Релиз: $release"
log_info "Рабочая директория: $TARGET_DIR"
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 " Перезапуск: systemctl restart rfb-data.service"
log_info " Остановка: systemctl stop rfb-data.service"
log_info " Статус: systemctl status rfb-data.service"
log_info " Логи: journalctl -u rfb-data.service"
log_info " Перезапуск: systemctl restart $SERVICE_NAME"
log_info " Остановка: systemctl stop $SERVICE_NAME"
log_info " Статус: systemctl status $SERVICE_NAME"
log_info " Логи: journalctl -u $SERVICE_NAME"
log_info ""
log_info "Проверка работы:"
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 "=================================================="
}
# Обработка прерывания
trap 'log_error "Прервано пользователем"; exit 1' INT TERM
# Запуск основной функции
main "$@"

View File

@@ -1,5 +1,6 @@
from fastapi import FastAPI
from fastapi.responses import Response, JSONResponse, HTMLResponse
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import Response, JSONResponse, HTMLResponse, StreamingResponse
from fastapi.encoders import jsonable_encoder
import pandas as pd
import requests, io, os
from requests.auth import HTTPBasicAuth
@@ -9,29 +10,39 @@ import uvicorn
from threading import Thread, Event, Lock
import time
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
current_tournament_id: int | None = None # будем обновлять при загрузке расписания
latest_game_data: dict | None = None # сюда кладём последние данные по матчу
current_tournament_id: int | None = None # будем обновлять при загрузке расписания
current_season: str | None = None # будем обновлять при загрузке расписания
latest_game_data: dict | None = None # сюда кладём последние данные по матчу
latest_game_error: str | None = None
_latest_lock = Lock()
_stop_event = Event()
_worker_thread: Thread | None = None
# Загружаем переменные из .env
load_dotenv()
pprint(f"Локальный файл окружения ={load_dotenv(verbose=True)}")
api_user = os.getenv("API_USER")
api_pass = os.getenv("API_PASS")
league = os.getenv("LEAGUE")
api_base_url = os.getenv("API_BASE_URL")
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():
"""Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF)."""
url_tournaments = "http://stat2tv.khl.ru/tournaments.xml"
url_tournaments = f"{api_base_url}tournaments.xml"
r = requests.get(
url_tournaments, auth=HTTPBasicAuth(api_user, api_pass), verify=False
)
@@ -51,14 +62,26 @@ def load_today_schedule():
return pd.DataFrame()
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
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)
schedule_df = pd.read_xml(io.StringIO(r.text))
# Нужные колонки (скорректируй под реальные имена из 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]
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:
# 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:
"""Один запрос к API матча -> чистый JSON из API."""
url = _build_game_url(tournament_id, game_id)
r = requests.get(
url,
auth=HTTPBasicAuth(api_user, api_pass),
verify=False,
timeout=10
url, auth=HTTPBasicAuth(api_user, api_pass), verify=False, timeout=10
)
r.raise_for_status()
@@ -121,7 +142,7 @@ def _fetch_game_once(tournament_id: int, game_id: int) -> dict:
# если это не JSON — вернём текст
data = {"raw": r.text}
return {"url": url, "json": data}
return data
def _game_poll_worker():
@@ -166,11 +187,18 @@ async def lifespan(app: FastAPI):
_worker_thread.join(timeout=2)
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")
async def games():
df = load_today_schedule()
if df.empty:
return JSONResponse({"message": "Сегодня матчей нет"})
@@ -179,8 +207,8 @@ async def games():
return Response(content=json_schedule, media_type="application/json")
@app.get("/games/html")
async def games_html():
@app.get("/select")
async def select():
df = load_today_schedule()
if df.empty:
return HTMLResponse(
@@ -194,7 +222,8 @@ async def games_html():
home = row.get("homeName_en", "")
away = row.get("visitorName_en", "")
when = row.get("datetime_str", "")
arena = row.get("arena", "")
arena = row.get("arena_en", "")
city = row.get("arena_city_en", "")
rows_html.append(
f"""
<tr data-id="{gid}">
@@ -203,6 +232,7 @@ async def games_html():
<td>{home}</td>
<td>{away}</td>
<td>{arena}</td>
<td>{city}</td>
<td><button class="pick">Выбрать</button></td>
</tr>
"""
@@ -233,7 +263,7 @@ async def games_html():
<table id="tbl">
<thead>
<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>
</thead>
<tbody>
@@ -309,20 +339,538 @@ async def get_selected_game():
async def game_url():
if not (selected_game_id and current_tournament_id):
return JSONResponse({"message": "game_id или tournament_id не задан"})
return JSONResponse({
"url": _build_game_url(current_tournament_id, selected_game_id),
"game_id": selected_game_id,
"tournament_id": current_tournament_id
})
return JSONResponse(
{
"url": _build_game_url(current_tournament_id, selected_game_id),
"game_id": selected_game_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():
with _latest_lock:
if latest_game_data:
return JSONResponse(latest_game_data)
if latest_game_error:
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__":
uvicorn.run(

View File

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