diff --git a/.gitignore b/.gitignore index 8a2b302..a1079ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,22 @@ -/target -.env .DS_Store *.xlsx -*.osheet +*.txt +# Файлы окружения +.env + +# Логи +logs/ +*.log + +# Выходные файлы +output/ +*.xlsx *.json -*.txt \ No newline at end of file + +# IDE +.vscode/ +.idea/ +*.swp + +# Скомпилированные файлы +target/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 095f161..7d04a30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,9 @@ dependencies = [ "calamine", "chrono", "dotenv", + "env_logger", "futures", + "log", "regex", "reqwest", "serde", @@ -44,6 +46,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -154,6 +206,12 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "core-foundation" version = "0.9.4" @@ -232,6 +290,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -705,12 +786,42 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -831,6 +942,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl" version = "0.10.77" @@ -916,6 +1033,21 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1530,6 +1662,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 4e5fcd4..985b28a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ chrono = "0.4" regex = "1.11" futures = "0.3" anyhow = "1.0" +log = "0.4" +env_logger = "0.11" [dev-dependencies] tokio = { version = "1.0", features = ["full", "rt-multi-thread"] } \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..9168f07 --- /dev/null +++ b/src/README.md @@ -0,0 +1,346 @@ +# AE Anons - Automated After Effects Announcement Generator + +Automated pipeline for generating sports announcements using After Effects templates via Nexrender, with data sourced from Synology Office spreadsheets. + +## Overview + +AE Anons automates the creation of broadcast announcement videos by: + +1. Connecting to Synology NAS to retrieve schedule data from Office spreadsheets (.osheet) +2. Parsing Excel data containing sports events, teams, channels and timing information +3. Generating Nexrender jobs with appropriate templates and assets +4. Monitoring render completion and managing output files + +## Features + +- **Synology Integration**: Seamless authentication and file retrieval from Synology NAS +- **Office Spreadsheet Export**: Automatic conversion of .osheet files to Excel format +- **Flexible Data Parsing**: Dynamic sheet parsing with header detection +- **Multi-variant Generation**: Creates "Today", "Tomorrow" and dated variants for each announcement +- **Smart Logo Management**: Automatic logo resolution and scaling based on team/sport associations +- **Nexrender Job Orchestration**: Automated job submission, monitoring and cleanup +- **Professional Logging**: Structured logging with configurable verbosity levels +- **Environment-based Configuration**: All settings managed via `.env` file + +## Prerequisites + +- Rust 1.70 or higher +- Access to Synology NAS with File Station and Office enabled +- Nexrender server instance +- After Effects templates configured on render nodes + +## Installation + +### 1. Clone the Repository + +```bash +git clone https://github.com/your-org/ae_anons.git +cd ae_anons + +2. Build the Project +bash + +cargo build --release + +The binary will be available at target/release/ae_anons +3. Configure Environment + +Create a .env file in the project root: +env + +NAS_FQDN=https://your-synology-nas.example.com:5001 +NAS_USER=service_account +NAS_PASS=secure_password +NAS_FILE=/Team Folder/Broadcast/Auto_Anons/schedule.osheet +NEXRENDER_API_URL=http://nexrender-server:3000/api/v1/jobs +OUTPUT_FOLDER=//file-server/edit/Auto_Anons +RUST_LOG=info + +See Configuration section for detailed options. +Usage +Basic Execution +bash + +# Run with default configuration +./target/release/ae_anons + +# Run with custom log level +RUST_LOG=debug ./target/release/ae_anons + +# Run with specific .env file +dotenv -f /path/to/.env.production run ./target/release/ae_anons + +Development Mode +bash + +cargo run + +Logging Levels + +Control output verbosity via RUST_LOG environment variable: + + error - Only critical errors + + warn - Warnings and errors + + info - General operational messages (default) + + debug - Detailed processing information + + trace - Full debugging with API call details + +bash + +RUST_LOG=debug cargo run + +Configuration +Environment Variables +Variable Required Default Description +NAS_FQDN Yes - Synology NAS URL with protocol and port +NAS_USER Yes - Synology account username +NAS_PASS Yes - Synology account password +NAS_FILE Yes - Full path to .osheet file on NAS +NEXRENDER_API_URL +OUTPUT_FOLDER Network path for rendered videos +RUST_LOG No info Logging verbosity level +Spreadsheet Structure + +The input Excel file (converted from .osheet) must contain the following sheets: +Start Sheet + +Main data source for announcements generation. +Column Description Example +STATE Processing flag FALSE (active), TRUE (skip) +SPORT Sport category Футбол, Хоккей +LEAGUE League name Премьер-лига +TEAM A Home team Спартак#FC Spartak#150 +TEAM B Away team Зенит#FC Zenit#150 +CHANEL Broadcast channel Матч ТВ +TIME Event time 19:30 +DATA Event date 15.04.2026 +SPORT Sheet + +Mapping between sports and their video pack templates. +Column Description +SPORT Sport identifier +LINK Path to video pack file +TEAMS Sheet + +Team logo registry with sport associations. +Column Description +TEAM Team identifier +SPORT Associated sport +LINK Path to team logo file +CHANELL Sheet + +Channel logo mappings. +Column Description +CHANELL Channel name +LINK Path to channel logo file +Team Name Format + +Team names can include resolution hints using hash separators: +text + +Display Name#Search Key#Target Size + +Example: Спартак#FC Spartak#150 + + Спартак - Display name in graphics + + FC Spartak - Key for logo lookup + + 150 - Target size in pixels for logo scaling + +Output Files +JSON Exports + +During processing, JSON representations of the workbook are saved: + + {filename}_workbook.json - Complete workbook structure + + {filename}_{SheetName}.json - Individual sheet data + +Rendered Videos + +Output videos are saved to OUTPUT_FOLDER with naming pattern: +text + +YYYYMMDD_Sport_League_TeamA_TeamB_Channel[_Variant].mp4 + +Examples: + + 20260415_Футбол_Премьер-лига_Спартак_Зенит_Матч-ТВ.mp4 (Base version) + + 20260415_Футбол_Премьер-лига_Спартак_Зенит_Матч-ТВ_Today.mp4 + + 20260415_Футбол_Премьер-лига_Спартак_Зенит_Матч-ТВ_Tomorrow.mp4 + +Workflow + + Authentication: Connects to Synology NAS using provided credentials + + File Retrieval: Locates and exports the specified .osheet file as Excel + + Data Parsing: Reads all sheets and structures the data + + Asset Resolution: Matches sports, teams and channels with their visual assets + + Job Generation: Creates Nexrender jobs for each active row with variants + + Cleanup: Removes completed/failed jobs from previous runs + + Submission: Sends jobs to Nexrender API + + Monitoring: Tracks job progress until completion + + Logout: Terminates Synology session + +After Effects Template Requirements + +Templates must be pre-configured on render nodes with specific layer names: +Template Types +Template File Use Case +Double Team PackShot_DOUBLE.aepx Matches with two teams +Single Team PackShot_SINGLE.aepx Single team announcements +Required Layers +Layer Name Type Description +DATA Text Date display (auto-adjusted) +TIME / TIME_H / TIME_M Text Time display +LEAGUE Text League name +SPORT Text Sport category +TEAMS Text Combined team names +TEAM_A_LOGO Image Home team logo +TEAM_B_LOGO Image Away team logo +CHANELL Image Channel logo +TOP Video Sport pack overlay +Composition Settings + + Composition name: pack + + Output module: Start_h264 + + Output format: mp4 + +Troubleshooting +Common Issues +Connection to NAS failed + + Verify NAS_FQDN includes protocol and port (e.g., https://nas.example.com:5001) + + Check network connectivity to NAS + + Ensure File Station and Office services are enabled + +File not found + + Verify the path in NAS_FILE exactly matches the Synology Drive path + + Path should start with /Team Folder/ for team folders + + Check file permissions for the service account + +Nexrender job submission fails + + Confirm Nexrender server is accessible + + Verify NEXRENDER_API_URL is correct + + Check that template files exist on render nodes + +Debug Mode + +Enable debug logging for detailed troubleshooting: +bash + +RUST_LOG=debug ./target/release/ae_anons + +This will output: + + API request/response details + + Sheet parsing information + + Job creation details + + Asset resolution process + +Performance Considerations + + Large Spreadsheets: Processing limited to 10,000 rows per sheet + + Network Latency: File downloads from NAS may take time for large files + + Concurrent Jobs: Nexrender handles job queuing internally + + Memory Usage: Excel parsing keeps entire workbook in memory + +Security Notes + + Store credentials only in .env file (excluded from git) + + Use dedicated service accounts with minimal required permissions + + Synology sessions are properly terminated after execution + + HTTPS recommended for NAS connections in production + +Development +Running Tests +bash + +cargo test + +Code Structure +text + +src/ +├── main.rs # Application entry point and orchestration +├── config.rs # Configuration management +├── nexrender.rs # Nexrender job generation and structures +└── synology.rs # Synology API client + +Adding New Features + + Extend JobData in nexrender.rs for new data fields + + Update sheet parsing logic if new columns are required + + Add corresponding After Effects layers to templates + + Update to_nexrender_job() method with new asset mappings + +Dependencies +Crate Version Purpose +reqwest 0.12 HTTP client for API communication +serde / serde_json 1.0 JSON serialization +calamine 0.26 Excel file parsing +chrono 0.4 Date/time handling +tokio 1.0 Async runtime +dotenv 0.15 Environment configuration +log / env_logger 0.4 Logging infrastructure +thiserror 2.0 Error type definitions +anyhow 1.0 Error handling +License + +[Specify your license here] +Support + +For issues and feature requests, please contact: + + [Your Team Email] + + [Internal Documentation Link] + +Changelog +v0.1.0 + + Initial release + + Synology Office integration + + Basic Nexrender job generation + + Excel parsing with dynamic sheet detection + + Multi-variant job creation diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..60a5ab5 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,28 @@ +use anyhow::{Context, Result}; +use dotenv::dotenv; +use std::env; + +#[derive(Debug, Clone)] +pub struct Config { + pub nas_fqdn: String, + pub nas_user: String, + pub nas_pass: String, + pub nas_file: String, + pub nexrender_api_url: String, + pub output_folder: String, +} + +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")?, + nas_pass: env::var("NAS_PASS").context("NAS_PASS not set")?, + nas_file: env::var("NAS_FILE").context("NAS_FILE not set")?, + nexrender_api_url: env::var("NEXRENDER_API_URL").context("NEXRENDER_API_URL not set")?, + output_folder: env::var("OUTPUT_FOLDER").context("OUTPUT_FOLDER not set")?, + }) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4e24aec..1df5b98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ +mod config; mod nexrender; mod synology; use anyhow::{anyhow, Context, Result}; use calamine::{Data, Reader, Xlsx}; use chrono::{Duration, NaiveDate}; -use dotenv::dotenv; +use config::Config; +use log::{debug, error, info}; use nexrender::{JobData, LogoRegistry}; use reqwest::Client; use serde_json::Value; use std::collections::HashMap; -use std::env; use std::fs::File; use std::io::Write; use std::path::Path; @@ -17,10 +18,6 @@ use std::time::Duration as StdDuration; use synology::SynologyClient; use tokio::time::sleep; -const NEXRENDER_API: &str = "http://10.10.2.20:3000/api/v1/jobs"; -const OUTPUT_FOLDER: &str = "//10.10.35.3/edit/Auto_Anons"; - -/// Универсальная структура для хранения данных любого листа #[derive(Debug, Clone)] pub struct SheetData { pub name: String, @@ -70,26 +67,41 @@ impl ExcelWorkbook { #[tokio::main] async fn main() -> Result<()> { - dotenv().ok(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - let nas_fqdn = env::var("NAS_FQDN").context("NAS_FQDN not set")?; - let nas_user = env::var("NAS_USER").context("NAS_USER not set")?; - let nas_pass = env::var("NAS_PASS").context("NAS_PASS not set")?; - let nas_file = env::var("NAS_FILE").context("NAS_FILE not set")?; + info!("Starting AE Anons processor"); + let config = Config::from_env()?; + debug!("Configuration loaded: {:?}", config); - let mut client = SynologyClient::new(&nas_fqdn); - client.login(&nas_user, &nas_pass).await?; - println!("✅ Logged in"); + let mut client = SynologyClient::new(&config.nas_fqdn); + client.login(&config.nas_user, &config.nas_pass).await?; + info!("Successfully authenticated with Synology NAS"); let info = client.get_info().await?; - println!("📡 Connected to: {}", info.hostname); + info!("Connected to NAS: {}", info.hostname); - let file_name_full = Path::new(&nas_file) + let workbook = download_and_parse_excel(&mut client, &config).await?; + display_workbook_structure(&workbook); + save_workbook_json(&workbook, &config.nas_file)?; + + process_nexrender_jobs(&workbook, &config).await?; + + client.logout().await?; + info!("Session terminated successfully"); + + Ok(()) +} + +async fn download_and_parse_excel( + client: &mut SynologyClient, + config: &Config, +) -> Result { + let file_name_full = Path::new(&config.nas_file) .file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown"); let search_name = file_name_full.replace(".osheet", ""); - let expected_path = Path::new(&nas_file) + let expected_path = Path::new(&config.nas_file) .parent() .and_then(|p| p.to_str()) .unwrap_or(""); @@ -99,7 +111,7 @@ async fn main() -> Result<()> { .replace(' ', ""); let full_expected_path_api = format!("{}/{}", api_format_path, file_name_full); - println!("\n🔍 Looking for file: {}", full_expected_path_api); + info!("Searching for file: {}", full_expected_path_api); let search_result = client.search_file_by_name(&search_name).await?; let items = search_result["data"]["items"] @@ -132,37 +144,47 @@ async fn main() -> Result<()> { .as_str() .ok_or_else(|| anyhow!("Missing name"))?; - println!(" 📄 Found: {}", actual_file_name); + info!("Found file: {}", actual_file_name); + info!("Exporting file from Synology Office to Excel format..."); - println!("\n📥 Exporting file..."); let output_path = format!("./{}_export.xlsx", search_name); - let data = client.export_by_file_id(file_id, actual_file_name).await?; let mut file = File::create(&output_path)?; file.write_all(&data)?; - println!(" ✅ Exported to: {} ({} bytes)", output_path, data.len()); + info!("Exported to: {} ({} bytes)", output_path, data.len()); - println!("\n📖 Parsing Excel file..."); - let workbook = read_excel_workbook(&output_path)?; + info!("Parsing Excel workbook..."); + read_excel_workbook(&output_path) +} - println!("\n📊 Excel Structure:"); - println!(" Total sheets: {}", workbook.sheets.len()); +fn display_workbook_structure(workbook: &ExcelWorkbook) { + info!("Workbook contains {} sheets", workbook.sheets.len()); for (idx, sheet) in workbook.sheets.iter().enumerate() { - println!("\n Sheet #{}: '{}'", idx + 1, sheet.name); - println!( - " Headers ({}): {:?}", + debug!( + "Sheet #{}: '{}' | Headers: {} | Rows: {}", + idx + 1, + sheet.name, sheet.headers.len(), - sheet.headers + sheet.rows.len() ); - println!(" Rows: {}", sheet.rows.len()); + if log::log_enabled!(log::Level::Debug) { + debug!(" Headers: {:?}", sheet.headers); + } } +} + +fn save_workbook_json(workbook: &ExcelWorkbook, nas_file: &str) -> Result<()> { + let search_name = Path::new(nas_file) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .replace(".osheet", ""); - // Сохраняем JSON let json_output = format!("./{}_workbook.json", search_name); let json_data = workbook.to_json(); std::fs::write(&json_output, serde_json::to_string_pretty(&json_data)?)?; - println!("\n💾 Full workbook saved to: {}", json_output); + info!("Full workbook saved to: {}", json_output); for sheet in &workbook.sheets { let safe_name = sheet @@ -175,8 +197,11 @@ async fn main() -> Result<()> { )?; } - // ============ NEXRENDER INTEGRATION ============ - println!("\n🎬 Creating Nexrender jobs..."); + Ok(()) +} + +async fn process_nexrender_jobs(workbook: &ExcelWorkbook, config: &Config) -> Result<()> { + info!("Preparing Nexrender jobs..."); let start_sheet = workbook .get_sheet("Start") @@ -187,7 +212,7 @@ async fn main() -> Result<()> { let teams_sheet = workbook .get_sheet("TEAMS") .ok_or_else(|| anyhow!("Sheet 'TEAMS' not found"))?; - let chanell_sheet = workbook + let channel_sheet = workbook .get_sheet("CHANELL") .ok_or_else(|| anyhow!("Sheet 'CHANELL' not found"))?; @@ -196,7 +221,7 @@ async fn main() -> Result<()> { .iter() .filter_map(|row| Some((row.get("SPORT")?.clone(), row.get("LINK")?.clone()))) .collect(); - println!(" 📦 Loaded {} sport packs", packs.len()); + info!("Loaded {} sport packs", packs.len()); let mut logos = LogoRegistry::new(); for row in &teams_sheet.rows { @@ -206,48 +231,45 @@ async fn main() -> Result<()> { logos.insert(team.clone(), sport.clone(), link.clone()); } } - println!(" 🏷️ Loaded {} team logos", teams_sheet.rows.len()); + info!("Loaded {} team logos", teams_sheet.rows.len()); - // Загружаем логотипы каналов из листа CHANELL - let channels: HashMap = chanell_sheet + let channels: HashMap = channel_sheet .rows .iter() .filter_map(|row| Some((row.get("CHANELL")?.clone(), row.get("LINK")?.clone()))) .collect(); - println!(" 📺 Loaded {} channel logos", channels.len()); + info!("Loaded {} channel logos", channels.len()); let mut jobs: Vec = Vec::new(); for (idx, row) in start_sheet.rows.iter().enumerate() { if let Some(state) = row.get("STATE") { if state == "FALSE" { if let Some(job) = JobData::from_row(row, idx, &packs, &logos, &channels) { - println!(" ✅ Job for row {}: {}", idx, job.outfile_name); + debug!("Created job for row {}: {}", idx, job.outfile_name); jobs.extend(job.create_variants()); } } } } - println!("\n📦 Created {} total jobs (with variants)", jobs.len()); + info!("Generated {} total jobs (including variants)", jobs.len()); if jobs.is_empty() { - println!(" ℹ️ No jobs with STATE='FALSE' found"); - client.logout().await?; - println!("\n👋 Logged out"); + info!("No jobs with STATE='FALSE' found"); return Ok(()); } - cleanup_finished_jobs().await?; + cleanup_finished_jobs(&config.nexrender_api_url).await?; let http_client = Client::new(); let mut submitted_jobs: Vec<(String, String)> = Vec::new(); for job in &jobs { - let nexrender_job = job.to_nexrender_job(OUTPUT_FOLDER); - println!(" 📤 Submitting: {}", job.outfile_name); + let nexrender_job = job.to_nexrender_job(&config.output_folder); + info!("Submitting job: {}", job.outfile_name); let response = http_client - .post(NEXRENDER_API) + .post(&config.nexrender_api_url) .json(&nexrender_job) .send() .await?; @@ -256,25 +278,25 @@ async fn main() -> Result<()> { let result: Value = response.json().await?; if let Some(uid) = result.get("uid").and_then(|u| u.as_str()) { submitted_jobs.push((uid.to_string(), job.outfile_name.clone())); - println!(" ✅ Submitted, uid: {}", uid); + info!("Job submitted successfully, UID: {}", uid); } } else { let status = response.status(); let text = response.text().await.unwrap_or_default(); - eprintln!(" ❌ Failed to submit job ({}): {}", status, text); + error!("Failed to submit job ({}): {}", status, text); } } - println!("\n📊 Submitted {} jobs to nexrender", submitted_jobs.len()); + info!( + "Successfully submitted {} jobs to Nexrender", + submitted_jobs.len() + ); if !submitted_jobs.is_empty() { - println!("\n⏳ Monitoring job completion..."); - monitor_jobs(&http_client, submitted_jobs).await?; + info!("Monitoring job completion..."); + monitor_jobs(&http_client, &config.nexrender_api_url, submitted_jobs).await?; } - client.logout().await?; - println!("\n👋 Done"); - Ok(()) } @@ -286,7 +308,7 @@ fn read_excel_workbook(file_path: &str) -> Result { let mut excel_workbook = ExcelWorkbook::default(); for sheet_name in sheet_names { - println!(" 📄 Processing sheet: '{}'", sheet_name); + debug!("Processing sheet: '{}'", sheet_name); let range = workbook .worksheet_range(&sheet_name) @@ -420,9 +442,9 @@ fn cell_to_string(cell: &Data) -> String { } } -async fn cleanup_finished_jobs() -> Result<()> { +async fn cleanup_finished_jobs(api_url: &str) -> Result<()> { let client = Client::new(); - let response = client.get(NEXRENDER_API).send().await?; + let response = client.get(api_url).send().await?; if response.status().is_success() { let jobs: Vec = response.json().await?; @@ -433,10 +455,10 @@ async fn cleanup_finished_jobs() -> Result<()> { ) { if state == "finished" || state == "error" { client - .delete(&format!("{}/{}", NEXRENDER_API, uid)) + .delete(&format!("{}/{}", api_url, uid)) .send() .await?; - println!(" 🧹 Cleaned up job: {}", uid); + info!("Cleaned up completed job: {}", uid); } } } @@ -444,16 +466,17 @@ async fn cleanup_finished_jobs() -> Result<()> { Ok(()) } -async fn monitor_jobs(client: &Client, mut pending: Vec<(String, String)>) -> Result<()> { +async fn monitor_jobs( + client: &Client, + api_url: &str, + mut pending: Vec<(String, String)>, +) -> Result<()> { while !pending.is_empty() { sleep(StdDuration::from_secs(25)).await; let mut remaining = Vec::new(); for (uid, outname) in &pending { - let response = client - .get(&format!("{}/{}", NEXRENDER_API, uid)) - .send() - .await?; + let response = client.get(&format!("{}/{}", api_url, uid)).send().await?; if response.status().is_success() { let job: Value = response.json().await?; @@ -464,9 +487,13 @@ async fn monitor_jobs(client: &Client, mut pending: Vec<(String, String)>) -> Re match state { "finished" => { - println!(" ✅ {} - finished ({} left)", outname, pending.len() - 1) + info!( + "Job completed: {} ({} remaining)", + outname, + pending.len() - 1 + ) } - "error" => eprintln!(" ❌ {} - error ({} left)", outname, pending.len() - 1), + "error" => error!("Job failed: {} ({} remaining)", outname, pending.len() - 1), _ => remaining.push((uid.clone(), outname.clone())), } } else { @@ -475,9 +502,9 @@ async fn monitor_jobs(client: &Client, mut pending: Vec<(String, String)>) -> Re } pending = remaining; if !pending.is_empty() { - println!(" ⏳ Waiting for {} jobs...", pending.len()); + info!("Waiting for {} jobs to complete...", pending.len()); } } - println!("\n🎉 All jobs completed!"); + info!("All jobs completed successfully"); Ok(()) } diff --git a/src/synology.rs b/src/synology.rs index f97c0e8..aec1913 100644 --- a/src/synology.rs +++ b/src/synology.rs @@ -1,3 +1,4 @@ +use log; use reqwest::{ multipart::{Form, Part}, Client, ClientBuilder, @@ -457,19 +458,14 @@ impl SynologyClient { url = format!("{}?{}", url, param_parts.join("&")); } - let debug = std::env::var("SYNO_DEBUG").unwrap_or_default() == "1"; - if debug { - eprintln!("[DEBUG] Request URL: {}", url); - } + log::debug!("Request URL: {}", url); let response = self.client.get(&url).send().await?; let status = response.status(); if !status.is_success() { let error_text = response.text().await?; - if debug { - eprintln!("[DEBUG] Error response: {}", error_text); - } + log::debug!("Error response: {}", error_text); return Err(SynologyError::Api { code: status.as_u16() as i64, message: Some(error_text), @@ -477,10 +473,7 @@ impl SynologyClient { } let response_text = response.text().await?; - - if debug { - eprintln!("[DEBUG] Response: {}", response_text); - } + log::debug!("Response: {}", response_text); let api_response: ApiResponse = serde_json::from_str(&response_text) .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; @@ -501,7 +494,7 @@ impl SynologyClient { }) } } - + /// Выполнение запроса с SID async fn request_auth( &self, @@ -1252,8 +1245,7 @@ impl SynologyClient { self.sid.as_ref().unwrap_or(&String::new()) ); - println!(" 🔍 Searching for file: {} in path: {}", name, path); - + log::debug!("Searching for file: {} in path: {}", name, path); let response = self .client .post(&url) @@ -1287,7 +1279,7 @@ impl SynologyClient { self.sid.as_ref().unwrap_or(&String::new()) ); - println!(" 🔍 Searching for file: {}", name); + log::debug!("Searching for file: {}", name); let response = self .client @@ -1325,8 +1317,7 @@ impl SynologyClient { ); let url = format!("{}/webapi/{}", self.base_url, endpoint); - println!(" 📤 Export URL: {}", url); - + log::debug!("Export URL: {}", url); let response = self .client .post(&url) @@ -1346,8 +1337,7 @@ impl SynologyClient { } let bytes = response.bytes().await?; - println!(" ✅ Got {} bytes", bytes.len()); - + log::debug!("Received {} bytes", bytes.len()); Ok(bytes.to_vec()) } }