Причёсинг
This commit is contained in:
346
src/README.md
Normal file
346
src/README.md
Normal file
@@ -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
|
||||
28
src/config.rs
Normal file
28
src/config.rs
Normal file
@@ -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<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")?,
|
||||
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")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
171
src/main.rs
171
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<ExcelWorkbook> {
|
||||
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<String, String> = chanell_sheet
|
||||
let channels: HashMap<String, String> = 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<JobData> = 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<ExcelWorkbook> {
|
||||
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<Value> = 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(())
|
||||
}
|
||||
|
||||
@@ -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<T> = 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<T: DeserializeOwned + Default>(
|
||||
&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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user