Compare commits

..

113 Commits

Author SHA1 Message Date
26eb5a9398 изменил функцию записи файлов 2025-10-30 10:58:31 +03:00
a97a28de6f исправил получение данных по сезону на команды. обновляется один раз 2025-10-29 14:23:49 +03:00
764c50e42d вернул обратно host на ref.russiabasket.org 2025-10-29 13:26:13 +03:00
d49244de54 Merge branch 'RFB4' of https://git.tvstart.ru/ychernenko/RFB into RFB4 2025-10-29 13:24:34 +03:00
a9605be520 подмена файлов get_data 2025-10-29 13:24:31 +03:00
763df5cb43 Добавлен аргумент -l 2025-10-29 09:16:10 +00:00
662f88c68b убрал indent=4. говорят это замедляет запись в json, если там много данных, а там много данных 2025-10-29 12:14:57 +03:00
c438f69578 поправил таймслип 2025-10-29 12:09:02 +03:00
9bf3624c8c поправил atomic_write_json 2025-10-29 11:52:17 +03:00
7b44ab3d5d get_json поменял местами данные 2025-10-29 11:44:13 +03:00
6721951c34 добавленны обработки json в потоках 2025-10-29 10:58:29 +03:00
ea34b6521f поправил нормальное отображение данных, пока данных по матчу нет.
1. отображение логотипов сегодняшего матча и надписи кто с кем
2. отображение средних показателей команд за сезон
3. турнирная таблица с подсветкой текущих команд
2025-10-28 16:12:18 +03:00
5ab179e47b Добавленна функция Pregame_JSON, которая сохраняет Team Comparison 2025-10-28 15:54:51 +03:00
1839b8aec3 time.sleep(.5) 2025-10-28 15:20:02 +03:00
f11b315d2f поправленны данные, когда матч онлайн, но нет live-status и box-score 2025-10-28 14:25:23 +03:00
15367b05fc обновил статусы матчей, чтобы правильнее ожидалось, когда матч сегодня 2025-10-28 13:45:50 +03:00
94d487fe88 добавлены Судьи, поправлены Ход игры и События игры на офлайн матч 2025-10-28 12:52:50 +03:00
049e2493b5 не доделал play_by_play 2025-10-27 21:14:52 +03:00
7e5ccdbb83 счет по четвертям в отображении 2025-10-27 21:06:14 +03:00
97045a0f72 поправил турнирку 2025-10-27 20:48:31 +03:00
b301f1e918 поправил еще раз запись данных, когда матч закончился 2025-10-27 20:34:24 +03:00
d86d1e5a4f поправил аргумент лиги, по дефолту vtb 2025-10-27 20:27:44 +03:00
6278a8e3df добавил функцию по прогону данных один раз, если матч старый 2025-10-27 20:17:24 +03:00
4ad11815a5 исправил проверку на live-status 2025-10-27 20:04:12 +03:00
cbcad5e525 подсветка команда в турнирке 2025-10-27 19:50:05 +03:00
48c5f552ec турнирка 2025-10-27 19:44:45 +03:00
b0511d51d3 отрефакторенный код 2025-10-27 19:34:24 +03:00
6fa1ad3f35 сон 2025-10-27 19:26:40 +03:00
d821967f33 добавил сон до следующего дня после выполнения работы 2025-10-27 19:23:27 +03:00
b11dbdeae8 test23 2025-10-27 19:11:08 +03:00
91c6eeddc5 test22 2025-10-27 19:02:32 +03:00
842af85847 test21 2025-10-27 19:00:15 +03:00
225e158c53 test20 2025-10-27 18:59:33 +03:00
276ecdcb85 test19 2025-10-27 18:58:50 +03:00
adc067293f test18 2025-10-27 18:52:32 +03:00
27a7ffcaad test17 2025-10-27 18:44:33 +03:00
02582bdab0 test16 2025-10-27 18:43:16 +03:00
4048f8fdfa test15 2025-10-27 18:40:31 +03:00
b0aba27764 test14 2025-10-27 18:39:25 +03:00
b27e02bb49 test13 2025-10-27 18:32:30 +03:00
ff3883aa36 test12 2025-10-27 18:29:19 +03:00
f72914a085 test11 2025-10-27 18:22:37 +03:00
10baa5b2fb test10 2025-10-27 18:18:40 +03:00
6a06d54aa4 test9 2025-10-27 18:15:14 +03:00
58bfdce841 test8 2025-10-27 18:13:03 +03:00
9fa68a165d test7 2025-10-27 18:01:04 +03:00
ee58ad868d test6 2025-10-27 17:54:49 +03:00
5470df8da9 test5 2025-10-27 17:53:18 +03:00
8fa3acf3f2 test4 2025-10-27 17:49:58 +03:00
858ed5c6a7 test3 2025-10-27 17:44:09 +03:00
09dfbef09a test2 2025-10-27 17:34:50 +03:00
c4de3c84fe test1 2025-10-27 16:55:15 +03:00
000b304ed9 test 2025-10-27 13:51:00 +03:00
8f34737186 исправил данные из box-score, которые успешно проебал 2025-10-24 19:09:18 +03:00
58bff8ba53 поправил фолы в live_status и отображение их в visual 2025-10-24 18:54:45 +03:00
82f1450ff5 Merge branch 'main' of https://git.tvstart.ru/ychernenko/RFB 2025-10-24 18:47:11 +03:00
7a61c0bfb6 обновил visual для подгрузки турнирной таблицы
поправил функцию в get_data которая пушит онлайн данные (требует тщательной проверки)
2025-10-24 18:47:07 +03:00
d75f0e6407 add oline logo 2025-10-24 15:37:42 +00:00
dcfcbb0958 добавлена функция генерации турнирной таблицы раз в 10 минут с сообщением в телеграм 2025-10-24 18:12:26 +03:00
629854c104 добавленны создание онлайн/оффлайн фолов и поправил функцию на склеивание онлайн данных (нужно проверить) 2025-10-24 17:35:12 +03:00
893d18ee23 изменил чат id для телеграм 2025-10-24 16:22:46 +03:00
c9613f91a3 добавил функцию Scores_Quarter 2025-10-24 15:41:42 +03:00
5ea03d2efc создание папки logs 2025-10-24 15:19:16 +03:00
bacee0e9b8 добавил две функции: командная статистика и рефери 2025-10-24 15:10:38 +03:00
0844d577ff добавил в Json_Team_Generation сохранение файлов Top_team и Started_team 2025-10-24 14:49:40 +03:00
d7c819357d поправил чтение json файлов 2025-10-24 14:15:37 +03:00
efa4812b09 Испараввлена проблема ковычек в аргументе передоваемом deploy.sh в data.py 2025-10-24 13:59:59 +03:00
c62c8a31a6 поправил некоторые выводы в телеграм 2025-10-24 10:35:02 +00:00
a13cb945c7 поправлен logger 2025-10-24 10:16:57 +00:00
5083423660 переобут logger 2025-10-24 10:06:38 +00:00
c30bc088c7 новая версия get_data. вроде как работает обработка данных в зависимости от статуса матча и если матч старый грузится один раз данные, и ожидает следующего дня 2025-10-24 09:32:19 +00:00
d9ad941a80 старая версия get_data 2025-10-24 09:31:02 +00:00
84169208ab Добавление ковычек при создании сервиса дата 2025-10-24 09:18:16 +00:00
39ba101948 Merge branch 'main' of https://git.tvstart.ru/ychernenko/RFB 2025-10-23 21:51:19 +03:00
886339b183 хз что) 2025-10-23 21:51:04 +03:00
00a01eb8a9 Update README.md 2025-10-22 18:21:33 +00:00
313c82c006 Merge branch 'Barabanov_TEST' into main 2025-10-22 17:06:18 +00:00
b45d7ec129 Merge branch 'Barabanov_TEST' of https://git.tvstart.ru/ychernenko/RFB into Barabanov_TEST 2025-10-22 16:08:46 +00:00
8b2f2f5e23 Add README.md 2025-10-22 15:16:53 +00:00
b021034b4a rename 2025-10-22 15:07:53 +00:00
963c7a503b no "$team" 2025-10-22 14:31:10 +00:00
e375613692 add config.toml 2025-10-22 13:47:36 +00:00
0b85081ba5 22.10.2025 test REF 2025-10-22 13:22:23 +00:00
b8b79b5f58 ThreadPoolExecutor = 1 2025-10-22 16:00:58 +03:00
6d3d5d3702 Console INFO 2025-10-22 15:59:06 +03:00
9b0c276d36 change from pro to ref 2025-10-22 15:50:03 +03:00
b0f59e5bf3 jkshdbgv 2025-10-22 12:47:16 +00:00
5cb7b2f6c8 GBPLF 2025-10-22 15:43:36 +03:00
a1b1466f4b add "$tem" to ExecStart 2025-10-22 11:06:24 +00:00
dbc6ca3ef9 Team name change 2025-10-22 10:56:28 +00:00
856ca094b1 Merge remote-tracking branch 'origin/main' into Barabanov_TEST 2025-10-22 10:49:11 +00:00
139740b48f dell 2025-10-22 10:47:27 +00:00
1b13768d12 Merge branch 'main' of https://git.tvstart.ru/ychernenko/RFB 2025-10-22 10:40:32 +00:00
c3469b62ef merge 2025-10-22 10:34:32 +00:00
74467aedb5 Merge branch 'main' into Barabanov_TEST 2025-10-22 09:52:20 +00:00
982655faa1 22.10.205 2025-10-22 09:47:44 +00:00
6e104a565c cleanUP_2 2025-10-22 12:40:57 +03:00
2e517607e4 cleanUP 2025-10-22 12:39:20 +03:00
aa62821e5f 22.10.2025 2025-10-22 12:34:34 +03:00
8ea87b371a add print for debug 2025-10-21 21:10:17 +00:00
8f6ae70389 split servises 2025-10-21 16:17:48 +00:00
92211a55f1 split servises 2025-10-21 09:27:29 +00:00
d4d9964282 add streamlit_autorefresh 2025-10-21 09:01:49 +00:00
24860de8c6 add start_rfb.sh 2025-10-21 08:56:41 +00:00
5ad95a1d57 setup.sh 2025-10-21 08:35:06 +00:00
d4e1dacf20 add streamlit 2025-10-20 16:41:52 +00:00
d528ee8de6 add requirements 2025-10-20 16:17:59 +00:00
2d553db6bc cleanUP 2025-10-20 15:55:42 +00:00
0db5d80e19 clean_UP 2025-10-20 15:53:18 +00:00
Alexey Barabanov
ca7cd72b74 SH update 2025-10-20 18:43:53 +03:00
Alexey Barabanov
1a344d2be2 RUN.sh update 2025-10-20 14:03:23 +03:00
Alexey Barabanov
2bd99cf663 add RUN.sh 2025-10-20 13:59:12 +03:00
Alexey Barabanov
e1aa4c993a Test_RUN 2025-10-20 13:33:37 +03:00
38 changed files with 8129 additions and 67303 deletions

