11 Commits

Author SHA1 Message Date
fde16de0c6 License update 2026-05-06 14:51:22 +03:00
418f12ca4a Финальное исправление иконок в оформлении 2026-05-06 14:21:09 +03:00
7001aeb070 Иправления 2026-05-06 12:58:29 +03:00
b793571681 Исправленны иконки 2026-05-06 12:47:46 +03:00
74412ad368 Не большие изменения 2026-05-06 12:31:42 +03:00
855bdcc3f9 небольшие правки 2026-04-18 11:21:11 +03:00
4646dd2b70 fix: исправлены роуты веб-сервера и обновлён Makefile
- Убран дублирующийся роут /favicon.ico
- Добавлен импорт ServeDir
- Makefile: поддержка ImageMagick 6 и 7
- Добавлена цель favicon
2026-04-18 11:12:54 +03:00
6a642beb5c 0.2.6 2026-04-17 20:06:58 +03:00
9b3d959ce3 feat: сортировка заданий по дате и времени, последовательная отправка
- Добавлена сортировка заданий по дате и времени события (самые ранние — первыми)
- Добавлена сортировка по варианту (оригинал -> today -> tomorrow)
- Изменена отправка на последовательную для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2+ дефиса -> 1)
- Исправлен SINGLE шаблон (убран слой TEAMS)
- Добавлена поддержка cache: true/false для ассетов
- Логотипы команд: cache: false (часто меняются)
- Логотип канала: cache: true (редко меняется)
- Видео-пак: cache: true (редко меняется)
- Версия 0.2.6
2026-04-17 20:00:06 +03:00
e06b55ad07 Оптимизирована работа с памятью 2026-04-17 19:14:38 +03:00
c3056010eb v0.2.4 2026-04-17 18:33:40 +03:00
30 changed files with 547 additions and 270 deletions

21
Cargo.lock generated
View File

@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "ae_anons" name = "ae_anons"
version = "0.2.4" version = "0.2.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"askama", "askama",
@@ -33,6 +33,7 @@ dependencies = [
"tower", "tower",
"tower-http", "tower-http",
"urlencoding", "urlencoding",
"winres",
] ]
[[package]] [[package]]
@@ -1768,6 +1769,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.3" version = "0.5.3"
@@ -2167,6 +2177,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winres"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
dependencies = [
"toml",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ae_anons" name = "ae_anons"
version = "0.2.4" version = "0.2.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Alexey Barabanov <a.barabanov@tvstart.ru>"] authors = ["Alexey Barabanov <a.barabanov@tvstart.ru>"]
@@ -48,3 +48,6 @@ opt-level = "z"
lto = true lto = true
codegen-units = 1 codegen-units = 1
strip = true strip = true
[build-dependencies]
winres = "0.1"

View File

@@ -1,7 +1,5 @@
# MIT License # MIT License
Copyright (c) 2026 [Your Name or Company]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View File

@@ -12,10 +12,15 @@
Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office. Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office.
## Новое в версии v0.2.4 ## Новое в версии v0.2.6
- Групповая отправка заданий по 3 (оригинал + today + tomorrow) для сохранения порядка в очереди - Сортировка заданий по дате и времени события (самые ранние — первыми)
- Улучшена читаемость логов отправки (добавлен UID) - Сортировка по варианту (оригинал → today → tomorrow)
- Последовательная отправка для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Добавлена поддержка кеширования ассетов (cache: true/false)
- Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
## Обзор ## Обзор
@@ -610,54 +615,64 @@ ae_anons/
## История изменений ## История изменений
### v0.2.4 (текущая) ### v0.2.6 (текущая)
- Групповая отправка заданий по 3 (оригинал + today + tomorrow) для сохранения порядка в очереди - Сортировка заданий по дате и времени события (самые ранние — первыми)
- Улучшена читаемость логов отправки (добавлен UID) - Сортировка по варианту (оригинал → today → tomorrow)
- Последовательная отправка для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Добавлена поддержка кеширования ассетов (cache: true/false)
- Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
### v0.2.5
- Оптимизация памяти: очистка неиспользуемых логотипов
- Групповая отправка заданий по 3 для сохранения порядка
- Добавлен UID в логи отправки
### v0.2.4
- Полностью автономный режим (встроенный Font Awesome)
- Добавлен favicon
- Все статические ресурсы встроены в бинарник
### v0.2.3 ### v0.2.3
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Веб-интерфейс с настраиваемым портом (WEB_PORT)
- Тёмная/светлая тема с автоопределением ОС
- Сортировка по всем колонкам в интерфейсе
- Фильтрация по имени файла/UID
- Остановка всех активных заданий
- Автообновление каждые 60 секунд
- Улучшенное debug-логирование - Улучшенное debug-логирование
- Исправлена обработка UNC/SMB путей - Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
### v0.2.2 ### v0.2.2
- Исправлено отображение имён выходных файлов в веб-интерфейсе - Исправлено отображение имён выходных файлов в веб-интерфейсе
- Улучшена цветовая схема - Улучшена цветовая схема
- Добавлена сортировка по всем колонкам - Добавлена сортировка по всем колонкам
- Вынесены стили в отдельный CSS файл - Вынесены стили в отдельный CSS файл
### v0.2.0 ### v0.2.0
- Добавлен веб-интерфейс для управления заданиями - Добавлен веб-интерфейс для управления заданиями
- Реализован REST API - Реализован REST API
- Добавлена поддержка тёмной/светлой темы - Добавлена поддержка тёмной/светлой темы
- Автообновление статуса заданий - Автообновление статуса заданий
- Возможность остановки всех активных заданий - Возможность остановки всех активных заданий
### v0.1.1 ### v0.1.1
- Оптимизирована работа с памятью - Оптимизирована работа с памятью
- Убрана функция создания .json - Убрана функция создания .json
### v0.1.0 ### v0.1.0
- Первый выпуск - Первый выпуск
- Интеграция с Synology Office - Интеграция с Synology Office
- Базовая генерация заданий Nexrender - Базовая генерация заданий Nexrender
- Парсинг Excel с динамическим обнаружением листов - Парсинг Excel с динамическим обнаружением листов
--- ---

BIN
assets/logo.icns Normal file

Binary file not shown.

BIN
assets/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

11
build.rs Normal file
View File

@@ -0,0 +1,11 @@
fn main() {
#[cfg(windows)]
{
if let Err(e) = winres::WindowsResource::new()
.set_icon("assets/logo.ico")
.compile()
{
eprintln!("warning: не удалось встроить иконку: {e}");
}
}
}

View File

