2 Commits

Author SHA1 Message Date
0f2fe50eab chore: bump version to 0.2.3 2026-04-17 16:29:03 +03:00
fbcb619544 улучшено debug-логирование и обработка путей
- Добавлен детальный вывод при парсинге листа TEAMS
- Добавлен вывод поиска логотипов и сводка по отсутствующим
- Исправлен порядок инициализации RUST_LOG
- Добавлена поддержка smb:// путей в unc_to_uri
2026-04-17 16:00:46 +03:00
5 changed files with 129 additions and 16 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "ae_anons"
version = "0.2.2"
version = "0.2.3"
edition = "2021"
license = "MIT"
license-file = "LICENSE"

View File

@@ -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<Self> {
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")?,

View File

@@ -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(())
}
}

View File

@@ -537,15 +537,23 @@ impl LogoRegistry {
self.logos.insert((team, sport), link);
}
// В impl LogoRegistry, функция find()
pub fn find(&self, team: &str, sport: &str) -> Option<String> {
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]",

View File

@@ -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<String, String> = 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<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
}
// Загружаем 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<String, String> = 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::<Vec<_>>());
}
let mut jobs: Vec<JobData> = Vec::with_capacity(start_sheet.rows.len() * 3);
// Собираем статистику по ненайденным логотипам для дебага
let mut missing_teams: HashSet<String> = HashSet::new();
let mut missing_sports: HashSet<String> = 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::<Vec<_>>()
);
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() {