улучшено debug-логирование и обработка путей
- Добавлен детальный вывод при парсинге листа TEAMS - Добавлен вывод поиска логотипов и сводка по отсутствующим - Исправлен порядок инициализации RUST_LOG - Добавлена поддержка smb:// путей в unc_to_uri
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use dotenv::dotenv;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -24,8 +23,6 @@ pub struct Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn from_env() -> Result<Self> {
|
pub fn from_env() -> Result<Self> {
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
nas_fqdn: env::var("NAS_FQDN").context("NAS_FQDN not set")?,
|
nas_fqdn: env::var("NAS_FQDN").context("NAS_FQDN not set")?,
|
||||||
nas_user: env::var("NAS_USER").context("NAS_USER not set")?,
|
nas_user: env::var("NAS_USER").context("NAS_USER not set")?,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod web;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
use dotenv::dotenv;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -23,6 +24,10 @@ struct Cli {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
// Загружаем .env до инициализации логгера
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
// Инициализируем логгер
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -35,7 +40,6 @@ async fn main() -> Result<()> {
|
|||||||
);
|
);
|
||||||
web::run_web_server(config).await?;
|
web::run_web_server(config).await?;
|
||||||
} else {
|
} else {
|
||||||
// По умолчанию или с флагом --once выполняем однократную обработку
|
|
||||||
info!(
|
info!(
|
||||||
"Starting AE Anons processor v{} (one-time mode)",
|
"Starting AE Anons processor v{} (one-time mode)",
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
|
|||||||
@@ -537,15 +537,23 @@ 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);
|
||||||
|
|
||||||
if let Some(link) = self.logos.get(&(team.to_string(), sport.to_string())) {
|
if let Some(link) = self.logos.get(&(team.to_string(), sport.to_string())) {
|
||||||
|
log::debug!("Found exact match: {}", link);
|
||||||
return Some(link.clone());
|
return Some(link.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((t, s), link) in &self.logos {
|
for ((t, s), link) in &self.logos {
|
||||||
if s == sport && team.starts_with(t) {
|
if s == sport && team.starts_with(t) {
|
||||||
|
log::debug!("Found prefix match: t='{}', link='{}'", t, link);
|
||||||
return Some(link.clone());
|
return Some(link.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::warn!("No logo found for team='{}', sport='{}'", team, sport);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,12 +611,26 @@ fn unc_to_uri(path: &str) -> String {
|
|||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Уже правильный URI
|
||||||
if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") {
|
if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("file://") {
|
||||||
return path.to_string();
|
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 {
|
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]",
|
||||||
|
|||||||
106
src/processor.rs
106
src/processor.rs
@@ -4,10 +4,10 @@ use crate::synology::SynologyClient;
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use calamine::{Data, Reader, Xlsx};
|
use calamine::{Data, Reader, Xlsx};
|
||||||
use chrono::{Duration, NaiveDate};
|
use chrono::{Duration, NaiveDate};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info, log_enabled, Level};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -211,10 +211,8 @@ fn cell_to_string_optimized(cell: &Data) -> String {
|
|||||||
if is_excel_date(*f) {
|
if is_excel_date(*f) {
|
||||||
excel_date_to_string(*f)
|
excel_date_to_string(*f)
|
||||||
} else if f.fract() == 0.0 {
|
} else if f.fract() == 0.0 {
|
||||||
// Используем itoa для целых чисел (опционально)
|
|
||||||
format!("{:.0}", f)
|
format!("{:.0}", f)
|
||||||
} else {
|
} else {
|
||||||
// Используем ryu для float (опционально)
|
|
||||||
f.to_string()
|
f.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +254,6 @@ fn excel_date_to_string(serial: f64) -> String {
|
|||||||
let days = serial as i64;
|
let days = serial as i64;
|
||||||
let base = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap();
|
let base = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap();
|
||||||
if let Some(date) = base.checked_add_signed(Duration::days(days)) {
|
if let Some(date) = base.checked_add_signed(Duration::days(days)) {
|
||||||
// Используем метод format напрямую - он публичный
|
|
||||||
date.format("%d.%m.%Y").to_string()
|
date.format("%d.%m.%Y").to_string()
|
||||||
} else {
|
} else {
|
||||||
serial.to_string()
|
serial.to_string()
|
||||||
@@ -296,6 +293,7 @@ async fn generate_and_submit_jobs(
|
|||||||
.get_sheet("CHANELL")
|
.get_sheet("CHANELL")
|
||||||
.ok_or_else(|| anyhow!("Sheet 'CHANELL' not found"))?;
|
.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()
|
||||||
@@ -303,16 +301,63 @@ async fn generate_and_submit_jobs(
|
|||||||
.collect();
|
.collect();
|
||||||
info!("Loaded {} sport packs", packs.len());
|
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());
|
let mut logos = LogoRegistry::with_capacity(teams_sheet.rows.len());
|
||||||
for row in &teams_sheet.rows {
|
for row in &teams_sheet.rows {
|
||||||
if let (Some(team), Some(sport), Some(link)) =
|
let team = get_cell_fuzzy(row, "TEAM");
|
||||||
(row.get("TEAM"), row.get("SPORT"), row.get("LINK"))
|
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());
|
logos.insert(team.clone(), sport.clone(), link.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!("Loaded {} team logos", teams_sheet.rows.len());
|
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
|
let channels: HashMap<String, String> = channel_sheet
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -320,11 +365,37 @@ async fn generate_and_submit_jobs(
|
|||||||
.collect();
|
.collect();
|
||||||
info!("Loaded {} channel logos", channels.len());
|
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 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() {
|
for (idx, row) in start_sheet.rows.iter().enumerate() {
|
||||||
if let Some(state) = row.get("STATE") {
|
if let Some(state) = row.get("STATE") {
|
||||||
if state == "FALSE" {
|
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) {
|
if let Some(job) = JobData::from_row(row, idx, &packs, &logos, &channels) {
|
||||||
jobs.extend(job.create_variants());
|
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());
|
info!("Generated {} total jobs (including variants)", jobs.len());
|
||||||
|
|
||||||
if jobs.is_empty() {
|
if jobs.is_empty() {
|
||||||
@@ -354,6 +437,13 @@ async fn generate_and_submit_jobs(
|
|||||||
let nexrender_job = job.to_nexrender_job(&config_clone);
|
let nexrender_job = job.to_nexrender_job(&config_clone);
|
||||||
info!("Submitting job: {}", job.outfile_name);
|
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?;
|
let response = client.post(&api_url).json(&nexrender_job).send().await?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
|
|||||||
Reference in New Issue
Block a user