feat: групповая отправка заданий по 3 для сохранения порядка в очереди

- Отправка заданий группами по 3 (оригинал + today + tomorrow) параллельно
- Сохранение порядка внутри группы для корректной FIFO очереди Nexrender
- Добавлен UID в логи отправки заданий
- Версия обновлена до 0.2.4
This commit is contained in:
2026-04-17 18:29:04 +03:00
parent 6f13679dd9
commit 9929dab66f
4 changed files with 66 additions and 63 deletions

2
Cargo.lock generated
View File

@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "ae_anons" name = "ae_anons"
version = "0.2.3" version = "0.2.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"askama", "askama",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ae_anons" name = "ae_anons"
version = "0.2.3" version = "0.2.4"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Alexey Barabanov <a.barabanov@tvstart.ru>"] authors = ["Alexey Barabanov <a.barabanov@tvstart.ru>"]

View File

@@ -12,17 +12,10 @@
Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office. Автоматизированная система для создания спортивных анонсов с использованием шаблонов After Effects через Nexrender, с данными из электронных таблиц Synology Office.
## Новое в версии 0.2.3 ## Новое в версии v0.2.4
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Групповая отправка заданий по 3 (оригинал + today + tomorrow) для сохранения порядка в очереди
- Тёмная/светлая тема с автоопределением ОС - Улучшена читаемость логов отправки (добавлен UID)
- Сортировка по всем колонкам
- Фильтрация по имени файла/UID
- Остановка всех активных заданий
- Автообновление каждые 60 секунд
- Улучшенное debug-логирование
- Исправлена обработка UNC/SMB путей
- Нормализация множественных пробелов в именах файлов"
## Обзор ## Обзор
@@ -576,6 +569,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 +581,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,7 +610,12 @@ ae_anons/
## История изменений ## История изменений
### v0.2.3 (текущая) ### v0.2.4 (текущая)
- Групповая отправка заданий по 3 (оригинал + today + tomorrow) для сохранения порядка в очереди
- Улучшена читаемость логов отправки (добавлен UID)
### v0.2.3
- Веб-интерфейс с настраиваемым портом (WEB_PORT) - Веб-интерфейс с настраиваемым портом (WEB_PORT)
- Улучшенное debug-логирование - Улучшенное debug-логирование

View File

@@ -303,13 +303,11 @@ async fn generate_and_submit_jobs(
// Вспомогательная функция для поиска значения по части ключа // Вспомогательная функция для поиска значения по части ключа
fn get_cell_fuzzy(row: &HashMap<String, String>, key_part: &str) -> Option<String> { fn get_cell_fuzzy(row: &HashMap<String, String>, key_part: &str) -> Option<String> {
// Точное совпадение
if let Some(val) = row.get(key_part) { if let Some(val) = row.get(key_part) {
if !val.is_empty() { if !val.is_empty() {
return Some(val.clone()); return Some(val.clone());
} }
} }
// Поиск по содержанию (без учёта регистра)
for (k, v) in row.iter() { for (k, v) in row.iter() {
if k.to_lowercase().contains(&key_part.to_lowercase()) && !v.is_empty() { if k.to_lowercase().contains(&key_part.to_lowercase()) && !v.is_empty() {
return Some(v.clone()); return Some(v.clone());
@@ -427,55 +425,61 @@ async fn generate_and_submit_jobs(
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());
let mut tasks = Vec::with_capacity(jobs.len()); // Отправляем задания группами по 3 (оригинал + today + tomorrow)
for job in jobs { for chunk in jobs.chunks(3) {
let client = http_client.clone(); let mut tasks = Vec::with_capacity(3);
let api_url = config.nexrender_api_url.clone();
let config_clone = config.clone(); for job in chunk {
let client = http_client.clone();
tasks.push(tokio::spawn(async move { let api_url = config.nexrender_api_url.clone();
let nexrender_job = job.to_nexrender_job(&config_clone); let config_clone = config.clone();
info!("Submitting job: {}", job.outfile_name); let job_owned = job.clone();
if log_enabled!(Level::Debug) { tasks.push(tokio::spawn(async move {
debug!( let nexrender_job = job_owned.to_nexrender_job(&config_clone);
"Job details: sport='{}', league='{}', channel='{}', team_a='{}', team_b='{}'", info!("Submitting job: {}", job_owned.outfile_name);
job.sport, job.league, job.channel, job.team_a, job.team_b
); if log_enabled!(Level::Debug) {
} debug!(
"Job details: sport='{}', league='{}', channel='{}', team_a='{}', team_b='{}'",
let response = client.post(&api_url).json(&nexrender_job).send().await?; job_owned.sport, job_owned.league, job_owned.channel, job_owned.team_a, job_owned.team_b
);
if response.status().is_success() { }
let result: Value = response.json().await?;
if let Some(uid) = result.get("uid").and_then(|u| u.as_str()) { let response = client.post(&api_url).json(&nexrender_job).send().await?;
Ok::<_, anyhow::Error>(Some((uid.to_string(), job.outfile_name)))
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 { } else {
let status = response.status();
let text = response.text().await.unwrap_or_default();
error!("Failed to submit job ({}): {}", status, text);
Ok(None) Ok(None)
} }
} else { }));
let status = response.status(); }
let text = response.text().await.unwrap_or_default();
error!("Failed to submit job ({}): {}", status, text); // Ждём завершения группы
Ok(None) for task in tasks {
} match task.await {
})); Ok(Ok(Some((uid, outfile_name)))) => {
} info!("Job submitted: {} (UID: {})", outfile_name, uid);
submitted_jobs.push((uid, outfile_name));
for task in tasks { }
match task.await { Ok(Ok(None)) => {
Ok(Ok(Some((uid, outfile_name)))) => { debug!("Job submission returned no UID");
info!("Job submitted: {}", outfile_name); }
submitted_jobs.push((uid, outfile_name)); Ok(Err(e)) => {
} error!("Job submission error: {}", e);
Ok(Ok(None)) => { }
debug!("Job submission returned no UID"); Err(e) => {
} error!("Task join error: {}", e);
Ok(Err(e)) => { }
error!("Job submission error: {}", e);
}
Err(e) => {
error!("Task join error: {}", e);
} }
} }
} }
@@ -518,4 +522,4 @@ pub async fn fetch_all_jobs(api_url: &str) -> Result<Vec<Value>> {
} else { } else {
Ok(Vec::new()) Ok(Vec::new())
} }
} }