diff --git a/src/config.rs b/src/config.rs index 53993da..1e9ed38 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use dotenv::dotenv; use std::env; #[derive(Debug, Clone)] @@ -24,8 +23,6 @@ pub struct Config { impl Config { pub fn from_env() -> Result { - dotenv().ok(); - Ok(Self { nas_fqdn: env::var("NAS_FQDN").context("NAS_FQDN not set")?, nas_user: env::var("NAS_USER").context("NAS_USER not set")?, diff --git a/src/main.rs b/src/main.rs index 0a553ab..cb6faf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod web; use anyhow::Result; use clap::Parser; use config::Config; +use dotenv::dotenv; use log::info; #[derive(Parser)] @@ -23,6 +24,10 @@ struct Cli { #[tokio::main] async fn main() -> Result<()> { + // Загружаем .env до инициализации логгера + dotenv().ok(); + + // Инициализируем логгер env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let cli = Cli::parse(); @@ -35,7 +40,6 @@ async fn main() -> Result<()> { ); web::run_web_server(config).await?; } else { - // По умолчанию или с флагом --once выполняем однократную обработку info!( "Starting AE Anons processor v{} (one-time mode)", env!("CARGO_PKG_VERSION") @@ -45,4 +49,4 @@ async fn main() -> Result<()> { } Ok(()) -} \ No newline at end of file +} diff --git a/src/nexrender.rs b/src/nexrender.rs index c649fba..bd7e29b 100644 --- a/src/nexrender.rs +++ b/src/nexrender.rs @@ -537,15 +537,23 @@ impl LogoRegistry { self.logos.insert((team, sport), link); } + // В impl LogoRegistry, функция find() pub fn find(&self, team: &str, sport: &str) -> Option { + log::debug!("LogoRegistry::find: team='{}', sport='{}'", team, sport); + if let Some(link) = self.logos.get(&(team.to_string(), sport.to_string())) { + log::debug!("Found exact match: {}", link); return Some(link.clone()); } + for ((t, s), link) in &self.logos { if s == sport && team.starts_with(t) { + log::debug!("Found prefix match: t='{}', link='{}'", t, link); return Some(link.clone()); } } + + log::warn!("No logo found for team='{}', sport='{}'", team, sport); None } } @@ -603,12 +611,26 @@ fn unc_to_uri(path: &str) -> String { if path.is_empty() { return String::new(); } + + // Уже правильный URI if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") { return path.to_string(); } - format!("file://{}", path.replace('\\', "/").trim_start_matches('/')) -} + // Mac SMB путь: smb://server/share/path -> file://server/share/path + if path.starts_with("smb://") { + let without_smb = path.strip_prefix("smb:").unwrap_or(path); + return format!("file:{}", without_smb.replace('\\', "/")); + } + + // Windows UNC путь: \\server\share\path -> file://server/share/path + let normalized = path.replace('\\', "/"); + + // Убираем ведущие слеши (для UNC их быть не должно) + let trimmed = normalized.trim_start_matches('/'); + + format!("file://{}", trimmed) +} 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]", diff --git a/src/processor.rs b/src/processor.rs index 90ad180..97de5e7 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -4,10 +4,10 @@ use crate::synology::SynologyClient; use anyhow::{anyhow, Result}; use calamine::{Data, Reader, Xlsx}; use chrono::{Duration, NaiveDate}; -use log::{debug, error, info}; +use log::{debug, error, info, log_enabled, Level}; use reqwest::Client; use serde_json::Value; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io::Cursor; use std::path::Path; @@ -211,10 +211,8 @@ fn cell_to_string_optimized(cell: &Data) -> String { if is_excel_date(*f) { excel_date_to_string(*f) } else if f.fract() == 0.0 { - // Используем itoa для целых чисел (опционально) format!("{:.0}", f) } else { - // Используем ryu для float (опционально) f.to_string() } } @@ -256,7 +254,6 @@ fn excel_date_to_string(serial: f64) -> String { let days = serial as i64; let base = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap(); if let Some(date) = base.checked_add_signed(Duration::days(days)) { - // Используем метод format напрямую - он публичный date.format("%d.%m.%Y").to_string() } else { serial.to_string() @@ -296,6 +293,7 @@ async fn generate_and_submit_jobs( .get_sheet("CHANELL") .ok_or_else(|| anyhow!("Sheet 'CHANELL' not found"))?; + // Загружаем sport packs let packs: HashMap = sport_sheet .rows .iter() @@ -303,16 +301,63 @@ async fn generate_and_submit_jobs( .collect(); info!("Loaded {} sport packs", packs.len()); + // Вспомогательная функция для поиска значения по части ключа + fn get_cell_fuzzy(row: &HashMap, key_part: &str) -> Option { + // Точное совпадение + 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 + } + + // Загружаем team logos + debug!("TEAMS headers: {:?}", teams_sheet.headers); + 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()); for row in &teams_sheet.rows { - if let (Some(team), Some(sport), Some(link)) = - (row.get("TEAM"), row.get("SPORT"), row.get("LINK")) - { + let team = get_cell_fuzzy(row, "TEAM"); + let sport = get_cell_fuzzy(row, "SPORT"); + let link = get_cell_fuzzy(row, "LINK"); + + if let (Some(team), Some(sport), Some(link)) = (team, sport, link) { logos.insert(team.clone(), sport.clone(), link.clone()); } } info!("Loaded {} team logos", teams_sheet.rows.len()); + // Дебаг: выводим статистику по TEAMS + if log_enabled!(Level::Debug) { + let sports: HashSet<_> = teams_sheet + .rows + .iter() + .filter_map(|r| r.get("SPORT")) + .collect(); + debug!("Sports found in TEAMS sheet: {:?}", sports); + + for sport in sports.iter() { + let examples: Vec<_> = teams_sheet + .rows + .iter() + .filter(|r| r.get("SPORT") == Some(*sport)) + .take(5) + .filter_map(|r| r.get("TEAM")) + .collect(); + debug!(" TEAMS examples for '{}': {:?}", sport, examples); + } + } + + // Загружаем channel logos let channels: HashMap = channel_sheet .rows .iter() @@ -320,11 +365,37 @@ async fn generate_and_submit_jobs( .collect(); info!("Loaded {} channel logos", channels.len()); + if log_enabled!(Level::Debug) { + debug!("Channels loaded: {:?}", channels.keys().collect::>()); + } + let mut jobs: Vec = Vec::with_capacity(start_sheet.rows.len() * 3); + // Собираем статистику по ненайденным логотипам для дебага + let mut missing_teams: HashSet = HashSet::new(); + let mut missing_sports: HashSet = HashSet::new(); + for (idx, row) in start_sheet.rows.iter().enumerate() { if let Some(state) = row.get("STATE") { if state == "FALSE" { + let team_a = row.get("TEAM A").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); + let logo_b = logos.find(&team_b, &sport); + + if !team_a.is_empty() && logo_a.is_none() { + 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()); } @@ -332,6 +403,18 @@ async fn generate_and_submit_jobs( } } + // Дебаг: выводим сводку по отсутствующим логотипам + if log_enabled!(Level::Debug) && !missing_teams.is_empty() { + debug!("=== MISSING LOGOS SUMMARY ==="); + debug!("Total unique teams missing logos: {}", missing_teams.len()); + debug!("Sports involved: {:?}", missing_sports); + debug!( + "Missing teams (first 20): {:?}", + missing_teams.iter().take(20).collect::>() + ); + debug!("=============================="); + } + info!("Generated {} total jobs (including variants)", jobs.len()); if jobs.is_empty() { @@ -354,6 +437,13 @@ async fn generate_and_submit_jobs( let nexrender_job = job.to_nexrender_job(&config_clone); info!("Submitting job: {}", job.outfile_name); + if log_enabled!(Level::Debug) { + debug!( + "Job details: sport='{}', league='{}', channel='{}', team_a='{}', team_b='{}'", + job.sport, job.league, job.channel, job.team_a, job.team_b + ); + } + let response = client.post(&api_url).json(&nexrender_job).send().await?; if response.status().is_success() {