@@ -1,4 +1,4 @@
.PHONY: all clean build-mac build-windows build-linux build-linux-musl package help quick package-single .PHONY: all clean build-mac build-windows build-linux build-linux-musl package help quick package-single icons
# Название проекта # Название проекта
PROJECT_NAME := ae_anons PROJECT_NAME := ae_anons
@@ -18,11 +18,14 @@ BLUE := \033[0;34m
CYAN := \033[0;36m CYAN := \033[0;36m
MAGENTA := \033[0;35m MAGENTA := \033[0;35m
BOLD := \033[1m BOLD := \033[1m
NC := \033[0m # No Color NC := \033[0m
# Файл для хранения времени сборки # Файл для хранения времени сборки
TIMING_FILE := .build_timing TIMING_FILE := .build_timing
# Определение команды ImageMagick (v6 = convert, v7 = magick)
IMAGEMAGICK := $(shell command -v magick 2>/dev/null || command -v convert 2>/dev/null || echo "false")
help: help:
@echo "$(BOLD)$(CYAN)AE Anons - Makefile команды$(NC)" @echo "$(BOLD)$(CYAN)AE Anons - Makefile команды$(NC)"
@echo "" @echo ""
@@ -41,6 +44,10 @@ help:
@echo " make package - Создать пакеты для всех платформ" @echo " make package - Создать пакеты для всех платформ"
@echo " make package-single - Создать пакет для текущей платформы" @echo " make package-single - Создать пакет для текущей платформы"
@echo "" @echo ""
@echo "$(GREEN)Подготовка ресурсов:$(NC)"
@echo " make setup - Подготовить иконки (логотипы, favicon)"
@echo " make setup-fontawesome-manual - Скачать и исправить all.min.css (если удалён)"
@echo ""
@echo "$(GREEN)Очистка:$(NC)" @echo "$(GREEN)Очистка:$(NC)"
@echo " make clean - Очистить все сборки" @echo " make clean - Очистить все сборки"
@echo " make clean-timing - Очистить файл с временами сборки" @echo " make clean-timing - Очистить файл с временами сборки"
@@ -49,24 +56,27 @@ help:
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ============================================ # ============================================
# Копирование общих файлов в пакет
define copy_common_files define copy_common_files
@cp .env.example $(1)/ 2>/dev/null || true @cp .env.example $(1)/ 2>/dev/null || true
@cp README.md $(1)/ 2>/dev/null || true @cp README.md $(1)/ 2>/dev/null || true
@if [ -f LICENSE ]; then cp LICENSE $(1)/; else echo "$(YELLOW)⚠️ LICENSE не найден, пропускаем$(NC)"; fi @if [ -f LICENSE ]; then cp LICENSE $(1)/; else echo "$(YELLOW)⚠️ LICENSE не найден, пропускаем$(NC)"; fi
endef endef
# Функция для замера времени
define measure_time define measure_time
@start=$$(date +%s); \ @start=$$(date +%s); \
$(2); \ $(2); \
end=$$(date +%s); \ end=$$(date +%s); \
duration=$$((end - start)); \ duration=$$((end - start)); \
echo "$(1): $$duration сек" >> $(TIMING_FILE); \ echo "$(1): $$duration сек" >> $(TIMING_FILE); \
echo "$(GREEN)$(1) завершён за $$duration сек$(NC)" if [ $$duration -ge 60 ]; then \
min=$$((duration / 60)); \
sec=$$((duration % 60)); \
echo "$(GREEN)$(1) завершён за $${min}m $${sec}s$(NC)"; \
else \
echo "$(GREEN)$(1) завершён за $${duration}s$(NC)"; \
fi
endef endef
# Функция для вывода итоговой таблицы
define print_timing_summary define print_timing_summary
@echo "" @echo ""
@echo "$(BOLD)$(CYAN)═══════════════════════════════════════════════════════════════$(NC)" @echo "$(BOLD)$(CYAN)═══════════════════════════════════════════════════════════════$(NC)"
@@ -88,6 +98,58 @@ define print_timing_summary
@rm -f $(TIMING_FILE) @rm -f $(TIMING_FILE)
endef endef
# ============================================
# ПОДГОТОВКА РЕСУРСОВ
# ============================================
setup: icons
@echo "$(GREEN)Все статические ресурсы подготовлены$(NC)"
# Генерация иконок (favicon, .ico, .icns) из логотипа
icons:
@echo "$(GREEN)🎨 Генерация иконок...$(NC)"
@if [ "$(IMAGEMAGICK)" = "false" ]; then \
echo "$(RED)❌ ImageMagick не установлен. Установите: brew install imagemagick$(NC)"; \
exit 1; \
fi
# favicon.ico
@$(IMAGEMAGICK) assets/logo.png -define icon:auto-resize=48,32,16 src/static/favicon.ico
@echo "$(GREEN) ✓ src/static/favicon.ico$(NC)"
# Windows .ico
@$(IMAGEMAGICK) assets/logo.png -define icon:auto-resize=256,128,64,48,32,16 assets/logo.ico
@echo "$(GREEN) ✓ assets/logo.ico$(NC)"
# macOS .icns
@mkdir -p assets/icon.iconset
@sips -z 16 16 assets/logo.png --out assets/icon.iconset/icon_16x16.png 2>/dev/null || true
@sips -z 32 32 assets/logo.png --out assets/icon.iconset/icon_16x16@2x.png 2>/dev/null || true
@sips -z 32 32 assets/logo.png --out assets/icon.iconset/icon_32x32.png 2>/dev/null || true
@sips -z 64 64 assets/logo.png --out assets/icon.iconset/icon_32x32@2x.png 2>/dev/null || true
@sips -z 128 128 assets/logo.png --out assets/icon.iconset/icon_128x128.png 2>/dev/null || true
@sips -z 256 256 assets/logo.png --out assets/icon.iconset/icon_128x128@2x.png 2>/dev/null || true
@sips -z 256 256 assets/logo.png --out assets/icon.iconset/icon_256x256.png 2>/dev/null || true
@sips -z 512 512 assets/logo.png --out assets/icon.iconset/icon_256x256@2x.png 2>/dev/null || true
@sips -z 512 512 assets/logo.png --out assets/icon.iconset/icon_512x512.png 2>/dev/null || true
@iconutil -c icns assets/icon.iconset -o assets/logo.icns 2>/dev/null || true
@rm -rf assets/icon.iconset
@echo "$(GREEN) ✓ assets/logo.icns$(NC)"
# Восстановить CSS Font Awesome вручную (если файл удалён)
setup-fontawesome-manual:
@echo "$(GREEN)📥 Скачивание Font Awesome CSS...$(NC)"
@curl -sL -o src/static/fontawesome/all.min.css $(FONTAWESOME_CSS_URL)
@sed -i '' -E 's|(\.\./)*webfonts/|/static/fontawesome/webfonts/|g' \
src/static/fontawesome/all.min.css
@echo "$(GREEN)✓ CSS восстановлен и пути исправлены$(NC)"
# ============================================
# ИКОНКА ДЛЯ WINDOWS .EXE
# ============================================
windows-icon:
@if [ ! -f assets/logo.ico ]; then \
echo "$(YELLOW)logo.ico не найден, запускаю генерацию иконок...$(NC)"; \
$(MAKE) icons; \
fi
# ============================================ # ============================================
# ОПРЕДЕЛЕНИЕ ПЛАТФОРМЫ # ОПРЕДЕЛЕНИЕ ПЛАТФОРМЫ
# ============================================ # ============================================
@@ -113,7 +175,6 @@ endif
quick: detect-platform quick: detect-platform
# Быстрая сборка без упаковки (только бинарник)
quick-release: quick-release:
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
@echo "$(GREEN)🍎 Сборка для macOS...$(NC)" @echo "$(GREEN)🍎 Сборка для macOS...$(NC)"
@@ -147,7 +208,7 @@ quick-linux:
@echo "$(GREEN)✅ Пакет: $(BINARIES_DIR)/$(PROJECT_NAME)-v$(VERSION)-linux-$(UNAME_M)/$(NC)" @echo "$(GREEN)✅ Пакет: $(BINARIES_DIR)/$(PROJECT_NAME)-v$(VERSION)-linux-$(UNAME_M)/$(NC)"
@$(call print_timing_summary) @$(call print_timing_summary)
quick-windows: quick-windows: windows-icon
@rm -f $(TIMING_FILE) @rm -f $(TIMING_FILE)
@if command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then \ @if command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then \
$(call measure_time,"🪟 Windows x64",cargo build --target x86_64-pc-windows-gnu --release); \ $(call measure_time,"🪟 Windows x64",cargo build --target x86_64-pc-windows-gnu --release); \
@@ -170,7 +231,7 @@ build-mac:
@echo "$(GREEN)🍎 Сборка для macOS...$(NC)" @echo "$(GREEN)🍎 Сборка для macOS...$(NC)"
@time cargo build --release @time cargo build --release
build-windows: build-windows: windows-icon
@echo "$(GREEN)🪟 Сборка для Windows x86_64...$(NC)" @echo "$(GREEN)🪟 Сборка для Windows x86_64...$(NC)"
@time cargo build --target x86_64-pc-windows-gnu --release @time cargo build --target x86_64-pc-windows-gnu --release

View File

@@ -1,7 +1,5 @@
# MIT License # MIT License
Copyright (c) 2026 [Your Name or Company]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View File

@@ -12,17 +12,15 @@
Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office. Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office.
## Новое в версии 0.2.3 ## Новое в версии v0.2.6
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Сортировка заданий по дате и времени события (самые ранние — первыми)
- Тёмная/светлая тема с автоопределением ОС - Сортировка по варианту (оригинал → today → tomorrow)
- Сортировка по всем колонкам - Последовательная отправка для гарантированного порядка в очереди
- Фильтрация по имени файла/UID - Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Остановка всех активных заданий - Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Автообновление каждые 60 секунд - Добавлена поддержка кеширования ассетов (cache: true/false)
- Улучшенное debug-логирование - Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
- Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
## Обзор ## Обзор
@@ -576,6 +574,7 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender ### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
@@ -587,7 +586,7 @@ ae_anons/
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL) - **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. — https://fontawesome.com © 2023 Fonticons, Inc. — <https://fontawesome.com>
## Лицензия ## Лицензия
@@ -616,49 +615,64 @@ ae_anons/
## История изменений ## История изменений
### v0.2.3 (текущая) ### v0.2.6 (текущая)
- Сортировка заданий по дате и времени события (самые ранние — первыми)
- Сортировка по варианту (оригинал → today → tomorrow)
- Последовательная отправка для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Добавлена поддержка кеширования ассетов (cache: true/false)
- Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
### v0.2.5
- Оптимизация памяти: очистка неиспользуемых логотипов
- Групповая отправка заданий по 3 для сохранения порядка
- Добавлен UID в логи отправки
### v0.2.4
- Полностью автономный режим (встроенный Font Awesome)
- Добавлен favicon
- Все статические ресурсы встроены в бинарник
### v0.2.3
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Веб-интерфейс с настраиваемым портом (WEB_PORT)
- Тёмная/светлая тема с автоопределением ОС
- Сортировка по всем колонкам в интерфейсе
- Фильтрация по имени файла/UID
- Остановка всех активных заданий
- Автообновление каждые 60 секунд
- Улучшенное debug-логирование - Улучшенное debug-логирование
- Исправлена обработка UNC/SMB путей - Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
### v0.2.2 ### v0.2.2
- Исправлено отображение имён выходных файлов в веб-интерфейсе - Исправлено отображение имён выходных файлов в веб-интерфейсе
- Улучшена цветовая схема - Улучшена цветовая схема
- Добавлена сортировка по всем колонкам - Добавлена сортировка по всем колонкам
- Вынесены стили в отдельный CSS файл - Вынесены стили в отдельный CSS файл
### v0.2.0 ### v0.2.0
- Добавлен веб-интерфейс для управления заданиями - Добавлен веб-интерфейс для управления заданиями
- Реализован REST API - Реализован REST API
- Добавлена поддержка тёмной/светлой темы - Добавлена поддержка тёмной/светлой темы
- Автообновление статуса заданий - Автообновление статуса заданий
- Возможность остановки всех активных заданий - Возможность остановки всех активных заданий
### v0.1.1 ### v0.1.1
- Оптимизирована работа с памятью - Оптимизирована работа с памятью
- Убрана функция создания .json - Убрана функция создания .json
### v0.1.0 ### v0.1.0
- Первый выпуск - Первый выпуск
- Интеграция с Synology Office - Интеграция с Synology Office
- Базовая генерация заданий Nexrender - Базовая генерация заданий Nexrender
- Парсинг Excel с динамическим обнаружением листов - Парсинг Excel с динамическим обнаружением листов
--- ---

View File

@@ -1,7 +1,5 @@
# MIT License # MIT License
Copyright (c) 2026 [Your Name or Company]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View File

@@ -12,17 +12,15 @@
Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office. Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office.
## Новое в версии 0.2.3 ## Новое в версии v0.2.6
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Сортировка заданий по дате и времени события (самые ранние — первыми)
- Тёмная/светлая тема с автоопределением ОС - Сортировка по варианту (оригинал → today → tomorrow)
- Сортировка по всем колонкам - Последовательная отправка для гарантированного порядка в очереди
- Фильтрация по имени файла/UID - Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Остановка всех активных заданий - Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Автообновление каждые 60 секунд - Добавлена поддержка кеширования ассетов (cache: true/false)
- Улучшенное debug-логирование - Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
- Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
## Обзор ## Обзор
@@ -576,6 +574,7 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender ### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
@@ -587,7 +586,7 @@ ae_anons/
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL) - **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. — https://fontawesome.com © 2023 Fonticons, Inc. — <https://fontawesome.com>
## Лицензия ## Лицензия
@@ -616,49 +615,64 @@ ae_anons/
## История изменений ## История изменений
### v0.2.3 (текущая) ### v0.2.6 (текущая)
- Сортировка заданий по дате и времени события (самые ранние — первыми)
- Сортировка по варианту (оригинал → today → tomorrow)
- Последовательная отправка для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Добавлена поддержка кеширования ассетов (cache: true/false)
- Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
### v0.2.5
- Оптимизация памяти: очистка неиспользуемых логотипов
- Групповая отправка заданий по 3 для сохранения порядка
- Добавлен UID в логи отправки
### v0.2.4
- Полностью автономный режим (встроенный Font Awesome)
- Добавлен favicon
- Все статические ресурсы встроены в бинарник
### v0.2.3
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Веб-интерфейс с настраиваемым портом (WEB_PORT)
- Тёмная/светлая тема с автоопределением ОС
- Сортировка по всем колонкам в интерфейсе
- Фильтрация по имени файла/UID
- Остановка всех активных заданий
- Автообновление каждые 60 секунд
- Улучшенное debug-логирование - Улучшенное debug-логирование
- Исправлена обработка UNC/SMB путей - Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
### v0.2.2 ### v0.2.2
- Исправлено отображение имён выходных файлов в веб-интерфейсе - Исправлено отображение имён выходных файлов в веб-интерфейсе
- Улучшена цветовая схема - Улучшена цветовая схема
- Добавлена сортировка по всем колонкам - Добавлена сортировка по всем колонкам
- Вынесены стили в отдельный CSS файл - Вынесены стили в отдельный CSS файл
### v0.2.0 ### v0.2.0
- Добавлен веб-интерфейс для управления заданиями - Добавлен веб-интерфейс для управления заданиями
- Реализован REST API - Реализован REST API
- Добавлена поддержка тёмной/светлой темы - Добавлена поддержка тёмной/светлой темы
- Автообновление статуса заданий - Автообновление статуса заданий
- Возможность остановки всех активных заданий - Возможность остановки всех активных заданий
### v0.1.1 ### v0.1.1
- Оптимизирована работа с памятью - Оптимизирована работа с памятью
- Убрана функция создания .json - Убрана функция создания .json
### v0.1.0 ### v0.1.0
- Первый выпуск - Первый выпуск
- Интеграция с Synology Office - Интеграция с Synology Office
- Базовая генерация заданий Nexrender - Базовая генерация заданий Nexrender
- Парсинг Excel с динамическим обнаружением листов - Парсинг Excel с динамическим обнаружением листов
--- ---

View File

@@ -1,7 +1,5 @@
# MIT License # MIT License
Copyright (c) 2026 [Your Name or Company]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View File

@@ -12,17 +12,15 @@
Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office. Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office.
## Новое в версии 0.2.3 ## Новое в версии v0.2.6
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Сортировка заданий по дате и времени события (самые ранние — первыми)
- Тёмная/светлая тема с автоопределением ОС - Сортировка по варианту (оригинал → today → tomorrow)
- Сортировка по всем колонкам - Последовательная отправка для гарантированного порядка в очереди
- Фильтрация по имени файла/UID - Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Остановка всех активных заданий - Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Автообновление каждые 60 секунд - Добавлена поддержка кеширования ассетов (cache: true/false)
- Улучшенное debug-логирование - Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
- Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
## Обзор ## Обзор
@@ -576,6 +574,7 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender ### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
@@ -587,7 +586,7 @@ ae_anons/
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL) - **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. — https://fontawesome.com © 2023 Fonticons, Inc. — <https://fontawesome.com>
## Лицензия ## Лицензия
@@ -616,49 +615,64 @@ ae_anons/
## История изменений ## История изменений
### v0.2.3 (текущая) ### v0.2.6 (текущая)
- Сортировка заданий по дате и времени события (самые ранние — первыми)
- Сортировка по варианту (оригинал → today → tomorrow)
- Последовательная отправка для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Добавлена поддержка кеширования ассетов (cache: true/false)
- Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
### v0.2.5
- Оптимизация памяти: очистка неиспользуемых логотипов
- Групповая отправка заданий по 3 для сохранения порядка
- Добавлен UID в логи отправки
### v0.2.4
- Полностью автономный режим (встроенный Font Awesome)
- Добавлен favicon
- Все статические ресурсы встроены в бинарник
### v0.2.3
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Веб-интерфейс с настраиваемым портом (WEB_PORT)
- Тёмная/светлая тема с автоопределением ОС
- Сортировка по всем колонкам в интерфейсе
- Фильтрация по имени файла/UID
- Остановка всех активных заданий
- Автообновление каждые 60 секунд
- Улучшенное debug-логирование - Улучшенное debug-логирование
- Исправлена обработка UNC/SMB путей - Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
### v0.2.2 ### v0.2.2
- Исправлено отображение имён выходных файлов в веб-интерфейсе - Исправлено отображение имён выходных файлов в веб-интерфейсе
- Улучшена цветовая схема - Улучшена цветовая схема
- Добавлена сортировка по всем колонкам - Добавлена сортировка по всем колонкам
- Вынесены стили в отдельный CSS файл - Вынесены стили в отдельный CSS файл
### v0.2.0 ### v0.2.0
- Добавлен веб-интерфейс для управления заданиями - Добавлен веб-интерфейс для управления заданиями
- Реализован REST API - Реализован REST API
- Добавлена поддержка тёмной/светлой темы - Добавлена поддержка тёмной/светлой темы
- Автообновление статуса заданий - Автообновление статуса заданий
- Возможность остановки всех активных заданий - Возможность остановки всех активных заданий
### v0.1.1 ### v0.1.1
- Оптимизирована работа с памятью - Оптимизирована работа с памятью
- Убрана функция создания .json - Убрана функция создания .json
### v0.1.0 ### v0.1.0
- Первый выпуск - Первый выпуск
- Интеграция с Synology Office - Интеграция с Synology Office
- Базовая генерация заданий Nexrender - Базовая генерация заданий Nexrender
- Парсинг Excel с динамическим обнаружением листов - Парсинг Excel с динамическим обнаружением листов
--- ---

View File

@@ -1,7 +1,5 @@
# MIT License # MIT License
Copyright (c) 2026 [Your Name or Company]
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View File

@@ -12,17 +12,15 @@
Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office. Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office.
## Новое в версии 0.2.3 ## Новое в версии v0.2.6
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Сортировка заданий по дате и времени события (самые ранние — первыми)
- Тёмная/светлая тема с автоопределением ОС - Сортировка по варианту (оригинал → today → tomorrow)
- Сортировка по всем колонкам - Последовательная отправка для гарантированного порядка в очереди
- Фильтрация по имени файла/UID - Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Остановка всех активных заданий - Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Автообновление каждые 60 секунд - Добавлена поддержка кеширования ассетов (cache: true/false)
- Улучшенное debug-логирование - Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
- Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
## Обзор ## Обзор
@@ -576,6 +574,7 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender ### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
@@ -587,7 +586,7 @@ ae_anons/
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL) - **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. — https://fontawesome.com © 2023 Fonticons, Inc. — <https://fontawesome.com>
## Лицензия ## Лицензия
@@ -616,49 +615,64 @@ ae_anons/
## История изменений ## История изменений
### v0.2.3 (текущая) ### v0.2.6 (текущая)
- Сортировка заданий по дате и времени события (самые ранние — первыми)
- Сортировка по варианту (оригинал → today → tomorrow)
- Последовательная отправка для гарантированного порядка в очереди
- Исправлена нормализация дефисов (2 и более дефиса подряд → 1)
- Исправлен SINGLE шаблон (убран несуществующий слой TEAMS)
- Добавлена поддержка кеширования ассетов (cache: true/false)
- Логотипы команд не кешируются, логотипы каналов и видео-паки кешируются
### v0.2.5
- Оптимизация памяти: очистка неиспользуемых логотипов
- Групповая отправка заданий по 3 для сохранения порядка
- Добавлен UID в логи отправки
### v0.2.4
- Полностью автономный режим (встроенный Font Awesome)
- Добавлен favicon
- Все статические ресурсы встроены в бинарник
### v0.2.3
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Веб-интерфейс с настраиваемым портом (WEB_PORT)
- Тёмная/светлая тема с автоопределением ОС
- Сортировка по всем колонкам в интерфейсе
- Фильтрация по имени файла/UID
- Остановка всех активных заданий
- Автообновление каждые 60 секунд
- Улучшенное debug-логирование - Улучшенное debug-логирование
- Исправлена обработка UNC/SMB путей - Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
### v0.2.2 ### v0.2.2
- Исправлено отображение имён выходных файлов в веб-интерфейсе - Исправлено отображение имён выходных файлов в веб-интерфейсе
- Улучшена цветовая схема - Улучшена цветовая схема
- Добавлена сортировка по всем колонкам - Добавлена сортировка по всем колонкам
- Вынесены стили в отдельный CSS файл - Вынесены стили в отдельный CSS файл
### v0.2.0 ### v0.2.0
- Добавлен веб-интерфейс для управления заданиями - Добавлен веб-интерфейс для управления заданиями
- Реализован REST API - Реализован REST API
- Добавлена поддержка тёмной/светлой темы - Добавлена поддержка тёмной/светлой темы
- Автообновление статуса заданий - Автообновление статуса заданий
- Возможность остановки всех активных заданий - Возможность остановки всех активных заданий
### v0.1.1 ### v0.1.1
- Оптимизирована работа с памятью - Оптимизирована работа с памятью
- Убрана функция создания .json - Убрана функция создания .json
### v0.1.0 ### v0.1.0
- Первый выпуск - Первый выпуск
- Интеграция с Synology Office - Интеграция с Synology Office
- Базовая генерация заданий Nexrender - Базовая генерация заданий Nexrender
- Парсинг Excel с динамическим обнаружением листов - Парсинг Excel с динамическим обнаружением листов
--- ---

View File

@@ -155,6 +155,8 @@ pub enum Asset {
asset_type: String, asset_type: String,
#[serde(rename = "layerName")] #[serde(rename = "layerName")]
layer_name: String, layer_name: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
cache: bool,
}, },
Video { Video {
src: String, src: String,
@@ -162,6 +164,8 @@ pub enum Asset {
asset_type: String, asset_type: String,
#[serde(rename = "layerName")] #[serde(rename = "layerName")]
layer_name: String, layer_name: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
cache: bool,
}, },
} }
@@ -208,6 +212,10 @@ pub struct JobData {
pub channel_logo: String, pub channel_logo: String,
pub outfile_name: String, pub outfile_name: String,
pub row_index: usize, pub row_index: usize,
// Поля для сортировки
pub sort_date: NaiveDate,
pub sort_time: String,
pub variant_order: u8,
} }
impl JobData { impl JobData {
@@ -233,6 +241,7 @@ impl JobData {
let (date_obj, data_display) = parse_date(&data_str); let (date_obj, data_display) = parse_date(&data_str);
let (time_h, time_m) = parse_time(&time); let (time_h, time_m) = parse_time(&time);
let sort_time = format!("{}:{}", time_h, time_m);
let pack_path = packs.get(&sport).cloned().unwrap_or_default(); let pack_path = packs.get(&sport).cloned().unwrap_or_default();
let channel_logo = channels.get(&channel).cloned().unwrap_or_default(); let channel_logo = channels.get(&channel).cloned().unwrap_or_default();
@@ -268,8 +277,8 @@ impl JobData {
.replace(' ', "-") .replace(' ', "-")
.replace('\'', ""); .replace('\'', "");
// Если 3 и более дефисов подряд -> заменяем на один // Заменяем 2 и более дефисов подряд на один
let re = Regex::new(r"-{3,}").unwrap(); let re = Regex::new(r"-{2,}").unwrap();
outfile_name = re.replace_all(&outfile_name, "-").to_string(); outfile_name = re.replace_all(&outfile_name, "-").to_string();
// Убираем дефисы в начале и конце // Убираем дефисы в начале и конце
@@ -294,19 +303,34 @@ impl JobData {
channel_logo: unc_to_uri(&channel_logo), channel_logo: unc_to_uri(&channel_logo),
outfile_name, outfile_name,
row_index, row_index,
sort_date: date_obj,
sort_time,
variant_order: 0,
}) })
} }
pub fn create_variants(&self) -> Vec<Self> { pub fn create_variants(&self) -> Vec<Self> {
let mut variants = vec![self.clone()]; let mut variants = Vec::new();
// Оригинал (0)
let mut original = self.clone();
original.variant_order = 0;
variants.push(original);
// Today (1)
let mut today = self.clone(); let mut today = self.clone();
today.data_display = "сегодня".to_string(); today.data_display = "сегодня".to_string();
today.outfile_name = format!("{}_Today", self.outfile_name); today.outfile_name = format!("{}_Today", self.outfile_name);
today.variant_order = 1;
variants.push(today); variants.push(today);
// Tomorrow (2)
let mut tomorrow = self.clone(); let mut tomorrow = self.clone();
tomorrow.data_display = "завтра".to_string(); tomorrow.data_display = "завтра".to_string();
tomorrow.outfile_name = format!("{}_Tomorrow", self.outfile_name); tomorrow.outfile_name = format!("{}_Tomorrow", self.outfile_name);
tomorrow.variant_order = 2;
variants.push(tomorrow); variants.push(tomorrow);
variants variants
} }
@@ -346,6 +370,7 @@ impl JobData {
src: self.pack_path.clone(), src: self.pack_path.clone(),
asset_type: "video".to_string(), asset_type: "video".to_string(),
layer_name: "TOP".to_string(), layer_name: "TOP".to_string(),
cache: true,
}); });
} }
@@ -357,6 +382,7 @@ impl JobData {
} }
fn add_text_assets(&self, assets: &mut Vec<Asset>) { fn add_text_assets(&self, assets: &mut Vec<Asset>) {
// DATA — всегда
assets.push(Asset::Data { assets.push(Asset::Data {
asset_type: "data".to_string(), asset_type: "data".to_string(),
layer_name: "DATA".to_string(), layer_name: "DATA".to_string(),
@@ -365,7 +391,9 @@ impl JobData {
}); });
self.add_date_adjustments(assets); self.add_date_adjustments(assets);
// TIME — зависит от шаблона
if !self.team_b.is_empty() { if !self.team_b.is_empty() {
// DOUBLE: TIME_H и TIME_M
assets.push(Asset::Data { assets.push(Asset::Data {
asset_type: "data".to_string(), asset_type: "data".to_string(),
layer_name: "TIME_H".to_string(), layer_name: "TIME_H".to_string(),
@@ -380,6 +408,7 @@ impl JobData {
}); });
self.add_time_adjustments(assets); self.add_time_adjustments(assets);
} else { } else {
// SINGLE: TIME
assets.push(Asset::Data { assets.push(Asset::Data {
asset_type: "data".to_string(), asset_type: "data".to_string(),
layer_name: "TIME".to_string(), layer_name: "TIME".to_string(),
@@ -388,6 +417,7 @@ impl JobData {
}); });
} }
// LEAGUE — всегда
assets.push(Asset::Data { assets.push(Asset::Data {
asset_type: "data".to_string(), asset_type: "data".to_string(),
layer_name: "LEAGUE".to_string(), layer_name: "LEAGUE".to_string(),
@@ -396,6 +426,7 @@ impl JobData {
}); });
self.add_league_adjustments(assets); self.add_league_adjustments(assets);
// SPORT — всегда
if !self.sport.is_empty() { if !self.sport.is_empty() {
assets.push(Asset::Data { assets.push(Asset::Data {
asset_type: "data".to_string(), asset_type: "data".to_string(),
@@ -405,14 +436,9 @@ impl JobData {
}); });
} }
let teams_text = if !self.team_a.is_empty() && !self.team_b.is_empty() { // TEAMS — ТОЛЬКО для DOUBLE шаблона
format!("{} - {}", self.team_a, self.team_b) if !self.team_b.is_empty() {
} else if !self.team_a.is_empty() { let teams_text = format!("{} - {}", self.team_a, self.team_b);
self.team_a.clone()
} else {
self.team_b.clone()
};
if !teams_text.is_empty() {
assets.push(Asset::Data { assets.push(Asset::Data {
asset_type: "data".to_string(), asset_type: "data".to_string(),
layer_name: "TEAMS".to_string(), layer_name: "TEAMS".to_string(),
@@ -485,11 +511,13 @@ impl JobData {
} }
fn add_image_assets(&self, assets: &mut Vec<Asset>) { fn add_image_assets(&self, assets: &mut Vec<Asset>) {
// Логотипы команд — НЕ кешируем (часто меняются)
if !self.team_a_logo.is_empty() { if !self.team_a_logo.is_empty() {
assets.push(Asset::Image { assets.push(Asset::Image {
src: self.team_a_logo.clone(), src: self.team_a_logo.clone(),
asset_type: "image".to_string(), asset_type: "image".to_string(),
layer_name: "TEAM_A_LOGO".to_string(), layer_name: "TEAM_A_LOGO".to_string(),
cache: false,
}); });
if let Some(res) = &self.team_a_logo_res { if let Some(res) = &self.team_a_logo_res {
assets.push(Asset::DataExpression { assets.push(Asset::DataExpression {
@@ -505,6 +533,7 @@ impl JobData {
src: self.team_b_logo.clone(), src: self.team_b_logo.clone(),
asset_type: "image".to_string(), asset_type: "image".to_string(),
layer_name: "TEAM_B_LOGO".to_string(), layer_name: "TEAM_B_LOGO".to_string(),
cache: false,
}); });
if let Some(res) = &self.team_b_logo_res { if let Some(res) = &self.team_b_logo_res {
assets.push(Asset::DataExpression { assets.push(Asset::DataExpression {
@@ -515,11 +544,13 @@ impl JobData {
}); });
} }
} }
// Логотип канала — КЕШИРУЕМ (редко меняется)
if !self.channel_logo.is_empty() { if !self.channel_logo.is_empty() {
assets.push(Asset::Image { assets.push(Asset::Image {
src: self.channel_logo.clone(), src: self.channel_logo.clone(),
asset_type: "image".to_string(), asset_type: "image".to_string(),
layer_name: "CHANELL".to_string(), layer_name: "CHANELL".to_string(),
cache: true,
}); });
} }
} }
@@ -545,7 +576,6 @@ impl LogoRegistry {
self.logos.insert((team, sport), link); self.logos.insert((team, sport), link);
} }
// В impl LogoRegistry, функция find()
pub fn find(&self, team: &str, sport: &str) -> Option<String> { pub fn find(&self, team: &str, sport: &str) -> Option<String> {
log::debug!("LogoRegistry::find: team='{}', sport='{}'", team, sport); log::debug!("LogoRegistry::find: team='{}', sport='{}'", team, sport);
@@ -564,6 +594,19 @@ impl LogoRegistry {
log::warn!("No logo found for team='{}', sport='{}'", team, sport); log::warn!("No logo found for team='{}', sport='{}'", team, sport);
None None
} }
/// Оставляет только указанные команды, удаляя все остальные
pub fn retain<F>(&mut self, mut predicate: F)
where
F: FnMut(&str, &str) -> bool,
{
self.logos.retain(|(team, sport), _| predicate(team, sport));
}
/// Возвращает количество записей в реестре
pub fn len(&self) -> usize {
self.logos.len()
}
} }
fn parse_date(s: &str) -> (NaiveDate, String) { fn parse_date(s: &str) -> (NaiveDate, String) {
@@ -639,6 +682,7 @@ fn unc_to_uri(path: &str) -> String {
format!("file://{}", trimmed) format!("file://{}", trimmed)
} }
fn logo_scale_expression(target_size: &str) -> String { fn logo_scale_expression(target_size: &str) -> String {
format!( format!(
"if (width > height) {{max_size = width;}} else {{max_size = height;}} var real_size = {}/max_size*100;[real_size,real_size]", "if (width > height) {{max_size = width;}} else {{max_size = height;}} var real_size = {}/max_size*100;[real_size,real_size]",

View File

@@ -274,52 +274,50 @@ fn display_workbook_structure(workbook: &ExcelWorkbook) {
} }
} }
// Вспомогательная функция для поиска значения по части ключа
fn get_cell_fuzzy(row: &HashMap<String, String>, key_part: &str) -> Option<String> {
if let Some(val) = row.get(key_part) {
if !val.is_empty() {
return Some(val.clone());
}
}
for (k, v) in row.iter() {
if k.to_lowercase().contains(&key_part.to_lowercase()) && !v.is_empty() {
return Some(v.clone());
}
}
None
}
async fn generate_and_submit_jobs( async fn generate_and_submit_jobs(
workbook: &ExcelWorkbook, workbook: &ExcelWorkbook,
config: &Config, config: &Config,
) -> Result<Vec<(String, String)>> { ) -> Result<Vec<(String, String)>> {
info!("Preparing Nexrender jobs..."); info!("Preparing Nexrender jobs...");
let start_sheet = workbook // ========== ЗАГРУЖАЕМ SPORT PACKS ==========
.get_sheet("Start")
.ok_or_else(|| anyhow!("Sheet 'Start' not found"))?;
let sport_sheet = workbook let sport_sheet = workbook
.get_sheet("SPORT") .get_sheet("SPORT")
.ok_or_else(|| anyhow!("Sheet 'SPORT' not found"))?; .ok_or_else(|| anyhow!("Sheet 'SPORT' not found"))?;
let teams_sheet = workbook
.get_sheet("TEAMS")
.ok_or_else(|| anyhow!("Sheet 'TEAMS' not found"))?;
let channel_sheet = workbook
.get_sheet("CHANELL")
.ok_or_else(|| anyhow!("Sheet 'CHANELL' not found"))?;
// Загружаем sport packs
let packs: HashMap<String, String> = sport_sheet let packs: HashMap<String, String> = sport_sheet
.rows .rows
.iter() .iter()
.filter_map(|row| Some((row.get("SPORT")?.clone(), row.get("LINK")?.clone()))) .filter_map(|row| Some((row.get("SPORT")?.clone(), row.get("LINK")?.clone())))
.collect(); .collect();
info!("Loaded {} sport packs", packs.len()); info!("Loaded {} sport packs", packs.len());
debug!("Processed SPORT sheet ({} rows)", sport_sheet.rows.len());
// Вспомогательная функция для поиска значения по части ключа // ========== ЗАГРУЖАЕМ TEAM LOGOS ==========
fn get_cell_fuzzy(row: &HashMap<String, String>, key_part: &str) -> Option<String> { let teams_sheet = workbook
if let Some(val) = row.get(key_part) { .get_sheet("TEAMS")
if !val.is_empty() { .ok_or_else(|| anyhow!("Sheet 'TEAMS' not found"))?;
return Some(val.clone());
}
}
for (k, v) in row.iter() {
if k.to_lowercase().contains(&key_part.to_lowercase()) && !v.is_empty() {
return Some(v.clone());
}
}
None
}
// Загружаем team logos
debug!("TEAMS headers: {:?}", teams_sheet.headers); debug!("TEAMS headers: {:?}", teams_sheet.headers);
for (i, row) in teams_sheet.rows.iter().take(3).enumerate() { if log_enabled!(Level::Debug) {
debug!("TEAMS row {}: {:?}", i, row); for (i, row) in teams_sheet.rows.iter().take(3).enumerate() {
debug!("TEAMS row {}: {:?}", i, row);
}
} }
let mut logos = LogoRegistry::with_capacity(teams_sheet.rows.len()); let mut logos = LogoRegistry::with_capacity(teams_sheet.rows.len());
@@ -332,7 +330,8 @@ async fn generate_and_submit_jobs(
logos.insert(team.clone(), sport.clone(), link.clone()); logos.insert(team.clone(), sport.clone(), link.clone());
} }
} }
info!("Loaded {} team logos", teams_sheet.rows.len()); let total_teams = teams_sheet.rows.len();
info!("Loaded {} team logos", total_teams);
// Дебаг: выводим статистику по TEAMS // Дебаг: выводим статистику по TEAMS
if log_enabled!(Level::Debug) { if log_enabled!(Level::Debug) {
@@ -343,19 +342,23 @@ async fn generate_and_submit_jobs(
.collect(); .collect();
debug!("Sports found in TEAMS sheet: {:?}", sports); debug!("Sports found in TEAMS sheet: {:?}", sports);
for sport in sports.iter() { for sport in sports.iter().take(5) {
let examples: Vec<_> = teams_sheet let examples: Vec<_> = teams_sheet
.rows .rows
.iter() .iter()
.filter(|r| r.get("SPORT") == Some(*sport)) .filter(|r| r.get("SPORT") == Some(*sport))
.take(5) .take(3)
.filter_map(|r| r.get("TEAM")) .filter_map(|r| r.get("TEAM"))
.collect(); .collect();
debug!(" TEAMS examples for '{}': {:?}", sport, examples); debug!(" TEAMS examples for '{}': {:?}", sport, examples);
} }
} }
// Загружаем channel logos // ========== ЗАГРУЖАЕМ CHANNEL LOGOS ==========
let channel_sheet = workbook
.get_sheet("CHANELL")
.ok_or_else(|| anyhow!("Sheet 'CHANELL' not found"))?;
let channels: HashMap<String, String> = channel_sheet let channels: HashMap<String, String> = channel_sheet
.rows .rows
.iter() .iter()
@@ -367,40 +370,76 @@ async fn generate_and_submit_jobs(
debug!("Channels loaded: {:?}", channels.keys().collect::<Vec<_>>()); debug!("Channels loaded: {:?}", channels.keys().collect::<Vec<_>>());
} }
let mut jobs: Vec<JobData> = Vec::with_capacity(start_sheet.rows.len() * 3); // ========== ОБРАБАТЫВАЕМ START (только активные строки) ==========
let start_sheet = workbook
.get_sheet("Start")
.ok_or_else(|| anyhow!("Sheet 'Start' not found"))?;
// Собираем статистику по ненайденным логотипам для дебага let total_start_rows = start_sheet.rows.len();
// Сразу фильтруем только строки с STATE = "FALSE"
let active_rows: Vec<(usize, &HashMap<String, String>)> = start_sheet
.rows
.iter()
.enumerate()
.filter(|(_, row)| row.get("STATE").map(|s| s.as_str()) == Some("FALSE"))
.collect();
info!(
"Found {} active rows (STATE='FALSE') out of {} total",
active_rows.len(),
total_start_rows
);
// Собираем использованные команды для очистки logos
let mut used_teams: HashSet<(String, String)> = HashSet::new();
let mut jobs: Vec<JobData> = Vec::with_capacity(active_rows.len() * 3);
// Для дебага
let mut missing_teams: HashSet<String> = HashSet::new(); let mut missing_teams: HashSet<String> = HashSet::new();
let mut missing_sports: HashSet<String> = HashSet::new(); let mut missing_sports: HashSet<String> = HashSet::new();
for (idx, row) in start_sheet.rows.iter().enumerate() { for (idx, row) in active_rows {
if let Some(state) = row.get("STATE") { let team_a = row.get("TEAM A").cloned().unwrap_or_default();
if state == "FALSE" { let team_b = row.get("TEAM B").cloned().unwrap_or_default();
let team_a = row.get("TEAM A").cloned().unwrap_or_default(); let sport = row.get("SPORT").cloned().unwrap_or_default();
let team_b = row.get("TEAM B").cloned().unwrap_or_default();
let sport = row.get("SPORT").cloned().unwrap_or_default();
if log_enabled!(Level::Debug) { // Запоминаем использованные команды
let logo_a = logos.find(&team_a, &sport); if !team_a.is_empty() {
let logo_b = logos.find(&team_b, &sport); used_teams.insert((team_a.clone(), sport.clone()));
}
if !team_b.is_empty() {
used_teams.insert((team_b.clone(), sport.clone()));
}
if !team_a.is_empty() && logo_a.is_none() { if log_enabled!(Level::Debug) {
missing_teams.insert(team_a.clone()); let logo_a = logos.find(&team_a, &sport);
missing_sports.insert(sport.clone()); let logo_b = logos.find(&team_b, &sport);
}
if !team_b.is_empty() && logo_b.is_none() {
missing_teams.insert(team_b.clone());
missing_sports.insert(sport.clone());
}
}
if let Some(job) = JobData::from_row(row, idx, &packs, &logos, &channels) { if !team_a.is_empty() && logo_a.is_none() {
jobs.extend(job.create_variants()); missing_teams.insert(team_a.clone());
} missing_sports.insert(sport.clone());
}
if !team_b.is_empty() && logo_b.is_none() {
missing_teams.insert(team_b.clone());
missing_sports.insert(sport.clone());
} }
} }
if let Some(job) = JobData::from_row(row, idx, &packs, &logos, &channels) {
jobs.extend(job.create_variants());
}
} }
// Очищаем logos от неиспользуемых команд
let logos_before = logos.len();
logos.retain(|team, sport| used_teams.contains(&(team.to_string(), sport.to_string())));
info!(
"Retained {} used team logos (cleaned up {} unused)",
logos.len(),
logos_before - logos.len()
);
// Дебаг: выводим сводку по отсутствующим логотипам // Дебаг: выводим сводку по отсутствующим логотипам
if log_enabled!(Level::Debug) && !missing_teams.is_empty() { if log_enabled!(Level::Debug) && !missing_teams.is_empty() {
debug!("=== MISSING LOGOS SUMMARY ==="); debug!("=== MISSING LOGOS SUMMARY ===");
@@ -415,72 +454,68 @@ async fn generate_and_submit_jobs(
info!("Generated {} total jobs (including variants)", jobs.len()); info!("Generated {} total jobs (including variants)", jobs.len());
// ========== СОРТИРОВКА ПО ДАТЕ И ВРЕМЕНИ ==========
jobs.sort_by(|a, b| match a.sort_date.cmp(&b.sort_date) {
std::cmp::Ordering::Equal => match a.sort_time.cmp(&b.sort_time) {
std::cmp::Ordering::Equal => a.variant_order.cmp(&b.variant_order),
other => other,
},
other => other,
});
info!("Jobs sorted by date and time (earliest first)");
if log_enabled!(Level::Debug) {
for (i, job) in jobs.iter().take(10).enumerate() {
debug!(
" {}: {} {} - {} (variant: {})",
i + 1,
job.sort_date,
job.sort_time,
job.outfile_name,
job.variant_order
);
}
}
if jobs.is_empty() { if jobs.is_empty() {
info!("No jobs with STATE='FALSE' found"); info!("No jobs with STATE='FALSE' found");
return Ok(Vec::new()); return Ok(Vec::new());
} }
// ========== ОТПРАВКА ЗАДАНИЙ (ПОСЛЕДОВАТЕЛЬНО) ==========
cleanup_finished_jobs(&config.nexrender_api_url).await?; cleanup_finished_jobs(&config.nexrender_api_url).await?;
let http_client = Client::new(); let http_client = Client::new();
let mut submitted_jobs: Vec<(String, String)> = Vec::with_capacity(jobs.len()); let mut submitted_jobs: Vec<(String, String)> = Vec::with_capacity(jobs.len());
// Отправляем задания группами по 3 (оригинал + today + tomorrow) for job in jobs {
for chunk in jobs.chunks(3) { let nexrender_job = job.to_nexrender_job(config);
let mut tasks = Vec::with_capacity(3); info!("Submitting job: {}", job.outfile_name);
for job in chunk { if log_enabled!(Level::Debug) {
let client = http_client.clone(); debug!(
let api_url = config.nexrender_api_url.clone(); "Job details: sport='{}', league='{}', channel='{}', team_a='{}', team_b='{}'",
let config_clone = config.clone(); job.sport, job.league, job.channel, job.team_a, job.team_b
let job_owned = job.clone(); );
tasks.push(tokio::spawn(async move {
let nexrender_job = job_owned.to_nexrender_job(&config_clone);
info!("Submitting job: {}", job_owned.outfile_name);
if log_enabled!(Level::Debug) {
debug!(
"Job details: sport='{}', league='{}', channel='{}', team_a='{}', team_b='{}'",
job_owned.sport, job_owned.league, job_owned.channel, job_owned.team_a, job_owned.team_b
);
}
let response = client.post(&api_url).json(&nexrender_job).send().await?;
if response.status().is_success() {
let result: Value = response.json().await?;
if let Some(uid) = result.get("uid").and_then(|u| u.as_str()) {
Ok::<_, anyhow::Error>(Some((uid.to_string(), job_owned.outfile_name)))
} else {
Ok(None)
}
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
error!("Failed to submit job ({}): {}", status, text);
Ok(None)
}
}));
} }
// Ждём завершения группы let response = http_client
for task in tasks { .post(&config.nexrender_api_url)
match task.await { .json(&nexrender_job)
Ok(Ok(Some((uid, outfile_name)))) => { .send()
info!("Job submitted: {} (UID: {})", outfile_name, uid); .await?;
submitted_jobs.push((uid, outfile_name));
} if response.status().is_success() {
Ok(Ok(None)) => { let result: Value = response.json().await?;
debug!("Job submission returned no UID"); if let Some(uid) = result.get("uid").and_then(|u| u.as_str()) {
} info!("Job submitted: {} (UID: {})", job.outfile_name, uid);
Ok(Err(e)) => { submitted_jobs.push((uid.to_string(), job.outfile_name.clone()));
error!("Job submission error: {}", e);
}
Err(e) => {
error!("Task join error: {}", e);
}
} }
} else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
error!("Failed to submit job ({}): {}", status, text);
} }
} }

BIN
src/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -4,12 +4,13 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AE Anons - Nexrender Job Manager</title> <title>AE Anons</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/assets/logo.png">
<!-- CSS -->
<link rel="stylesheet" href="/static/fontawesome/all.min.css"> <link rel="stylesheet" href="/static/fontawesome/all.min.css">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
@@ -26,7 +27,7 @@
<i class="fas fa-bolt"></i> <i class="fas fa-bolt"></i>
</div> </div>
</div> </div>
<h1>AE Anons - Nexrender Job Manager</h1> <h1>AE Anons</h1>
</div> </div>
<div class="header-controls"> <div class="header-controls">
<button class="theme-toggle" onclick="toggleTheme()"> <button class="theme-toggle" onclick="toggleTheme()">

View File

@@ -11,7 +11,6 @@ use serde::Serialize;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
#[derive(Clone)] #[derive(Clone)]
@@ -89,7 +88,18 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
.route("/favicon.ico", get(favicon)) .route("/favicon.ico", get(favicon))
.route("/static/style.css", get(style_css)) .route("/static/style.css", get(style_css))
.route("/static/fontawesome/all.min.css", get(fontawesome_css)) .route("/static/fontawesome/all.min.css", get(fontawesome_css))
.nest_service("/static/fontawesome/webfonts", ServeDir::new("src/static/fontawesome/webfonts")) .route(
"/static/fontawesome/webfonts/fa-solid-900.woff2",
get(fa_solid_woff2),
)
.route(
"/static/fontawesome/webfonts/fa-regular-400.woff2",
get(fa_regular_woff2),
)
.route(
"/static/fontawesome/webfonts/fa-brands-400.woff2",
get(fa_brands_woff2),
)
.route("/assets/logo.png", get(logo_png)) .route("/assets/logo.png", get(logo_png))
.route("/api/jobs", get(list_jobs)) .route("/api/jobs", get(list_jobs))
.route("/api/generate", post(generate_jobs)) .route("/api/generate", post(generate_jobs))
@@ -98,7 +108,6 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
.route("/api/jobs/stop-all", post(stop_all_jobs)) .route("/api/jobs/stop-all", post(stop_all_jobs))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.with_state(state); .with_state(state);
let addr: SocketAddr = format!("0.0.0.0:{}", web_port).parse()?; let addr: SocketAddr = format!("0.0.0.0:{}", web_port).parse()?;
log::info!("Web server listening on http://{}", addr); log::info!("Web server listening on http://{}", addr);
@@ -112,19 +121,31 @@ async fn index_page() -> Html<&'static str> {
} }
async fn style_css() -> impl IntoResponse { async fn style_css() -> impl IntoResponse {
([(CONTENT_TYPE, "text/css")], include_str!("static/style.css")) (
[(CONTENT_TYPE, "text/css")],
include_str!("static/style.css"),
)
} }
async fn fontawesome_css() -> impl IntoResponse { async fn fontawesome_css() -> impl IntoResponse {
([(CONTENT_TYPE, "text/css")], include_str!("static/fontawesome/all.min.css")) (
[(CONTENT_TYPE, "text/css")],
include_str!("static/fontawesome/all.min.css"),
)
} }
async fn logo_png() -> impl IntoResponse { async fn logo_png() -> impl IntoResponse {
([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice()) (
[(CONTENT_TYPE, "image/png")],
include_bytes!("../assets/logo.png").as_slice(),
)
} }
async fn favicon() -> impl IntoResponse { async fn favicon() -> impl IntoResponse {
([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice()) (
[(CONTENT_TYPE, "image/x-icon")],
include_bytes!("static/favicon.ico").as_slice(),
)
} }
async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> { async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> {
@@ -214,6 +235,27 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
Ok((StatusCode::OK, format!("Stopped {} jobs", stopped))) Ok((StatusCode::OK, format!("Stopped {} jobs", stopped)))
} }
async fn fa_solid_woff2() -> impl IntoResponse {
(
[(CONTENT_TYPE, "font/woff2")],
include_bytes!("static/fontawesome/webfonts/fa-solid-900.woff2").as_slice(),
)
}
async fn fa_regular_woff2() -> impl IntoResponse {
(
[(CONTENT_TYPE, "font/woff2")],
include_bytes!("static/fontawesome/webfonts/fa-regular-400.woff2").as_slice(),
)
}
async fn fa_brands_woff2() -> impl IntoResponse {
(
[(CONTENT_TYPE, "font/woff2")],
include_bytes!("static/fontawesome/webfonts/fa-brands-400.woff2").as_slice(),
)
}
struct AppError(StatusCode, String); struct AppError(StatusCode, String);
impl IntoResponse for AppError { impl IntoResponse for AppError {