7
.gitignore vendored
View File

@@ -1,3 +1,8 @@
.venv/*
/JSON/*
/logs/*
/logs/*
/static/*
get_data_new copy 2.py
get_data_new copy.py
temp.json
get_data_new copy 3.py

5
.streamlit/config.toml Normal file
View File

@@ -0,0 +1,5 @@
[browser]
gatherUsageStats = false
[server]
enableStaticServing = true

105
README.md Normal file
View File

@@ -0,0 +1,105 @@
![VTB logo](https://vtb-league.com/docs/brand/2019/VTB%20League%20logo%20RGB.png)
RFB Stat - это система для автоматического сбора баскетбольной статистики.
Система состоит из двух основных компонентов:
Сервис данных (rfb-data.service) - сбор и обработка данных
Сервис визуализации (rfb-visual.service) - веб-интерфейс на основе Streamlit
Требования
* Ubuntu Linux
* Доступ к репозиторию `https://git.tvstart.ru/`
* *Права root для установки!!*
# Установка
Скрипт установки выполняет полную настройку системы:
```shell
chmod +x deploy.sh
./deploy.sh -t <команда> -r <релиз>
```
Параметры командной строки
-t - Название команды (опционально, определяется по IP если не указано)
-r - Релиз (ветка или тег в git, по умолчанию: main)
-h - Справка по использованию
# Примеры использования
Установка с автоопределением команды по IP
```shell
./deploy.sh -r main
```
Установка для конкретной команды
```shell
./deploy.sh -t cska -r main
```
Установка тестовой версии
```shell
./deploy.sh -t zenit -r Barabanov_TEST
```
# Пакеты и прочее что будет установленно:
Системные пакеты:
* Python3
* pip3
* virtualenv
* Git
* Net-tools
Виртуальное окружение Python с зависимостями:
* streamlit
* requests
* pandas
* numpy
* plotly
* watchdog
* pillow
* streamlit_autorefresh
# Systemd сервисы:
`rfb-data.service - сбор данных`
`rfb-visual.service - веб-интерфейс`
# Управление сервисами
Просмотр статуса обоих сервисов одновременно
```shell
systemctl status rfb-data.service rfb-visual.service
```
# Просмотр логов
Логи сбора данных
```shell
journalctl -u rfb-data.service -f
```
Логи веб-интерфейса
```shell
journalctl -u rfb-visual.service -f
```
# Управление сервисами
Перезапуск всех сервисов
```shell
systemctl restart rfb-data.service rfb-visual.service
```
Остановка всех сервисов
```shell
systemctl stop rfb-data.service rfb-visual.service
```
Запуск всех сервисов
```shell
systemctl start rfb-data.service rfb-visual.service
```
# Доступ к приложению
После установки приложение доступно по адресу:
`http://ВАШ_IP_АДРЕС:8501`

View File

@@ -1,6 +0,0 @@
@echo off
REM 1. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> vmix_url_replace.exe <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
"%~dp0vmix_url_replace.exe" --file "D:\extGFX\VTB.vmix" --url "https://per.tvstart.ru/app/static/per_"
REM 2. <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> vMix
start "" "D:\extGFX\VTB.vmix"

View File

@@ -1,6 +0,0 @@
@echo off
REM 1. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> vmix_url_replace.exe <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
"%~dp0vmix_url_replace.exe" --file "D:\extGFX\VTB.vmix" --url "https://kaz.tvstart.ru/app/static/kaz_"
REM 2. <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> vMix
start "" "D:\extGFX\VTB.vmix"

495
deploy.sh Executable file
View File

@@ -0,0 +1,495 @@
#!/bin/bash
# Настройка цветов для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
show_help() {
echo "Использование: $0 -t <домашняя команда> -r <релиз> [-l <лига>]"
echo " -t Домашняя команда"
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 ""
exit 0
}
# Функция для вывода цветных сообщений
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Функция проверки и установки пакетов
install_packages() {
log_info "Обновление списка пакетов..."
apt-get update
log_info "Установка необходимых пакетов..."
apt-get install -y python3 python3-pip python3-venv git systemd net-tools
# Проверка установки
if ! command -v python3 &> /dev/null; then
log_error "Python3 не установлен!"
exit 1
fi
if ! command -v pip3 &> /dev/null; then
log_error "pip3 не установлен!"
exit 1
fi
log_info "Версия Python: $(python3 --version)"
log_info "Версия pip: $(pip3 --version)"
}
# Функция загрузки кода
download_code() {
local repo_url="https://git.tvstart.ru/ychernenko/RFB.git"
local release="$1"
local target_dir="/root/RFB"
log_info "Создание рабочей директории $target_dir..."
mkdir -p $target_dir
if [ -d "$target_dir/.git" ]; then
log_info "Обновление существующего репозитория..."
cd $target_dir
git fetch --all
git checkout $release
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
fi
log_info "Код успешно загружен (релиз: $release)"
log_info "Текущая ветка: $(git branch --show-current)"
}
# Функция обновления requirements.txt
update_requirements() {
local target_dir="/root/RFB"
cd $target_dir
log_info "Обновление requirements.txt..."
# Создаем или обновляем requirements.txt
if [ ! -f "requirements.txt" ]; then
log_warn "requirements.txt не найден, создаем новый..."
cat > requirements.txt << EOF
streamlit>=1.28.0
requests>=2.31.0
pandas>=2.0.0
numpy>=1.24.0
plotly>=5.15.0
watchdog>=3.0.0
pillow>=10.0.0
streamlit_autorefresh>=0.1.7
EOF
else
# Проверяем, есть ли streamlit_autorefresh в requirements.txt
if ! grep -q "streamlit_autorefresh" requirements.txt; then
log_info "Добавляем streamlit_autorefresh в requirements.txt..."
echo "streamlit_autorefresh>=0.1.7" >> requirements.txt
else
log_info "streamlit_autorefresh уже есть в requirements.txt"
fi
fi
log_info "Содержимое requirements.txt:"
cat requirements.txt
}
# Функция настройки виртуального окружения
setup_venv() {
local target_dir="/root/RFB"
cd $target_dir
log_info "Создание виртуального окружения..."
python3 -m venv .venv
if [ ! -f ".venv/bin/activate" ]; then
log_error "Не удалось создать виртуальное окружение"
exit 1
fi
log_info "Активация виртуального окружения и установка зависимостей..."
source .venv/bin/activate
# Обновление pip
pip install --upgrade pip
# Обновляем requirements.txt перед установкой
update_requirements
if [ -f "requirements.txt" ]; then
log_info "Установка зависимостей из requirements.txt..."
pip install -r requirements.txt
else
log_warn "Файл requirements.txt не найден, устанавливаем базовые зависимости..."
pip install streamlit requests pandas numpy plotly watchdog pillow streamlit_autorefresh
fi
# Дополнительная проверка и установка streamlit_autorefresh на всякий случай
if ! python -c "import streamlit_autorefresh" 2>/dev/null; then
log_info "Устанавливаем streamlit_autorefresh..."
pip install streamlit_autorefresh
fi
# Проверка установки streamlit
if ! [ -f ".venv/bin/streamlit" ]; then
log_error "Streamlit не установлен в виртуальном окружении"
exit 1
fi
log_info "Проверка установленных пакетов:"
python -c "
import streamlit, requests, pandas, numpy, plotly, streamlit_autorefresh
print('✓ Все основные пакеты успешно импортируются')
"
log_info "Установленные пакеты:"
pip list --format=columns | grep -E "(streamlit|requests|pandas|numpy|plotly|autorefresh)"
}
# Функция создания systemd сервисов
create_systemd_services() {
local team="$1"
local league="$2"
log_info "Создание отдельных systemd сервисов для команды: $team"
if [ -n "$league" ]; then
log_info "Лига: $league"
fi
# Останавливаем и отключаем старые сервисы если они есть
for service in rfb-data.service rfb-visual.service rfb-stat.service; do
if systemctl is-active --quiet $service; then
log_info "Остановка $service..."
systemctl stop $service
fi
if systemctl is-enabled --quiet $service; then
log_info "Отключение $service..."
systemctl disable $service
fi
done
# Формируем команду для 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
# Сервис для data
local data_service_file="/etc/systemd/system/rfb-data.service"
cat > $data_service_file << EOF
[Unit]
Description=RFB Data Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/RFB
Environment=PATH=/root/RFB/.venv/bin
ExecStart=$data_command
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Сервис для visual
local visual_service_file="/etc/systemd/system/rfb-visual.service"
cat > $visual_service_file << EOF
[Unit]
Description=RFB Visual Service
After=network.target rfb-data.service
[Service]
Type=simple
User=root
WorkingDirectory=/root/RFB
Environment=PATH=/root/RFB/.venv/bin
ExecStart=/root/RFB/.venv/bin/streamlit run /root/RFB/visual.py --server.port 8501 --server.address 0.0.0.0
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Настройка прав
chmod 644 $data_service_file $visual_service_file
log_info "Перезагрузка systemd демонов..."
systemctl daemon-reload
log_info "Включение сервисов..."
systemctl enable rfb-data.service rfb-visual.service
}
# Функция проверки файлов
check_required_files() {
local target_dir="/root/RFB"
cd $target_dir
local missing_files=()
if [ ! -f "visual.py" ]; then
missing_files+=("visual.py")
fi
if [ ! -f "get_data.py" ]; then
missing_files+=("get_data.py")
fi
if [ ${#missing_files[@]} -ne 0 ]; then
log_error "Отсутствуют необходимые файлы: ${missing_files[*]}"
log_error "Содержимое директории $target_dir:"
ls -la
exit 1
fi
log_info "Все необходимые файлы присутствуют"
}
# Функция определения IP и команды
detect_team() {
local team_arg="$1"
# Определение IP адреса
if [[ "$(uname -s)" == "Linux" ]]; then
ip_address=$(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 "Доступные команды:"
for ip in "${!hosts[@]}"; do
echo " $ip: ${hosts[$ip]}"
done
exit 1
fi
# Возвращаем только чистую строку с названием команды
echo "$final_team"
}
# Функция настройки firewall
setup_firewall() {
log_info "Настройка firewall (открытие порта 8501)..."
# Проверяем наличие ufw
if command -v ufw &> /dev/null; then
ufw allow 8501/tcp
log_info "Порт 8501 открыт в ufw"
fi
# Для firewalld (CentOS/RHEL)
if command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-port=8501/tcp
firewall-cmd --reload
log_info "Порт 8501 открыт в firewalld"
fi
}
# Функция проверки порта
check_port() {
local port=8501
if command -v netstat &> /dev/null && netstat -tuln | grep ":$port " > /dev/null; then
log_warn "Порт $port уже занят. Возможно, приложение уже запущено."
return 1
fi
if command -v ss &> /dev/null && ss -tuln | grep ":$port " > /dev/null; then
log_warn "Порт $port уже занят. Возможно, приложение уже запущено."
return 1
fi
return 0
}
# Функция управления сервисами
manage_services() {
local action="$1"
case $action in
"start")
log_info "Запуск сервисов..."
systemctl start rfb-data.service rfb-visual.service
;;
"restart")
log_info "Перезапуск сервисов..."
systemctl restart rfb-data.service rfb-visual.service
;;
"stop")
log_info "Остановка сервисов..."
systemctl stop rfb-data.service rfb-visual.service
;;
esac
# Даем время сервисам на запуск/остановку
sleep 3
}
# Функция проверки статуса сервисов
check_services_status() {
log_info "Статус сервисов:"
echo "=== RFB Data Service ==="
systemctl status rfb-data.service --no-pager
echo ""
echo "=== RFB Visual Service ==="
systemctl status rfb-visual.service --no-pager
}
# Основная функция
main() {
local team=""
local release="main" # значение по умолчанию
local league="" # новая переменная для лиги
# Обработка аргументов командной строки
while getopts "t:r:l:h" opt; do
case $opt in
t) team="$OPTARG" ;;
r) release="$OPTARG" ;;
l) league="$OPTARG" ;;
h) show_help ;;
*) log_error "Неверный аргумент"; exit 1 ;;
esac
done
log_info "Начало установки RFB Stat..."
log_info "Команда: $team, Релиз: $release"
if [ -n "$league" ]; then
log_info "Лига: $league"
else
log_info "Лига: не указана (будет использовано значение по умолчанию)"
fi
# Проверка прав root
if [[ $EUID -ne 0 ]]; then
log_error "Этот скрипт должен запускаться с правами root"
exit 1
fi
# Установка пакетов
install_packages
# Загрузка кода
download_code "$release"
# Настройка виртуального окружения
setup_venv
# Определение команды - ВАЖНО: используем временный файл для чистого вывода
local temp_team_file=$(mktemp)
final_team=$(detect_team "$team" | tail -1)
rm -f "$temp_team_file"
log_info "Финальная команда: $final_team"
# Проверка файлов
check_required_files
# Настройка firewall
setup_firewall
# Проверка порта
check_port
# Создание systemd сервисов
create_systemd_services "$final_team" "$league"
log_info "Настройка завершена!"
# Запуск сервисов
manage_services "start"
# Проверка статуса
check_services_status
# Получаем IP еще раз для вывода
ip_address=$(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 "Приложение должно быть доступно по адресу: http://${ip_address}:8501"
log_info "Команда: $final_team"
if [ -n "$league" ]; then
log_info "Лига: $league"
fi
log_info "Режим: $release"
log_info ""
log_info "Для просмотра логов:"
log_info " Данные: journalctl -u rfb-data.service -f"
log_info " Визуал: journalctl -u rfb-visual.service -f"
log_info ""
log_info "Управление сервисами:"
log_info " Перезапуск всех: systemctl restart rfb-data.service rfb-visual.service"
log_info " Остановка всех: systemctl stop rfb-data.service rfb-visual.service"
log_info " Статус: systemctl status rfb-data.service rfb-visual.service"
log_info "=================================================="
}
# Запуск основной функции
main "$@"

File diff suppressed because it is too large Load Diff

3165
get_data_old.py Normal file

File diff suppressed because it is too large Load Diff

2113
get_data_old2.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"10.10.1.180": {"host": "", "tag": "vtb", "root": 1, "team": "cska"},
"10.0.85.2111": {"host": "", "tag": "vtb", "root": 1, "team": ""},
"10.10.35.21": {"host": "gfx", "tag": "vtb", "root": 1, "team": "Lokomotiv Kuban"},
"10.10.35.22": {"host": "krd", "tag": "vtb", "root": 1, "team": "Lokomotiv Kuban1"},
"10.10.35.23": {"host": "ekb", "tag": "vtb", "root": 1, "team": "uralmash1"},

Binary file not shown.

Binary file not shown.

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
requests>=2.28.0
numpy>=1.21.0
pandas>=1.5.0
urllib3>=1.26.0
python-telegram-handler>=0.1.0
streamlit
streamlit_autorefresh

View File

@@ -1,7 +0,0 @@
::Скрипт запуска отображения данных в RFB
chcp 65001
@echo off >nul
echo запускаю софт
streamlit run visual.py

View File

@@ -1,97 +0,0 @@
Dim url As String = "https://ekb.tvstart.ru/app/static/ekb_team1.json"
Dim json As String = ""
' Чтение URL
Try
Dim wc As New System.Net.WebClient
wc.Encoding = System.Text.Encoding.UTF8
json = wc.DownloadString(url)
Catch ex As Exception
Console.WriteLine("Ошибка загрузки JSON: " & ex.Message)
Return
End Try
' --- Парсинг первых 12 num + NameGFX ---
Dim nums(11) As String
Dim names(11) As String
Dim count As Integer = 0
Dim pos As Integer = 0
While count < 12
' ищем "num"
Dim k As Integer = json.IndexOf("""num""", pos)
If k = -1 Then Exit While
Dim c As Integer = json.IndexOf(":", k)
If c = -1 Then Exit While
Dim j As Integer = c + 1
While j < json.Length AndAlso Char.IsWhiteSpace(json(j))
j += 1
End While
Dim numVal As String = ""
If j < json.Length AndAlso json(j) = """"c Then
j += 1
Dim startQ As Integer = j
While j < json.Length AndAlso json(j) <> """"c
j += 1
End While
numVal = json.Substring(startQ, j - startQ)
j += 1
Else
Dim startN As Integer = j
While j < json.Length AndAlso (Char.IsDigit(json(j)) OrElse json(j) = "-"c OrElse json(j) = "."c)
j += 1
End While
numVal = json.Substring(startN, j - startN)
End If
' ищем "NameGFX"
pos = j
Dim kn As Integer = json.IndexOf("""NameGFX""", pos)
If kn = -1 Then Exit While
Dim cn As Integer = json.IndexOf(":", kn)
If cn = -1 Then Exit While
Dim jn As Integer = cn + 1
While jn < json.Length AndAlso Char.IsWhiteSpace(json(jn))
jn += 1
End While
Dim nameVal As String = ""
If jn < json.Length AndAlso json(jn) = """"c Then
jn += 1
Dim startGN As Integer = jn
While jn < json.Length AndAlso json(jn) <> """"c
jn += 1
End While
nameVal = json.Substring(startGN, jn - startGN)
jn += 1
End If
nums(count) = numVal
names(count) = nameVal
count += 1
pos = jn
End While
' --- Выводим результат ---
Console.WriteLine("=== Первые " & count.ToString() & " игроков ===")
For i As Integer = 0 To count - 1
Console.WriteLine(nums(i) & "_" & names(i))
Next
' --- Выводим результат ---
Console.WriteLine("=== Первые " & count.ToString() & " игроков ===")
For i As Integer = 0 To count - 1
Console.WriteLine(nums(i) & "_" & names(i))
' === Отправляем в титр TeamRoster.gtzip ===
' Номер
API.Function("SetText", Input:="TeamRoster.gtzip", SelectedName:="PlayerNamber" & (i + 1).ToString() & ".Text", Value:=nums(i))
' Имя
API.Function("SetText", Input:="TeamRoster.gtzip", SelectedName:="PlayerName" & (i + 1).ToString() & ".Text", Value:=names(i))
Next

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
[
{
"displayNumber": "",
"positionName": "Crew chief",
"lastNameGFX": "Aleksey Davydov",
"secondName": "Mikhailovich",
"birthday": "1984-10-05T00:00:00",
"age": 41,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Referee 1",
"lastNameGFX": "Sergey Mikhailov",
"secondName": "Alexandrovich",
"birthday": "1977-05-20T00:00:00",
"age": 48,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Referee 2",
"lastNameGFX": "Maksim Zhitlukhin",
"secondName": "Sergeevich",
"birthday": "1986-12-19T00:00:00",
"age": 38,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Commissioner",
"lastNameGFX": "Igor Lebedev",
"secondName": "Anatolevich",
"birthday": "1965-05-27T00:00:00",
"age": 60,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Scorer",
"lastNameGFX": "Viktoriya Isaeva",
"secondName": "Dmitrievna",
"birthday": "1996-01-09T00:00:00",
"age": 29,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Assistant Scorer",
"lastNameGFX": "Dmitriy Kibenko",
"secondName": "Andreevich",
"birthday": "1983-12-25T00:00:00",
"age": 41,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Timekeeper",
"lastNameGFX": "Olga Prosneva",
"secondName": "Nikolaevna",
"birthday": "1971-06-15T00:00:00",
"age": 54,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Operator 24 sec",
"lastNameGFX": "Aleksey Nagibin",
"secondName": "Vitalevich",
"birthday": "1982-08-21T00:00:00",
"age": 43,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Dictor",
"lastNameGFX": "Адель Халимов",
"secondName": "Рашидович",
"birthday": "1996-07-31T00:00:00",
"age": 29,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Statistic",
"lastNameGFX": "Veronika Shuvagina",
"secondName": "Vladimirovna",
"birthday": "1968-05-08T00:00:00",
"age": 57,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "IS Operator",
"lastNameGFX": "Rashid Khabibullin",
"secondName": "Rinatovich",
"birthday": "1987-07-26T00:00:00",
"age": 38,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Statistic",
"lastNameGFX": "Mariya Shuvagina",
"secondName": "Dmitrievna",
"birthday": "2002-08-04T00:00:00",
"age": 23,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Video reviewer",
"lastNameGFX": "Kamil Habibullin",
"secondName": "Ildarovich",
"birthday": "1975-12-08T00:00:00",
"age": 49,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "-",
"lastNameGFX": "None None",
"secondName": null,
"birthday": null,
"age": null,
"flag": "https://flagicons.lipis.dev/flags/4x3/.svg"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
[
{
"Q": "Q1",
"score1": "33",
"score2": "20"
},
{
"Q": "Q2",
"score1": "26",
"score2": "21"
},
{
"Q": "Q3",
"score1": "26",
"score2": "20"
},
{
"Q": "Q4",
"score1": "18",
"score2": "22"
},
{
"Q": "OT1",
"score1": "",
"score2": ""
},
{
"Q": "OT2",
"score1": "",
"score2": ""
},
{
"Q": "OT3",
"score1": "",
"score2": ""
},
{
"Q": "OT4",
"score1": "",
"score2": ""
}
]

View File

@@ -1,88 +0,0 @@
[
{
"team": "UNICS",
"winQ1": 0,
"loseQ1": 0,
"drawQ1": 0,
"scoreQ1": 0,
"score_avgQ1": null,
"winQ2": 0,
"loseQ2": 0,
"drawQ2": 0,
"scoreQ2": 0,
"score_avgQ2": null,
"winQ3": 0,
"loseQ3": 0,
"drawQ3": 0,
"scoreQ3": 0,
"score_avgQ3": null,
"winQ4": 0,
"loseQ4": 0,
"drawQ4": 0,
"scoreQ4": 0,
"score_avgQ4": null,
"winOT1": 0,
"loseOT1": 0,
"drawOT1": 0,
"scoreOT1": 0,
"score_avgOT1": null,
"winOT2": 0,
"loseOT2": 0,
"drawOT2": 0,
"scoreOT2": 0,
"score_avgOT2": null,
"winOT3": 0,
"loseOT3": 0,
"drawOT3": 0,
"scoreOT3": 0,
"score_avgOT3": null,
"winOT4": 0,
"loseOT4": 0,
"drawOT4": 0,
"scoreOT4": 0,
"score_avgOT4": null
},
{
"team": "BETCITY PARMA",
"winQ1": 0,
"loseQ1": 0,
"drawQ1": 0,
"scoreQ1": 0,
"score_avgQ1": null,
"winQ2": 0,
"loseQ2": 0,
"drawQ2": 0,
"scoreQ2": 0,
"score_avgQ2": null,
"winQ3": 0,
"loseQ3": 0,
"drawQ3": 0,
"scoreQ3": 0,
"score_avgQ3": null,
"winQ4": 0,
"loseQ4": 0,
"drawQ4": 0,
"scoreQ4": 0,
"score_avgQ4": null,
"winOT1": 0,
"loseOT1": 0,
"drawOT1": 0,
"scoreOT1": 0,
"score_avgOT1": null,
"winOT2": 0,
"loseOT2": 0,
"drawOT2": 0,
"scoreOT2": 0,
"score_avgOT2": null,
"winOT3": 0,
"loseOT3": 0,
"drawOT3": 0,
"scoreOT3": 0,
"score_avgOT3": null,
"winOT4": 0,
"loseOT4": 0,
"drawOT4": 0,
"scoreOT4": 0,
"score_avgOT4": null
}
]

View File

@@ -1 +0,0 @@
[]

View File

@@ -1 +0,0 @@
[]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,296 +0,0 @@
[
{
"name": "points",
"nameGFX_rus": "Очки",
"nameGFX_eng": "points",
"val1": "103",
"val2": "83"
},
{
"name": "goal2",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "28",
"val2": "25"
},
{
"name": "shot2",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "42",
"val2": "43"
},
{
"name": "goal3",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "9",
"val2": "7"
},
{
"name": "shot3",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "24",
"val2": "17"
},
{
"name": "goal1",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "20",
"val2": "12"
},
{
"name": "shot1",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "21",
"val2": "16"
},
{
"name": "assist",
"nameGFX_rus": "Передачи",
"nameGFX_eng": "assists",
"val1": "29",
"val2": "24"
},
{
"name": "pass",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "32",
"val2": "24"
},
{
"name": "steal",
"nameGFX_rus": "Перехваты",
"nameGFX_eng": "steals",
"val1": "10",
"val2": "9"
},
{
"name": "block",
"nameGFX_rus": "Блокшоты",
"nameGFX_eng": "blocks",
"val1": "3",
"val2": "0"
},
{
"name": "blocked",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "3"
},
{
"name": "defReb",
"nameGFX_rus": "подборы в защите",
"nameGFX_eng": "",
"val1": "19",
"val2": "15"
},
{
"name": "offReb",
"nameGFX_rus": "подборы в нападении",
"nameGFX_eng": "",
"val1": "14",
"val2": "8"
},
{
"name": "foulsOn",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "24",
"val2": "23"
},
{
"name": "turnover",
"nameGFX_rus": "Потери",
"nameGFX_eng": "turnovers",
"val1": "16",
"val2": "19"
},
{
"name": "foul",
"nameGFX_rus": "Фолы",
"nameGFX_eng": "fouls",
"val1": "23",
"val2": "24"
},
{
"name": "foulT",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "foulD",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "foulC",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "foulB",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "second",
"nameGFX_rus": "секунды",
"nameGFX_eng": "seconds",
"val1": "12000",
"val2": "12000"
},
{
"name": "dunk",
"nameGFX_rus": "данки",
"nameGFX_eng": "dunks",
"val1": "7",
"val2": "4"
},
{
"name": "fastBreak",
"nameGFX_rus": "",
"nameGFX_eng": "fast breaks",
"val1": "6",
"val2": "4"
},
{
"name": "plusMinus",
"nameGFX_rus": "+/-",
"nameGFX_eng": "+/-",
"val1": "100",
"val2": "-100"
},
{
"name": "pt-1",
"nameGFX_rus": "Штрафные",
"nameGFX_eng": "free throws",
"val1": "20/21",
"val2": "12/16"
},
{
"name": "pt-2",
"nameGFX_rus": "2-очковые",
"nameGFX_eng": "2-points",
"val1": "28/42",
"val2": "25/43"
},
{
"name": "pt-3",
"nameGFX_rus": "3-очковые",
"nameGFX_eng": "3-points",
"val1": "9/24",
"val2": "7/17"
},
{
"name": "fg",
"nameGFX_rus": "очки с игры",
"nameGFX_eng": "field goals",
"val1": "37/66",
"val2": "32/60"
},
{
"name": "pt-1_pro",
"nameGFX_rus": "штрафные, процент",
"nameGFX_eng": "free throws pro",
"val1": "95%",
"val2": "75%"
},
{
"name": "pt-2_pro",
"nameGFX_rus": "2-очковые, процент",
"nameGFX_eng": "2-points pro",
"val1": "67%",
"val2": "58%"
},
{
"name": "pt-3_pro",
"nameGFX_rus": "3-очковые, процент",
"nameGFX_eng": "3-points pro",
"val1": "38%",
"val2": "41%"
},
{
"name": "fg_pro",
"nameGFX_rus": "Очки с игры, процент",
"nameGFX_eng": "field goals pro",
"val1": "56%",
"val2": "53%"
},
{
"name": "Reb",
"nameGFX_rus": "Подборы",
"nameGFX_eng": "rebounds",
"val1": "33",
"val2": "23"
},
{
"name": "avgAge",
"nameGFX_rus": "",
"nameGFX_eng": "avg Age",
"val1": "26.8",
"val2": "25.4"
},
{
"name": "ptsStart",
"nameGFX_rus": "",
"nameGFX_eng": "Start PTS",
"val1": "74",
"val2": "51"
},
{
"name": "ptsStart_pro",
"nameGFX_rus": "",
"nameGFX_eng": "Start PTS, %",
"val1": "72%",
"val2": "61%"
},
{
"name": "ptsBench",
"nameGFX_rus": "",
"nameGFX_eng": "Bench PTS",
"val1": "29",
"val2": "32"
},
{
"name": "ptsBench_pro",
"nameGFX_rus": "",
"nameGFX_eng": "Bench PTS, %",
"val1": "28%",
"val2": "39%"
},
{
"name": "avgHeight",
"nameGFX_rus": "",
"nameGFX_eng": "avg height",
"val1": "198.8 cm",
"val2": "201.1 cm"
},
{
"name": "timeout_left",
"nameGFX_rus": "",
"nameGFX_eng": "timeout left",
"val1": "2",
"val2": "1"
},
{
"name": "timeout_str",
"nameGFX_rus": "",
"nameGFX_eng": "timeout str",
"val1": "2 Time-outs left in 2nd half",
"val2": "1 Time-out left in 2nd half"
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
import requests
import json
url = "https://org.infobasket.su/Widget/GetOnline/921412?format=json&lang=ru"
response = requests.get(url)
response.raise_for_status()
data = response.json()
# print(data)
team1 = data["GameTeams"][0]
team2 = data["GameTeams"][1]
print(team1)

11252
temp.json

File diff suppressed because it is too large Load Diff

479
visual.py
View File

@@ -19,7 +19,7 @@ st.set_page_config(
page_icon="🏀",
layout="wide",
initial_sidebar_state="expanded",
menu_items={"About": "версия 2.0 от 08.10.2025"},
menu_items={"About": "версия 3.2 от 23.10.2025"},
)
REMOVE_PADDING_FROM_SIDES = """
<style>
@@ -181,7 +181,29 @@ def process_player_data(team_json, player_index):
"time": str(player_data["CareerTPlayedTime"]),
}
return [season_total, season_avg, career_total], player_data
career_avg = {
"name": "Career Average",
"game_count": "",
"start_count": "",
"pts": str(player_data["AvgCarPoints"]),
"pt-2": str(player_data["CareerTShot2Percent"]),
"pt-3": str(player_data["CareerTShot3Percent"]),
"pt-1": str(player_data["CareerTShot1Percent"]),
"fg": str(player_data["CareerTShot23Percent"]),
"ast": str(player_data["AvgCarAssist"]),
"stl": str(player_data["AvgCarSteal"]),
"blk": str(player_data["AvgCarBlocks"]),
"dreb": str(player_data["AvgCarDefRebound"]),
"oreb": str(player_data["AvgCarOffRebound"]),
"reb": str(player_data["AvgCarRebound"]),
# "to": str(player_data["AvgTurnover"]),
# "foul": str(player_data["AvgFoul"]),
"fouled": str(player_data["AvgCarOpponentFoul"]),
"dunk": str(player_data["AvgCarDunk"]),
"time": str(player_data["AvgCarPlayedTime"]),
}
return [season_total, season_avg, career_total, career_avg], player_data
config = {
@@ -190,24 +212,24 @@ config = {
"num": st.column_config.TextColumn("#", width=27),
"NameGFX": st.column_config.TextColumn(width=170),
"isOn": st.column_config.TextColumn("🏀", width=27),
"pts": st.column_config.NumberColumn("PTS", width=27),
"pt-2": st.column_config.TextColumn("2-PT", width=45),
"pt-3": st.column_config.TextColumn("3-PT", width=45),
"pt-1": st.column_config.TextColumn("FT", width=45),
"fg": st.column_config.TextColumn("FG", width=45),
"ast": st.column_config.NumberColumn("AS", width=27),
"stl": st.column_config.NumberColumn("ST", width=27),
"blk": st.column_config.NumberColumn("BL", width=27),
"blkVic": st.column_config.NumberColumn("BV", width=27),
"dreb": st.column_config.NumberColumn("DR", width=27),
"oreb": st.column_config.NumberColumn("OR", width=27),
"reb": st.column_config.NumberColumn("R", width=27),
"to": st.column_config.NumberColumn("TO", width=27),
"foul": st.column_config.NumberColumn("F", width=27),
"fouled": st.column_config.NumberColumn("Fed", width=27),
"plusMinus": st.column_config.NumberColumn("+/-", width=27),
"dunk": st.column_config.NumberColumn("DUNK", width=27),
"kpi": st.column_config.NumberColumn("KPI", width=27),
"pts": st.column_config.TextColumn("PTS", width="content", help="⭐ = Career High"),
"pt-2": st.column_config.TextColumn("2-PT", width="content"),
"pt-3": st.column_config.TextColumn("3-PT", width="content"),
"pt-1": st.column_config.TextColumn("FT", width="content"),
"fg": st.column_config.TextColumn("FG", width="content"),
"ast": st.column_config.TextColumn("AS", width="content"),
"stl": st.column_config.TextColumn("ST", width="content"),
"blk": st.column_config.TextColumn("BL", width="content"),
"blkVic": st.column_config.TextColumn("BV", width="content"),
"dreb": st.column_config.TextColumn("DR", width="content"),
"oreb": st.column_config.TextColumn("OR", width="content"),
"reb": st.column_config.TextColumn("R", width="content"),
"to": st.column_config.TextColumn("TO", width="content"),
"foul": st.column_config.TextColumn("F", width="content"),
"fouled": st.column_config.TextColumn("Fed", width="content"),
"plusMinus": st.column_config.TextColumn("+/-", width="content"),
"dunk": st.column_config.TextColumn("DUNK", width="content"),
"kpi": st.column_config.TextColumn("KPI", width="content"),
"time": st.column_config.TextColumn("TIME"),
"game_count": st.column_config.TextColumn("G", width=27),
"start_count": st.column_config.TextColumn("S", width=27),
@@ -261,11 +283,7 @@ if "player2" not in st.session_state:
st.session_state.player2 = None
myhost = platform.node()
if sys.platform.startswith("win"): # было: if platform == "win32":
FOLDER_JSON = "JSON"
else:
FOLDER_JSON = "static"
FOLDER_JSON = "static"
def get_ip_address():
@@ -303,7 +321,8 @@ def load_data_from_json(filepath):
directory = FOLDER_JSON
os.makedirs(directory, exist_ok=True)
filepath_full = os.path.join(directory, f"{filepath}.json")
print(filepath_full)
print(filepath)
# вычисление ключа
# ip = get_ip_address()
# host = ip_check.get(ip, {}).get("host") or ""
@@ -389,9 +408,7 @@ def rewrite_file(filename: str, data: dict, directory: str = "JSON") -> None:
os.makedirs(directory, exist_ok=True)
host_prefix = _ipcheck()
filepath = os.path.join(directory, f"{host_prefix}{filename}.json")
filepath = os.path.join(directory, f"{filename}.json")
# print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании
try:
@@ -406,53 +423,51 @@ ip_check = read_match_id_json("match_id.json") or {}
prefix = _ipcheck()
load_data_from_json(f"{prefix}game_online")
cached_game_online = st.session_state.get("game_online")
load_data_from_json("game")
cached_game_online = st.session_state.get("game")
load_data_from_json(f"{prefix}team1")
load_data_from_json("api_game")
cached_api_game = st.session_state.get("api_game")
load_data_from_json("team1")
cached_team1 = st.session_state.get("team1")
load_data_from_json(f"{prefix}team2")
load_data_from_json("team2")
cached_team2 = st.session_state.get("team2")
load_data_from_json(f"{prefix}referee")
load_data_from_json("referee")
cached_referee = st.session_state.get("referee")
# standings — может не быть тега/файла
league_tag = None
if isinstance(cached_game_online, dict):
league_tag = ((cached_game_online.get("result") or {}).get("league") or {}).get(
"tag"
)
comp_name = (
((cached_game_online.get("result") or {}).get("comp") or {})
.get("name")
.replace(" ", "_")
)
if isinstance(cached_api_game, dict):
league_tag = (cached_api_game["result"].get("league") or {}).get("tag")
comp_name = (cached_api_game["result"].get("comp") or {}).get("name").replace(" ", "_").replace("|", "")
if league_tag:
load_data_from_json(f"{prefix}standings_{league_tag}_{comp_name}")
load_data_from_json(f"standings_{league_tag}_{comp_name}")
cached_standings = (
st.session_state.get(f"standings_{league_tag}_{comp_name}") if league_tag else None
)
load_data_from_json(f"{prefix}scores_quarter")
load_data_from_json("scores_quarter")
cached_scores_quarter = st.session_state.get("scores_quarter")
load_data_from_json(f"{prefix}play_by_play")
load_data_from_json("play_by_play")
cached_play_by_play = st.session_state.get("play_by_play")
load_data_from_json(f"{prefix}team_stats")
load_data_from_json("team_stats")
cached_team_stats = st.session_state.get("team_stats")
load_data_from_json(f"{prefix}scores")
load_data_from_json("scores")
cached_scores = st.session_state.get("scores") or [] # важно!
load_data_from_json(f"{prefix}live_status")
cached_live_status = st.session_state.get("live_status")
load_data_from_json("api_live-status")
cached_live_status = st.session_state.get("api_live-status")
load_data_from_json(f"{prefix}schedule")
load_data_from_json("schedule")
cached_schedule = st.session_state.get("schedule")
load_data_from_json(f"{prefix}team_comparison")
load_data_from_json("team_comparison")
cached_team_comparison = st.session_state.get("team_comparison")
@@ -544,8 +559,8 @@ if isinstance(cached_play_by_play, list) and isinstance(cached_game_online, dict
timeout1 = []
timeout2 = []
if isinstance(cached_game_online, dict):
result = cached_game_online.get("result") or {}
if isinstance(cached_api_game, dict):
result = cached_api_game["result"]
plays = result.get("plays") or []
timeout1, timeout2 = [], []
@@ -560,6 +575,7 @@ if isinstance(cached_game_online, dict):
t1 = result.get("team1") or {}
t2 = result.get("team2") or {}
if t1.get("logo"):
col1.image(t1["logo"], width=100)
team1_name = t1.get("name") or ""
@@ -593,18 +609,16 @@ if isinstance(cached_game_online, dict):
col4_3.metric("TimeOuts", len(timeout1))
col5_1.metric("TimeOuts", len(timeout2))
if isinstance(cached_live_status, list) and cached_live_status:
foulsA = (cached_live_status[0] or {}).get("foulsA")
foulsB = (cached_live_status[0] or {}).get("foulsB")
if isinstance([cached_live_status], list) and cached_live_status:
foulsA = (cached_live_status["result"] or {}).get("foulsA")
foulsB = (cached_live_status["result"] or {}).get("foulsB")
if foulsA is not None:
col4_2.metric("Fouls", foulsA)
if foulsB is not None:
col5_2.metric("Fouls", foulsB)
if isinstance(cached_game_online, dict) and (
(cached_game_online.get("result") or {}).get("plays") or []
):
if isinstance(cached_game_online, dict) and (cached_game_online.get("plays") or []):
col_1_col = [f"col_1_{i}" for i in range(1, period_max + 1)]
col_2_col = [f"col_2_{i}" for i in range(1, period_max + 1)]
count_q = 0
@@ -814,18 +828,154 @@ if cached_team1 and cached_team2:
team1_data = process_team_data(cached_team1, columns_game)
team2_data = process_team_data(cached_team2, columns_game)
# Добавляем звездочку, если pts > PTS_Career_High
def _get_first_number(x):
"""Безопасно вытащить число из строки/значения (например '12 (60%)' -> 12)."""
try:
if x is None:
return None
s = str(x)
# заберём ведущие число/знак (поддержим +/-)
import re
m = re.search(r"[-+]?\d+(\.\d+)?", s)
return float(m.group(0)) if m else None
except Exception:
return None
CAREER_HIGH_KEYS = {
"pts": ["PTS_Career_High", "CareerHighPoints", "career_high_pts"],
"ast": ["AST_Career_High", "CareerHighAssist", "career_high_ast"],
"stl": ["STL_Career_High", "CareerHighSteal", "career_high_stl"],
"blk": ["BLK_Career_High", "CareerHighBlocks", "career_high_blk"],
"reb": ["REB_Career_High", "CareerHighRebound", "career_high_reb"],
# если нужно — добавь ещё пары "df_column": ["possible_key1","possible_key2"...]
}
def _build_career_high_map(cached_team_list):
"""Вернёт словарь: player_id -> {stat_key: value} для всех доступных максимумов."""
out = {}
if not isinstance(cached_team_list, list):
return out
for p in cached_team_list:
if not isinstance(p, dict):
continue
pid = p.get("id")
if pid is None:
continue
out[pid] = {}
for stat_col, aliases in CAREER_HIGH_KEYS.items():
for k in aliases:
if k in p and p[k] not in (None, ""):
out[pid][stat_col] = _get_first_number(p[k])
break
return out
def _ensure_id_column(df, cached_team_list):
"""Присвоить игрокам id в том же порядке, что и в списке cached_team."""
try:
ids = [
p.get("id") if isinstance(p, dict) else None for p in cached_team_list
][: len(df)]
if "id" not in df.columns:
df["id"] = ids
else:
# не затираем, только заполняем пустые
df["id"] = df["id"].fillna(pd.Series(ids, index=df.index))
except Exception:
pass
def _mark_star_for_columns(df, cached_team_list, columns):
"""
Для каждого col в columns: если текущее значение > career high — добавляем ' ⭐️'.
Преобразуем колонки в текст (для отображения эмодзи).
"""
_ensure_id_column(df, cached_team_list)
ch_map = _build_career_high_map(cached_team_list)
def format_with_star(val, career_max):
v = _get_first_number(val)
cm = _get_first_number(career_max)
if v is not None and cm is not None and v >= cm and val > 0:
# сохраняем исходное текстовое представление + ⭐️
return f"{val} ⭐️"
return f"{val}" if val is not None else ""
for col in columns:
if col not in df.columns:
continue
new_vals = []
for idx, row in df.iterrows():
pid = row.get("id")
career_max = (ch_map.get(pid, {}) or {}).get(col)
new_vals.append(format_with_star(row[col], career_max))
df[col] = new_vals # теперь это текст для отображения
STAR_COLUMNS = [
"pts",
"ast",
"stl",
"blk",
"reb",
]
team1_data["_pts_num"] = pd.to_numeric(team1_data["pts"], errors="coerce")
team1_data["_kpi_num"] = pd.to_numeric(team1_data["kpi"], errors="coerce")
team2_data["_pts_num"] = pd.to_numeric(team2_data["pts"], errors="coerce")
team2_data["_kpi_num"] = pd.to_numeric(team2_data["kpi"], errors="coerce")
def highlight_max_by_refcol(df, view_col, ref_col):
ref = pd.to_numeric(df[ref_col], errors="coerce")
mx = ref.max()
return [
("background-color: green" if (pd.notna(v) and v == mx and v > 0) else "")
for v in ref
]
_mark_star_for_columns(team1_data, cached_team1, STAR_COLUMNS)
_mark_star_for_columns(team2_data, cached_team2, STAR_COLUMNS)
# Стилизация данных
# team1_styled = (
# team1_data.style.apply(highlight_grey, axis=1)
# .apply(highlight_foul, subset="foul")
# .apply(highlight_max, subset="pts")
# .apply(highlight_max, subset="kpi")
# )
# team2_styled = (
# team2_data.style.apply(highlight_grey, axis=1)
# .apply(highlight_foul, subset="foul")
# .apply(highlight_max, subset="pts")
# .apply(highlight_max, subset="kpi")
# )
team1_styled = (
team1_data.style.apply(highlight_grey, axis=1)
team1_data[columns_game]
.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
.apply(highlight_max, subset="kpi")
.apply(
lambda _: highlight_max_by_refcol(team1_data, "pts", "_pts_num"),
axis=0,
subset=["pts"],
)
.apply(
lambda _: highlight_max_by_refcol(team1_data, "kpi", "_kpi_num"),
axis=0,
subset=["kpi"],
)
)
team2_styled = (
team2_data.style.apply(highlight_grey, axis=1)
team2_data[columns_game]
.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
.apply(highlight_max, subset="kpi")
.apply(
lambda _: highlight_max_by_refcol(team2_data, "pts", "_pts_num"),
axis=0,
subset=["pts"],
)
.apply(
lambda _: highlight_max_by_refcol(team2_data, "kpi", "_kpi_num"),
axis=0,
subset=["kpi"],
)
)
def get_player_all_game(player_data_1):
@@ -863,29 +1013,28 @@ if cached_team1 and cached_team2:
# Сортировка от последнего матча к первому
df_filtered = df_filtered.sort_values(by="game.gameDate", ascending=False)
# Указать нужные колонки для вывода
columns_to_show = [
"season",
"game.gameDate",
"game.team1Name",
"game.team2Name",
"game.score",
"stats.points",
"stats.shot2Percent",
"stats.shot3Percent",
"stats.shot23Percent",
"stats.shot1Percent",
"stats.assist",
"stats.steal",
"stats.blockShot",
"stats.defRebound",
"stats.offRebound",
"stats.rebound",
"stats.turnover",
"stats.foul",
"stats.playedTime",
"stats.plusMinus",
"Сезон",
"Дата",
"Команда 1",
"Команда 2",
"Счёт",
"PTS",
"2-PTS%",
"3-PTS%",
"FG%",
"FT%",
"AST",
"STL",
"BLK",
"DR",
"OR",
"REB",
"TO",
"F",
"TIME",
"+/-",
]
numeric_cols = [
"stats.points",
@@ -897,24 +1046,63 @@ if cached_team1 and cached_team2:
# df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(
# pd.to_numeric, errors="coerce"
# )
df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(pd.to_numeric, errors="coerce")
# 🟢 Переименовываем колонки для отображения в Streamlit
rename_map = {
"season": "Сезон",
"game.gameDate": "Дата",
"game.team1Name": "Команда 1",
"game.team2Name": "Команда 2",
"game.score": "Счёт",
"stats.points": "PTS",
"stats.shot2Percent": "2-PTS%",
"stats.shot3Percent": "3-PTS%",
"stats.shot23Percent": "FG%",
"stats.shot1Percent": "FT%",
"stats.assist": "AST",
"stats.steal": "STL",
"stats.blockShot": "BLK",
"stats.defRebound": "DR",
"stats.offRebound": "OR",
"stats.rebound": "REB",
"stats.turnover": "TO",
"stats.foul": "F",
"stats.playedTime": "TIME",
"stats.plusMinus": "+/-",
}
df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(
pd.to_numeric, errors="coerce"
)
df_filtered[numeric_cols] = df_filtered[numeric_cols].round(0).astype("Int64")
df_filtered = df_filtered.rename(columns=rename_map)
df_filtered["Дата"] = df_filtered["Дата"].dt.strftime("%d.%m.%Y")
styled = (
df_filtered[columns_to_show]
.style
.apply(highlight_max, subset=["stats.points"])
.apply(highlight_max, subset=["stats.assist"])
.apply(highlight_max, subset=["stats.steal"])
.apply(highlight_max, subset=["stats.blockShot"])
.apply(highlight_max, subset=["stats.rebound"])
.style.apply(highlight_max, subset=["PTS"])
.apply(highlight_max, subset=["AST"])
.apply(highlight_max, subset=["STL"])
.apply(highlight_max, subset=["BLK"])
.apply(highlight_max, subset=["REB"])
.format(
{
"Дата": lambda x: x, # уже строка, просто оставляем как есть
"PTS": "{:,.0f}".format,
"AST": "{:,.0f}".format,
"STL": "{:,.0f}".format,
"BLK": "{:,.0f}".format,
"REB": "{:,.0f}".format,
}
)
)
return styled
# Вывод данных
col_player1, col_player2 = tab_temp_1.columns((5, 5))
config_copy = config.copy()
event1 = col_player1.dataframe(
team1_styled,
column_config=config,
column_config=config_copy,
hide_index=True,
height=460,
on_select="rerun",
@@ -960,7 +1148,8 @@ if cached_team1 and cached_team2:
hide_index=True,
)
col_player1.dataframe(get_player_all_game(player_data_1))
col_player1.title("Статистика каждой игры")
col_player1.dataframe(get_player_all_game(player_data_1), hide_index=True)
if event2.selection and event2.selection.get("rows"):
selected_index2 = event2.selection["rows"][0]
@@ -988,7 +1177,8 @@ if cached_team1 and cached_team2:
column_config=config_season,
hide_index=True,
)
col_player2.dataframe(get_player_all_game(player_data_2))
col_player2.title("Статистика каждой игры")
col_player2.dataframe(get_player_all_game(player_data_2), hide_index=True)
team_col1, team_col2 = tab_temp_2.columns((5, 5))
if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34:
@@ -1054,8 +1244,8 @@ column_config_ref = {
def highlight_teams(s):
try:
if s.iloc[0] in (
cached_game_online["result"]["team1"]["teamId"],
cached_game_online["result"]["team2"]["teamId"],
cached_api_game["result"]["team1"]["teamId"],
cached_api_game["result"]["team2"]["teamId"],
):
return ["background-color: #FF4B4B"] * len(s)
else:
@@ -1069,28 +1259,41 @@ if cached_standings:
def highlight_teams(s):
try:
t1 = (
((cached_game_online or {}).get("result") or {})
.get("team1", {})
.get("teamId")
)
t2 = (
((cached_game_online or {}).get("result") or {})
.get("team2", {})
.get("teamId")
)
t1 = (cached_api_game["result"] or {}).get("team1", {}).get("teamId")
t2 = (cached_api_game["result"] or {}).get("team2", {}).get("teamId")
if s.iloc[0] in (t1, t2):
return ["background-color: #FF4B4B"] * len(s)
if s.iloc[0] in (t1, t2):
return ["background-color: #FF4B4B"] * len(s)
except Exception:
pass
return [""] * len(s)
styled = df_st.style.apply(highlight_teams, axis=1)
styled = df_st[
[
"teamId",
"start",
"place",
"name",
"regionName",
"totalGames",
"totalWin",
"totalDefeat",
"totalPoints",
"totalGoalPlus",
"totalGoalMinus",
"logo",
"w_l",
"procent",
"plus_minus",
]
].style.apply(highlight_teams, axis=1)
tab_temp_4.dataframe(
styled,
column_config={"logo": st.column_config.ImageColumn("logo")},
hide_index=True,
height=610,
width="content",
)
@@ -1156,7 +1359,7 @@ if isinstance(cached_scores_quarter, list) and len(cached_scores_quarter) >= 2:
if isinstance(cached_play_by_play, list) and isinstance(cached_game_online, dict):
plays = (cached_game_online.get("result") or {}).get("plays") or []
plays = cached_game_online.get("plays") or []
if plays:
tab_temp_6.table(cached_play_by_play)
@@ -1534,23 +1737,22 @@ with tab_online:
live_data_map = {}
# Собираем live-данные по каждому game.id
for _, row in df_filtered.iterrows():
game_id = row["game.id"]
try:
json_data = requests.get(
f"https://pro.russiabasket.org/api/abc/games/live-status?Id={game_id}&Lang=en",
f"https://vtb-league.org/api/abc/games/live-status?Id={game_id}&Lang=en",
).json()
except Exception as ex:
# json_data = {
# "period": None,
# "timeToGo": 0.0,
# }
print(ex)
# Берём содержимое result (словарь с gameId и данными)
result = json_data.get("result", {})
if result and "gameId" in result:
live_data_map[result["gameId"]] = result
if json_data:
result = json_data.get("result", {})
if result and "gameId" in result:
live_data_map[result["gameId"]] = result
# Создаём колонки для live-данных
df_filtered["live_period"] = None
@@ -1667,8 +1869,7 @@ try:
except (FileNotFoundError, json.JSONDecodeError):
play_type_id = []
teams_section = ((cached_game_online or {}).get("result") or {}).get("teams") or {}
teams_section = (cached_game_online or {}).get("teams") or {}
# Если teams_section — список (например, [{"starts": [...]}, {...}])
if isinstance(teams_section, list):
if len(teams_section) >= 2:
@@ -1681,6 +1882,7 @@ if isinstance(teams_section, list):
else:
starts1 = []
starts2 = []
# Если teams_section — словарь (обычно {"1": {...}, "2": {...}})
elif isinstance(teams_section, dict):
starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or []
@@ -1726,6 +1928,41 @@ def get_event_time(row):
return None
teams_section = (cached_game_online or {}).get("teams") or {}
# Если teams_section — список (например, [{"starts": [...]}, {...}])
if isinstance(teams_section, list):
if len(teams_section) >= 2:
starts1 = next(
(t.get("starts") for t in teams_section if t["teamNumber"] == 1), None
)
starts2 = next(
(t.get("starts") for t in teams_section if t["teamNumber"] == 2), None
)
else:
starts1 = []
starts2 = []
# Если teams_section — словарь (обычно {"1": {...}, "2": {...}})
elif isinstance(teams_section, dict):
starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or []
starts2 = (teams_section.get(2) or teams_section.get("2") or {}).get("starts") or []
else:
starts1 = []
starts2 = []
teams_temp = sorted(
[x for x in starts1 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0)
) + sorted(
[x for x in starts2 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0)
)
list_fullname = [None] + [
f"({x.get('displayNumber')}) {x.get('firstName','')} {x.get('lastName','')}".strip()
for x in teams_temp
if x.get("startRole") == "Player"
]
plays = plays if 'plays' in locals() else []
with tab_pbp:
plays = ((cached_game_online or {}).get("result") or {}).get("plays") or []
if plays:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,421 +0,0 @@
*** visual.py 2025-01-22 00:00:00.000000000 +0000
--- visual.py 2025-10-07 00:00:00.000000000 +0000
***************
*** 1,10 ****
import os
import json
import socket
import platform
import numpy as np
import pandas as pd
import streamlit as st
import sys
from streamlit_autorefresh import st_autorefresh
-
st.set_page_config(
page_title="Баскетбол",
page_icon="🏀",
layout="wide",
--- 1,10 ----
import os
import json
import socket
import platform
import numpy as np
import pandas as pd
import streamlit as st
import sys
from streamlit_autorefresh import st_autorefresh
+
st.set_page_config(
page_title="Баскетбол",
page_icon="🏀",
layout="wide",
***************
*** 164,169 ****
--- 164,216 ----
def ensure_state(key: str, default=None):
# Инициализирует ключ один раз и возвращает значение
return st.session_state.setdefault(key, default)
+
+ # ======== UNIVERSAL SAFE RENDER WRAPPER ========
+ def _is_empty_like(x) -> bool:
+ if x is None:
+ return True
+ # DataFrame
+ if isinstance(x, pd.DataFrame):
+ return x.empty
+ # Pandas Styler
+ try:
+ # импортируем лениво, чтобы не ломаться, если нет pandas.io.formats.style в рантайме
+ from pandas.io.formats.style import Styler # type: ignore
+ if isinstance(x, Styler):
+ # у Styler нет __len__, но есть .data
+ return getattr(x, "data", pd.DataFrame()).empty
+ except Exception:
+ pass
+ # пустые коллекции
+ if isinstance(x, (list, tuple, dict, set)):
+ return len(x) == 0
+ return False
+
+ def safe_show(func, *args, **kwargs):
+ """
+ Безопасно вызывает функции отображения Streamlit (dataframe, table, metric, image, markdown, ...).
+ - Ничего не рендерит, если основной аргумент данных пуст/None.
+ - Нормализует height (убирает None, <0; приводит к int).
+ - Возвращает результат вызова func (нужно для dataframe-selection).
+ - Перехватывает исключения и показывает предупреждение.
+ """
+ # Если среди позиционных/именованных аргументов есть пустые/None-данные — не показываем
+ for a in args:
+ if _is_empty_like(a):
+ return None
+ for k, v in kwargs.items():
+ # пропускаем не-данные параметры (типа width/unsafe_allow_html)
+ if k.lower() in ("height", "width", "use_container_width", "unsafe_allow_html", "on_select", "selection_mode", "column_config", "hide_index", "border", "delta_color", "key"):
+ continue
+ if _is_empty_like(v):
+ return None
+
+ # height -> валидный int, иначе уберём
+ if "height" in kwargs:
+ h = kwargs.get("height")
+ if h is None:
+ kwargs.pop("height")
+ else:
+ try:
+ h = int(h)
+ if h < 0:
+ kwargs.pop("height")
+ else:
+ kwargs["height"] = h
+ except Exception:
+ kwargs.pop("height")
+
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ st.warning(f"⚠️ Ошибка при отображении: {e}")
+ return None
+ # ======== /SAFE WRAPPER ========
+
***************
*** 221,231 ****
col1, col4, col2, col5, col3 = st.columns([1, 5, 3, 5, 1])
t1 = (result.get("team1") or {})
t2 = (result.get("team2") or {})
- if t1.get("logo"):
- col1.image(t1["logo"], width=100)
team1_name = t1.get("name") or ""
team2_name = t2.get("name") or ""
- if team1_name or team2_name:
- col2.markdown(
- f"<h2 style='text-align: center'>{team1_name} — {team2_name}</h2>",
- unsafe_allow_html=True,
- )
- if t2.get("logo"):
- col3.image(t2["logo"], width=100)
+ safe_show(col1.image, t1.get("logo"), width=100)
+ if team1_name or team2_name:
+ safe_show(
+ col2.markdown,
+ f"<h2 style='text-align: center'>{team1_name} — {team2_name}</h2>",
+ unsafe_allow_html=True,
+ )
+ safe_show(col3.image, t2.get("logo"), width=100)
col4_1, col4_2, col4_3 = col4.columns((1, 1, 1))
col5_1, col5_2, col5_3 = col5.columns((1, 1, 1))
***************
*** 237,253 ****
if isinstance(cached_team_stats, list) and len(cached_team_stats) > 0:
v1 = cached_team_stats[0].get("val1")
v2 = cached_team_stats[0].get("val2")
if v1 is not None and v2 is not None:
val1, val2 = int(v1), int(v2)
delta_color_1 = "off" if val1 == val2 else "normal"
- col4_1.metric("Points", v1, val1 - val2, delta_color_1)
- col5_3.metric("Points", v2, val2 - val1, delta_color_1)
+ safe_show(col4_1.metric, "Points", v1, val1 - val2, delta_color_1)
+ safe_show(col5_3.metric, "Points", v2, val2 - val1, delta_color_1)
- col4_3.metric("TimeOuts", len(timeout1))
- col5_1.metric("TimeOuts", len(timeout2))
+ safe_show(col4_3.metric, "TimeOuts", len(timeout1))
+ safe_show(col5_1.metric, "TimeOuts", len(timeout2))
if isinstance(cached_live_status, list) and cached_live_status:
foulsA = (cached_live_status[0] or {}).get("foulsA")
foulsB = (cached_live_status[0] or {}).get("foulsB")
if foulsA is not None:
- col4_2.metric("Fouls", foulsA)
+ safe_show(col4_2.metric, "Fouls", foulsA)
if foulsB is not None:
- col5_2.metric("Fouls", foulsB)
+ safe_show(col5_2.metric, "Fouls", foulsB)
***************
*** 270,280 ****
for q1, q2, col1_i, col2_i in zip(score_by_quarter_1, score_by_quarter_2, col_1_col, col_2_col):
count_q += 1
name_q = f"OT{count_q-4}" if count_q > 4 else f"Q{count_q}"
try:
delta_color = "off" if int(q1) == int(q2) else "normal"
- col1_i.metric(name_q, q1, int(q1) - int(q2), delta_color, border=True)
- col2_i.metric(name_q, q2, int(q2) - int(q1), delta_color, border=True)
+ safe_show(col1_i.metric, name_q, q1, int(q1) - int(q2), delta_color, border=True)
+ safe_show(col2_i.metric, name_q, q2, int(q2) - int(q1), delta_color, border=True)
except (ValueError, TypeError):
# если кривые данные в JSON, просто пропустим
pass
***************
*** 403,424 ****
team1_styled = (
team1_data.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
)
team2_styled = (
team2_data.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
)
# Вывод данных
col_player1, col_player2 = tab_temp_1.columns((5, 5))
- event1 = col_player1.dataframe(
- team1_styled,
- column_config=config,
- hide_index=True,
- height=460,
- on_select="rerun",
- selection_mode=[
- "single-row",
- ],
- )
- event2 = col_player2.dataframe(
- team2_styled,
- column_config=config,
- hide_index=True,
- height=460,
- on_select="rerun",
- selection_mode=[
- "single-row",
- ],
- )
+ event1 = safe_show(
+ col_player1.dataframe,
+ team1_styled,
+ column_config=config,
+ hide_index=True,
+ height=460,
+ on_select="rerun",
+ selection_mode=["single-row"],
+ )
+ event2 = safe_show(
+ col_player2.dataframe,
+ team2_styled,
+ column_config=config,
+ hide_index=True,
+ height=460,
+ on_select="rerun",
+ selection_mode=["single-row"],
+ )
if event1 and getattr(event1, "selection", None) and event1.selection.get("rows"):
selected_index1 = event1.selection["rows"][0]
st.session_state["player1"] = (
selected_index1 # Сохранение состояния в session_state
***************
*** 433,441 ****
if player_data_1["num"]:
z, a, b, c, d, e = col_player1.columns((1, 6, 1, 1, 1, 1))
- z.metric("Номер", player_data_1["num"], border=False)
- a.metric("Игрок", player_data_1["NameGFX"], border=False)
- b.metric("Амплуа", player_data_1["roleShort"], border=False)
- c.metric("Возраст", player_data_1["age"], border=False)
- d.metric("Рост", player_data_1["height"].split()[0], border=False)
- e.metric("Вес", player_data_1["weight"].split()[0], border=False)
-
- col_player1.dataframe(
- selected_player_1,
- column_config=config_season,
- hide_index=True,
- )
+ safe_show(z.metric, "Номер", player_data_1["num"], border=False)
+ safe_show(a.metric, "Игрок", player_data_1["NameGFX"], border=False)
+ safe_show(b.metric, "Амплуа", player_data_1["roleShort"], border=False)
+ safe_show(c.metric, "Возраст", player_data_1["age"], border=False)
+ safe_show(d.metric, "Рост", player_data_1["height"].split()[0], border=False)
+ safe_show(e.metric, "Вес", player_data_1["weight"].split()[0], border=False)
+
+ safe_show(
+ col_player1.dataframe,
+ selected_player_1,
+ column_config=config_season,
+ hide_index=True,
+ )
***************
*** 446,454 ****
if player_data_2["num"]:
z, a, b, c, d, e = col_player2.columns((1, 6, 1, 1, 1, 1))
- z.metric("Номер", player_data_2["num"], border=False)
- a.metric("Игрок", player_data_2["NameGFX"], border=False)
- b.metric("Амплуа", player_data_2["roleShort"], border=False)
- c.metric("Возраст", player_data_2["age"], border=False)
- d.metric("Рост", player_data_2["height"].split()[0], border=False)
- e.metric("Вес", player_data_2["weight"].split()[0], border=False)
-
- col_player2.dataframe(
- selected_player_2,
- column_config=config_season,
- hide_index=True,
- )
+ safe_show(z.metric, "Номер", player_data_2["num"], border=False)
+ safe_show(a.metric, "Игрок", player_data_2["NameGFX"], border=False)
+ safe_show(b.metric, "Амплуа", player_data_2["roleShort"], border=False)
+ safe_show(c.metric, "Возраст", player_data_2["age"], border=False)
+ safe_show(d.metric, "Рост", player_data_2["height"].split()[0], border=False)
+ safe_show(e.metric, "Вес", player_data_2["weight"].split()[0], border=False)
+
+ safe_show(
+ col_player2.dataframe,
+ selected_player_2,
+ column_config=config_season,
+ hide_index=True,
+ )
***************
*** 459,468 ****
if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34:
cached_team_stats_new = [
cached_team_stats[0],
*cached_team_stats[25:29],
cached_team_stats[7],
cached_team_stats[33],
*cached_team_stats[9:11],
*cached_team_stats[15:17],
]
- tab_temp_2.table(cached_team_stats_new)
+ safe_show(tab_temp_2.table, cached_team_stats_new)
***************
*** 470,484 ****
- if isinstance(cached_referee, (list, pd.DataFrame)):
- tab_temp_3.dataframe(cached_referee, height=600, column_config={"flag": st.column_config.ImageColumn("flag")})
-
-
column_config_ref = {
"flag": st.column_config.ImageColumn(
"flag",
),
}
if cached_referee:
- tab_temp_3.dataframe(cached_referee, height=600, column_config=column_config_ref)
+ safe_show(tab_temp_3.dataframe, cached_referee, height=600, column_config=column_config_ref)
***************
*** 503,511 ****
styled = df_st.style.apply(highlight_teams, axis=1)
- tab_temp_4.dataframe(
- styled,
- column_config={"logo": st.column_config.ImageColumn("logo")},
- hide_index=True,
- height=610,
- )
+ safe_show(
+ tab_temp_4.dataframe,
+ styled,
+ column_config={"logo": st.column_config.ImageColumn("logo")},
+ hide_index=True,
+ height=610,
+ )
***************
*** 552,558 ****
]
col.write(q)
- col.dataframe(df_col)
+ safe_show(col.dataframe, df_col)
# Овертаймы
for index, col in enumerate(columns_quarters):
q = columns_quarters_name_ot[index]
df_col = [
--- 564,570 ----
***************
*** 572,578 ****
]
col.write(q)
- col.dataframe(df_col)
+ safe_show(col.dataframe, df_col)
***************
*** 582,586 ****
if isinstance(cached_play_by_play, list) and isinstance(cached_game_online, dict):
plays = (cached_game_online.get("result") or {}).get("plays") or []
if plays:
- tab_temp_6.table(cached_play_by_play)
+ safe_show(tab_temp_6.table, cached_play_by_play)
***************
*** 703,724 ****
height1 = 38 * max(count_game_1, 10)
height2 = 38 * max(count_game_2, 10)
- col1_schedule.dataframe(
- team1_data,
- hide_index=True,
- height=int(min(height1, 1200)),
- column_config=column_config,
- )
- col2_schedule.dataframe(
- team2_data,
- hide_index=True,
- height=int(min(height2, 1200)),
- column_config=column_config,
- )
+ safe_show(
+ col1_schedule.dataframe,
+ team1_data,
+ hide_index=True,
+ height=int(min(height1, 1200)),
+ column_config=column_config,
+ )
+ safe_show(
+ col2_schedule.dataframe,
+ team2_data,
+ hide_index=True,
+ height=int(min(height2, 1200)),
+ column_config=column_config,
+ )
***************
*** 836,845 ****
filtered_data_pbp = temp_data_pbp[mask1]
count_pbp = len(filtered_data_pbp)
column_pbp = ["num", "info", "who", "period", "time"]
column_config_pbp = {
"info": st.column_config.TextColumn(width="medium"),
"who": st.column_config.TextColumn(width="large"),
}
- col2_pbp.dataframe(
- filtered_data_pbp[column_pbp],
- column_config=column_config_pbp,
- hide_index=True,
- height=(38 * count_pbp if count_pbp > 10 else None),
- )
+ safe_show(
+ col2_pbp.dataframe,
+ filtered_data_pbp[column_pbp],
+ column_config=column_config_pbp,
+ hide_index=True,
+ height=(38 * count_pbp if count_pbp > 10 else None),
+ )
else:
st.info("Данных play-by-play нет.")

Binary file not shown.

View File

@@ -1,79 +0,0 @@
import os
import shutil
import argparse
import xml.etree.ElementTree as ET
from urllib.parse import urlparse, unquote
def localname(tag: str) -> str:
return tag.split('}', 1)[-1].lower()
def extract_filename(u: str) -> str:
s = (u or "").strip()
if not s:
return ""
p = urlparse(s)
path = p.path if p.scheme else s
path = path.replace("\\", "/")
name = path.split("/")[-1]
return unquote(name)
def replace_urls_in_vmix(xml_path: str, prefix_url: str) -> int:
if not os.path.isfile(xml_path):
raise FileNotFoundError(f"Файл не найден: {xml_path}")
backup_path = xml_path + ".bak"
shutil.copy2(xml_path, backup_path)
tree = ET.parse(xml_path)
root = tree.getroot()
replaced = 0
for node in root.iter():
if localname(node.tag).startswith("datasource") and node.get("friendlyName") == "JSON":
for sub in node.iter():
if localname(sub.tag) == "url":
old = (sub.text or "").strip()
# ✅ проверка: если уже новый URL — пропускаем
if old.startswith(prefix_url):
print(f"Пропускаем — уже новый URL: {old}")
continue
tail = extract_filename(old)
if not tail:
continue
new_full = prefix_url + tail
if old != new_full:
print(f"{old} -> {new_full}")
sub.text = new_full
replaced += 1
# сохраняем без XML декларации
tree.write(xml_path, encoding="utf-8", xml_declaration=False)
return replaced
def main():
parser = argparse.ArgumentParser(
description="Заменяет <url> в datasource[friendlyName=JSON] на PREFIX + имя файла."
)
parser.add_argument("--file", "-f", required=True, help="Путь к .vmix файлу")
parser.add_argument("--url", "-u", required=False, help="Префикс нового URL (например, https://gfx.tvstart.ru/app/static/gfx_)")
parser.add_argument("--pause", action="store_true", help="Ожидать Enter в конце (удобно при запуске двойным кликом)")
args = parser.parse_args()
prefix = args.url or input("Введите префикс нового URL (например, https://gfx.tvstart.ru/app/static/gfx_): ").strip()
if not prefix:
print("Префикс пустой — ничего не сделано.")
return
try:
count = replace_urls_in_vmix(args.file, prefix)
print(f"Готово. Заменено тегов <url>: {count}")
print(f"Резервная копия: {args.file}.bak")
except Exception as e:
print("Ошибка:", e)
if args.pause:
input("Нажмите Enter для выхода...")
if __name__ == "__main__":
main()