Files
AE_Anons/src/nexrender.rs
Алексей Барабанов 11fc697771 Оптимизирована работа с данными листов и улучшена обработка Excel‑дат
Упрощено формирование row_data без лишних переменных.
Удалены избыточные отладочные println!, уменьшив шум в консоли.
Переписаны комментарии к блокам парсинга, убраны неиспользуемые выводы количества заголовков и строк.
Объединено условие проверки Excel‑даты в компактный диапазон (1.0..100000.0).
Удалены устаревшие комментарии о базовой дате; оставлено только вычисление даты.
Добавлена асинхронная функция cleanup_finished_jobs для очистки завершённых/ошибочных задач Nexrender, с выводом информации о выполненных чистках.
Реализована функция monitor_jobs, периодически проверяющая статус ожидающих заданий, выводящая прогресс и финальное сообщение о завершении всех работ.
Эти изменения делают код чище, повышают читаемость и добавляют автоматизированное управление задачами Nexrender.
2026-04-15 20:25:05 +03:00

587 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use chrono::{Datelike, NaiveDate};
use serde::Serialize;
use serde_json::json;
use std::collections::HashMap;
fn transliterate(text: &str, _reversed: bool) -> String {
let mapping = [
("а", "a"),
("б", "b"),
("в", "v"),
("г", "g"),
("д", "d"),
("е", "e"),
("ё", "yo"),
("ж", "zh"),
("з", "z"),
("и", "i"),
("й", "y"),
("к", "k"),
("л", "l"),
("м", "m"),
("н", "n"),
("о", "o"),
("п", "p"),
("р", "r"),
("с", "s"),
("т", "t"),
("у", "u"),
("ф", "f"),
("х", "h"),
("ц", "ts"),
("ч", "ch"),
("ш", "sh"),
("щ", "sch"),
("ъ", ""),
("ы", "y"),
("ь", ""),
("э", "e"),
("ю", "yu"),
("я", "ya"),
("А", "A"),
("Б", "B"),
("В", "V"),
("Г", "G"),
("Д", "D"),
("Е", "E"),
("Ё", "Yo"),
("Ж", "Zh"),
("З", "Z"),
("И", "I"),
("Й", "Y"),
("К", "K"),
("Л", "L"),
("М", "M"),
("Н", "N"),
("О", "O"),
("П", "P"),
("Р", "R"),
("С", "S"),
("Т", "T"),
("У", "U"),
("Ф", "F"),
("Х", "H"),
("Ц", "Ts"),
("Ч", "Ch"),
("Ш", "Sh"),
("Щ", "Sch"),
("Ъ", ""),
("Ы", "Y"),
("Ь", ""),
("Э", "E"),
("Ю", "Yu"),
("Я", "Ya"),
];
let mut result = text.to_string();
for (cyr, lat) in mapping {
result = result.replace(cyr, lat);
}
result
}
#[derive(Debug, Clone, Serialize)]
pub struct Template {
pub src: String,
pub composition: String,
#[serde(rename = "outputModule")]
pub output_module: String,
#[serde(rename = "outputExt")]
pub output_ext: String,
}
impl Default for Template {
fn default() -> Self {
Self {
src: "file:///c:/users/virtVmix-2/Downloads/PackShot_DOUBLE.aepx".to_string(),
composition: "pack".to_string(),
output_module: "Start_h264".to_string(),
output_ext: "mp4".to_string(),
}
}
}
impl Template {
pub fn single() -> Self {
Self {
src: "file:///c:/users/virtVmix-2/Downloads/PackShot_SINGLE.aepx".to_string(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "module")]
pub enum PostrenderAction {
#[serde(rename = "@nexrender/action-encode")]
Encode { preset: String, output: String },
#[serde(rename = "@nexrender/action-copy")]
Copy { input: String, output: String },
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum Asset {
Data {
#[serde(rename = "type")]
asset_type: String,
#[serde(rename = "layerName")]
layer_name: String,
property: String,
value: serde_json::Value,
},
DataExpression {
#[serde(rename = "type")]
asset_type: String,
#[serde(rename = "layerName")]
layer_name: String,
property: String,
expression: String,
},
Image {
src: String,
#[serde(rename = "type")]
asset_type: String,
#[serde(rename = "layerName")]
layer_name: String,
},
Video {
src: String,
#[serde(rename = "type")]
asset_type: String,
#[serde(rename = "layerName")]
layer_name: String,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct NexrenderJob {
pub template: Template,
pub actions: Actions,
pub assets: Vec<Asset>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Actions {
pub postrender: Vec<PostrenderAction>,
}
impl Default for Actions {
fn default() -> Self {
Self {
postrender: vec![PostrenderAction::Encode {
preset: "mp4".to_string(),
output: "encoded.mp4".to_string(),
}],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct JobData {
pub date: String,
pub time: String,
pub time_h: String,
pub time_m: String,
pub channel: String,
pub sport: String,
pub league: String,
pub team_a: String,
pub team_b: String,
pub data_display: String,
pub pack_path: String,
pub team_a_logo: String,
pub team_b_logo: String,
pub team_a_logo_res: Option<String>,
pub team_b_logo_res: Option<String>,
pub channel_logo: String,
pub outfile_name: String,
pub row_index: usize,
}
impl JobData {
pub fn from_row(
row: &HashMap<String, String>,
row_index: usize,
packs: &HashMap<String, String>,
logos: &LogoRegistry,
channels: &HashMap<String, String>,
) -> Option<Self> {
let state = row.get("STATE").map(|s| s.as_str()).unwrap_or("");
if state != "FALSE" {
return None;
}
let sport = row.get("SPORT")?.clone();
let league = row.get("LEAGUE")?.clone();
let team_a = row.get("TEAM A").cloned().unwrap_or_default();
let team_b = row.get("TEAM B").cloned().unwrap_or_default();
let channel = row.get("CHANEL").cloned().unwrap_or_default();
let time = row.get("TIME")?.clone();
let data_str = row.get("DATA")?.clone();
let (date_obj, data_display) = parse_date(&data_str);
let (time_h, time_m) = parse_time(&time);
let pack_path = packs.get(&sport).cloned().unwrap_or_default();
let channel_logo = channels.get(&channel).cloned().unwrap_or_default();
let (team_a_name, team_a_res) = parse_team_name(&team_a);
let (team_b_name, team_b_res) = parse_team_name(&team_b);
let team_a_logo = logos.find(&team_a, &sport).unwrap_or_default();
let team_b_logo = logos.find(&team_b, &sport).unwrap_or_default();
let mut fn_parts = Vec::new();
fn_parts.push(format!(
"{:04}{:02}{:02}",
date_obj.year(),
date_obj.month(),
date_obj.day()
));
if !sport.is_empty() && sport != "Без оформления" {
fn_parts.push(sport.clone());
}
fn_parts.push(league.trim_end_matches('.').to_string());
if !team_a_name.is_empty() {
fn_parts.push(team_a_name.clone());
}
if !team_b_name.is_empty() {
fn_parts.push(team_b_name.clone());
}
if !channel.is_empty() {
fn_parts.push(channel.clone());
}
let outfile_name = transliterate(&fn_parts.join("_"), true)
.replace(' ', "-")
.replace('\'', "");
Some(Self {
date: data_str,
time: time.clone(),
time_h,
time_m,
channel,
sport,
league,
team_a: team_a_name,
team_b: team_b_name,
data_display,
pack_path: unc_to_uri(&pack_path),
team_a_logo: unc_to_uri(&team_a_logo),
team_b_logo: unc_to_uri(&team_b_logo),
team_a_logo_res: team_a_res,
team_b_logo_res: team_b_res,
channel_logo: unc_to_uri(&channel_logo),
outfile_name,
row_index,
})
}
pub fn create_variants(&self) -> Vec<Self> {
let mut variants = vec![self.clone()];
let mut today = self.clone();
today.data_display = "сегодня".to_string();
today.outfile_name = format!("{}_Today", self.outfile_name);
variants.push(today);
let mut tomorrow = self.clone();
tomorrow.data_display = "завтра".to_string();
tomorrow.outfile_name = format!("{}_Tomorrow", self.outfile_name);
variants.push(tomorrow);
variants
}
pub fn to_nexrender_job(&self, output_folder: &str) -> NexrenderJob {
let template = if self.team_b.is_empty() {
Template::single()
} else {
Template::default()
};
let output_path = format!("{}/{}.mp4", output_folder, self.outfile_name);
let mut actions = Actions::default();
actions.postrender.push(PostrenderAction::Copy {
input: "encoded.mp4".to_string(),
output: output_path,
});
let mut assets = Vec::new();
self.add_text_assets(&mut assets);
self.add_image_assets(&mut assets);
if !self.pack_path.is_empty() {
assets.push(Asset::Video {
src: self.pack_path.clone(),
asset_type: "video".to_string(),
layer_name: "TOP".to_string(),
});
}
NexrenderJob {
template,
actions,
assets,
}
}
fn add_text_assets(&self, assets: &mut Vec<Asset>) {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "DATA".to_string(),
property: "Source Text".to_string(),
value: json!(self.data_display),
});
self.add_date_adjustments(assets);
if !self.team_b.is_empty() {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "TIME_H".to_string(),
property: "Source Text".to_string(),
value: json!(self.time_h),
});
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "TIME_M".to_string(),
property: "Source Text".to_string(),
value: json!(self.time_m),
});
self.add_time_adjustments(assets);
} else {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "TIME".to_string(),
property: "Source Text".to_string(),
value: json!(self.time),
});
}
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "LEAGUE".to_string(),
property: "Source Text".to_string(),
value: json!(self.league),
});
self.add_league_adjustments(assets);
if !self.sport.is_empty() {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "SPORT".to_string(),
property: "Source Text".to_string(),
value: json!(self.sport),
});
}
let teams_text = if !self.team_a.is_empty() && !self.team_b.is_empty() {
format!("{} - {}", self.team_a, self.team_b)
} else if !self.team_a.is_empty() {
self.team_a.clone()
} else {
self.team_b.clone()
};
if !teams_text.is_empty() {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "TEAMS".to_string(),
property: "Source Text".to_string(),
value: json!(teams_text),
});
self.add_teams_adjustments(assets);
}
}
fn add_date_adjustments(&self, assets: &mut Vec<Asset>) {
let (font_size, anchor_point): (&str, Vec<i32>) = match self.data_display.as_str() {
"сегодня" if !self.team_b.is_empty() => ("105", vec![0, 5]),
"завтра" if !self.team_b.is_empty() => ("115", vec![0, 25]),
_ if self.data_display.len() < 6 && !self.team_b.is_empty() => ("120", vec![0, 20]),
_ => return,
};
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "DATA".to_string(),
property: "Source Text.fontSize".to_string(),
value: json!(font_size),
});
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "DATA".to_string(),
property: "transform.anchorPoint".to_string(),
value: json!(anchor_point),
});
}
fn add_time_adjustments(&self, assets: &mut Vec<Asset>) {
let anchor_point = if self.time_h.len() < 2 {
vec![60, 0]
} else if self.time_h.len() == 2 && self.time_h.parse::<i32>().unwrap_or(20) < 20 {
vec![20, 0]
} else {
return;
};
for layer in ["TIME_H", "TIME_M", "TIME"] {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: layer.to_string(),
property: "transform.anchorPoint".to_string(),
value: json!(anchor_point),
});
}
}
fn add_league_adjustments(&self, assets: &mut Vec<Asset>) {
if self.league.len() > 16 {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "LEAGUE".to_string(),
property: "Source Text.fontSize".to_string(),
value: json!("73"),
});
}
}
fn add_teams_adjustments(&self, assets: &mut Vec<Asset>) {
if (self.team_a.len() + self.team_b.len()) >= 32 {
assets.push(Asset::Data {
asset_type: "data".to_string(),
layer_name: "TEAMS".to_string(),
property: "Source Text.fontSize".to_string(),
value: json!("55"),
});
}
}
fn add_image_assets(&self, assets: &mut Vec<Asset>) {
if !self.team_a_logo.is_empty() {
assets.push(Asset::Image {
src: self.team_a_logo.clone(),
asset_type: "image".to_string(),
layer_name: "TEAM_A_LOGO".to_string(),
});
if let Some(res) = &self.team_a_logo_res {
assets.push(Asset::DataExpression {
asset_type: "data".to_string(),
layer_name: "TEAM_A_LOGO".to_string(),
property: "scale".to_string(),
expression: logo_scale_expression(res),
});
}
}
if !self.team_b_logo.is_empty() {
assets.push(Asset::Image {
src: self.team_b_logo.clone(),
asset_type: "image".to_string(),
layer_name: "TEAM_B_LOGO".to_string(),
});
if let Some(res) = &self.team_b_logo_res {
assets.push(Asset::DataExpression {
asset_type: "data".to_string(),
layer_name: "TEAM_B_LOGO".to_string(),
property: "scale".to_string(),
expression: logo_scale_expression(res),
});
}
}
if !self.channel_logo.is_empty() {
assets.push(Asset::Image {
src: self.channel_logo.clone(),
asset_type: "image".to_string(),
layer_name: "CHANELL".to_string(),
});
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LogoRegistry {
logos: HashMap<(String, String), String>,
}
impl LogoRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, team: String, sport: String, link: String) {
self.logos.insert((team, sport), link);
}
pub fn find(&self, team: &str, sport: &str) -> Option<String> {
if let Some(link) = self.logos.get(&(team.to_string(), sport.to_string())) {
return Some(link.clone());
}
for ((t, s), link) in &self.logos {
if s == sport && team.starts_with(t) {
return Some(link.clone());
}
}
None
}
}
fn parse_date(s: &str) -> (NaiveDate, String) {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() == 3 {
if let (Ok(day), Ok(month), Ok(year)) = (
parts[0].parse::<u32>(),
parts[1].parse::<u32>(),
parts[2].parse::<i32>(),
) {
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
let months = [
"",
"января",
"февраля",
"марта",
"апреля",
"мая",
"июня",
"июля",
"августа",
"сентября",
"октября",
"ноября",
"декабря",
];
return (date, format!("{} {}", day, months[month as usize]));
}
}
}
(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), s.to_string())
}
fn parse_time(s: &str) -> (String, String) {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() >= 2 {
(parts[0].to_string(), parts[1].to_string())
} else {
(s.to_string(), "00".to_string())
}
}
fn parse_team_name(s: &str) -> (String, Option<String>) {
let parts: Vec<&str> = s.split('#').collect();
if parts.len() >= 3 {
(parts[0].to_string(), Some(parts[2].to_string()))
} else {
(parts[0].to_string(), None)
}
}
fn unc_to_uri(path: &str) -> String {
if path.is_empty() {
return String::new();
}
if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") {
return path.to_string();
}
format!("file://{}", path.replace('\\', "/").trim_start_matches('/'))
}
fn logo_scale_expression(target_size: &str) -> String {
format!(
"if (width > height) {{max_size = width;}} else {{max_size = height;}} var real_size = {}/max_size*100;[real_size,real_size]",
target_size
)
}